615 lines
19 KiB
Go
615 lines
19 KiB
Go
// Package authdirectory implements the auth-facing user-resolution, ensure,
|
|
// existence, and block use cases owned by the user service.
|
|
package authdirectory
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"galaxy/user/internal/domain/account"
|
|
"galaxy/user/internal/domain/common"
|
|
"galaxy/user/internal/domain/entitlement"
|
|
"galaxy/user/internal/ports"
|
|
"galaxy/user/internal/service/shared"
|
|
"galaxy/user/internal/telemetry"
|
|
)
|
|
|
|
const (
|
|
initialEntitlementSource common.Source = "auth_registration"
|
|
initialEntitlementReasonCode common.ReasonCode = "initial_free_entitlement"
|
|
initialEntitlementActorType common.ActorType = "service"
|
|
initialEntitlementActorID common.ActorID = "user-service"
|
|
|
|
ensureCreateRetryLimit = 8
|
|
)
|
|
|
|
// ResolveByEmailInput stores one auth-facing resolve-by-email request.
|
|
type ResolveByEmailInput struct {
|
|
// Email stores the caller-supplied e-mail subject.
|
|
Email string
|
|
}
|
|
|
|
// ResolveByEmailResult stores one auth-facing resolve-by-email response.
|
|
type ResolveByEmailResult struct {
|
|
// Kind stores the coarse user-resolution outcome.
|
|
Kind string
|
|
|
|
// UserID is present only when Kind is `existing`.
|
|
UserID string
|
|
|
|
// BlockReasonCode is present only when Kind is `blocked`.
|
|
BlockReasonCode string
|
|
}
|
|
|
|
// Resolver executes the auth-facing resolve-by-email use case.
|
|
type Resolver struct {
|
|
store ports.AuthDirectoryStore
|
|
logger *slog.Logger
|
|
telemetry *telemetry.Runtime
|
|
}
|
|
|
|
// NewResolver returns one resolve-by-email use case instance.
|
|
func NewResolver(store ports.AuthDirectoryStore) (*Resolver, error) {
|
|
return NewResolverWithObservability(store, nil, nil)
|
|
}
|
|
|
|
// NewResolverWithObservability returns one resolve-by-email use case instance
|
|
// with optional structured logging and metrics hooks.
|
|
func NewResolverWithObservability(
|
|
store ports.AuthDirectoryStore,
|
|
logger *slog.Logger,
|
|
telemetryRuntime *telemetry.Runtime,
|
|
) (*Resolver, error) {
|
|
if store == nil {
|
|
return nil, fmt.Errorf("authdirectory resolver: auth directory store must not be nil")
|
|
}
|
|
|
|
return &Resolver{
|
|
store: store,
|
|
logger: logger,
|
|
telemetry: telemetryRuntime,
|
|
}, nil
|
|
}
|
|
|
|
// Execute resolves one e-mail subject without creating any account.
|
|
func (service *Resolver) Execute(ctx context.Context, input ResolveByEmailInput) (result ResolveByEmailResult, err error) {
|
|
outcome := "failed"
|
|
defer func() {
|
|
if service.telemetry != nil {
|
|
service.telemetry.RecordAuthResolutionOutcome(ctx, "resolve_by_email", outcome)
|
|
}
|
|
if err != nil {
|
|
shared.LogServiceOutcome(service.logger, ctx, "auth resolution failed", err,
|
|
"use_case", "resolve_by_email",
|
|
"outcome", outcome,
|
|
)
|
|
}
|
|
}()
|
|
|
|
if ctx == nil {
|
|
return ResolveByEmailResult{}, shared.InvalidRequest("context must not be nil")
|
|
}
|
|
|
|
email, err := shared.ParseEmail(input.Email)
|
|
if err != nil {
|
|
return ResolveByEmailResult{}, err
|
|
}
|
|
|
|
resolution, err := service.store.ResolveByEmail(ctx, email)
|
|
if err != nil {
|
|
return ResolveByEmailResult{}, shared.ServiceUnavailable(err)
|
|
}
|
|
if err := resolution.Validate(); err != nil {
|
|
return ResolveByEmailResult{}, shared.InternalError(err)
|
|
}
|
|
|
|
result = ResolveByEmailResult{
|
|
Kind: string(resolution.Kind),
|
|
}
|
|
if !resolution.UserID.IsZero() {
|
|
result.UserID = resolution.UserID.String()
|
|
}
|
|
if !resolution.BlockReasonCode.IsZero() {
|
|
result.BlockReasonCode = resolution.BlockReasonCode.String()
|
|
}
|
|
outcome = result.Kind
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// RegistrationContext stores the create-only auth-facing initialization
|
|
// context forwarded by authsession.
|
|
type RegistrationContext struct {
|
|
// PreferredLanguage stores the initial preferred language.
|
|
PreferredLanguage string
|
|
|
|
// TimeZone stores the initial declared time-zone name.
|
|
TimeZone string
|
|
}
|
|
|
|
// EnsureByEmailInput stores one auth-facing ensure-by-email request.
|
|
type EnsureByEmailInput struct {
|
|
// Email stores the caller-supplied e-mail subject.
|
|
Email string
|
|
|
|
// RegistrationContext stores the required create-only registration context.
|
|
RegistrationContext *RegistrationContext
|
|
}
|
|
|
|
// EnsureByEmailResult stores one auth-facing ensure-by-email response.
|
|
type EnsureByEmailResult struct {
|
|
// Outcome stores the coarse ensure outcome.
|
|
Outcome string
|
|
|
|
// UserID is present only for `existing` and `created`.
|
|
UserID string
|
|
|
|
// BlockReasonCode is present only for `blocked`.
|
|
BlockReasonCode string
|
|
}
|
|
|
|
// Ensurer executes the auth-facing ensure-by-email use case.
|
|
type Ensurer struct {
|
|
store ports.AuthDirectoryStore
|
|
clock ports.Clock
|
|
idGenerator ports.IDGenerator
|
|
policy ports.RaceNamePolicy
|
|
logger *slog.Logger
|
|
telemetry *telemetry.Runtime
|
|
profilePublisher ports.ProfileChangedPublisher
|
|
settingsPublisher ports.SettingsChangedPublisher
|
|
entitlementPublisher ports.EntitlementChangedPublisher
|
|
}
|
|
|
|
// NewEnsurer returns one ensure-by-email use case instance.
|
|
func NewEnsurer(
|
|
store ports.AuthDirectoryStore,
|
|
clock ports.Clock,
|
|
idGenerator ports.IDGenerator,
|
|
policy ports.RaceNamePolicy,
|
|
) (*Ensurer, error) {
|
|
return NewEnsurerWithObservability(store, clock, idGenerator, policy, nil, nil, nil, nil, nil)
|
|
}
|
|
|
|
// NewEnsurerWithObservability returns one ensure-by-email use case instance
|
|
// with optional structured logging, metrics, and post-commit event
|
|
// publication hooks.
|
|
func NewEnsurerWithObservability(
|
|
store ports.AuthDirectoryStore,
|
|
clock ports.Clock,
|
|
idGenerator ports.IDGenerator,
|
|
policy ports.RaceNamePolicy,
|
|
logger *slog.Logger,
|
|
telemetryRuntime *telemetry.Runtime,
|
|
profilePublisher ports.ProfileChangedPublisher,
|
|
settingsPublisher ports.SettingsChangedPublisher,
|
|
entitlementPublisher ports.EntitlementChangedPublisher,
|
|
) (*Ensurer, error) {
|
|
switch {
|
|
case store == nil:
|
|
return nil, fmt.Errorf("authdirectory ensurer: auth directory store must not be nil")
|
|
case clock == nil:
|
|
return nil, fmt.Errorf("authdirectory ensurer: clock must not be nil")
|
|
case idGenerator == nil:
|
|
return nil, fmt.Errorf("authdirectory ensurer: id generator must not be nil")
|
|
case policy == nil:
|
|
return nil, fmt.Errorf("authdirectory ensurer: race-name policy must not be nil")
|
|
default:
|
|
return &Ensurer{
|
|
store: store,
|
|
clock: clock,
|
|
idGenerator: idGenerator,
|
|
policy: policy,
|
|
logger: logger,
|
|
telemetry: telemetryRuntime,
|
|
profilePublisher: profilePublisher,
|
|
settingsPublisher: settingsPublisher,
|
|
entitlementPublisher: entitlementPublisher,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// Execute ensures that one e-mail subject maps to an existing user, a newly
|
|
// created user, or a blocked outcome.
|
|
func (service *Ensurer) Execute(ctx context.Context, input EnsureByEmailInput) (result EnsureByEmailResult, err error) {
|
|
outcome := "failed"
|
|
userIDString := ""
|
|
defer func() {
|
|
if service.telemetry != nil {
|
|
service.telemetry.RecordUserCreationOutcome(ctx, outcome)
|
|
}
|
|
shared.LogServiceOutcome(service.logger, ctx, "ensure by email completed", err,
|
|
"use_case", "ensure_by_email",
|
|
"outcome", outcome,
|
|
"user_id", userIDString,
|
|
"source", initialEntitlementSource.String(),
|
|
)
|
|
}()
|
|
|
|
if ctx == nil {
|
|
return EnsureByEmailResult{}, shared.InvalidRequest("context must not be nil")
|
|
}
|
|
|
|
email, err := shared.ParseEmail(input.Email)
|
|
if err != nil {
|
|
return EnsureByEmailResult{}, err
|
|
}
|
|
if input.RegistrationContext == nil {
|
|
return EnsureByEmailResult{}, shared.InvalidRequest("registration_context must be present")
|
|
}
|
|
|
|
preferredLanguage, err := shared.ParseRegistrationPreferredLanguage(input.RegistrationContext.PreferredLanguage)
|
|
if err != nil {
|
|
return EnsureByEmailResult{}, err
|
|
}
|
|
timeZone, err := shared.ParseRegistrationTimeZoneName(input.RegistrationContext.TimeZone)
|
|
if err != nil {
|
|
return EnsureByEmailResult{}, err
|
|
}
|
|
|
|
now := service.clock.Now().UTC()
|
|
|
|
for attempt := 0; attempt < ensureCreateRetryLimit; attempt++ {
|
|
userID, err := service.idGenerator.NewUserID()
|
|
if err != nil {
|
|
return EnsureByEmailResult{}, shared.ServiceUnavailable(err)
|
|
}
|
|
raceName, err := service.idGenerator.NewInitialRaceName()
|
|
if err != nil {
|
|
return EnsureByEmailResult{}, shared.ServiceUnavailable(err)
|
|
}
|
|
|
|
accountRecord := account.UserAccount{
|
|
UserID: userID,
|
|
Email: email,
|
|
RaceName: raceName,
|
|
PreferredLanguage: preferredLanguage,
|
|
TimeZone: timeZone,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
entitlementSnapshot := entitlement.CurrentSnapshot{
|
|
UserID: userID,
|
|
PlanCode: entitlement.PlanCodeFree,
|
|
IsPaid: false,
|
|
StartsAt: now,
|
|
Source: initialEntitlementSource,
|
|
Actor: common.ActorRef{Type: initialEntitlementActorType, ID: initialEntitlementActorID},
|
|
ReasonCode: initialEntitlementReasonCode,
|
|
UpdatedAt: now,
|
|
}
|
|
entitlementRecordID, err := service.idGenerator.NewEntitlementRecordID()
|
|
if err != nil {
|
|
return EnsureByEmailResult{}, shared.ServiceUnavailable(err)
|
|
}
|
|
entitlementRecord := entitlement.PeriodRecord{
|
|
RecordID: entitlementRecordID,
|
|
UserID: userID,
|
|
PlanCode: entitlement.PlanCodeFree,
|
|
Source: initialEntitlementSource,
|
|
Actor: common.ActorRef{Type: initialEntitlementActorType, ID: initialEntitlementActorID},
|
|
ReasonCode: initialEntitlementReasonCode,
|
|
StartsAt: now,
|
|
CreatedAt: now,
|
|
}
|
|
reservation, err := shared.BuildRaceNameReservation(service.policy, userID, raceName, now)
|
|
if err != nil {
|
|
return EnsureByEmailResult{}, shared.ServiceUnavailable(err)
|
|
}
|
|
|
|
ensureResult, err := service.store.EnsureByEmail(ctx, ports.EnsureByEmailInput{
|
|
Email: email,
|
|
Account: accountRecord,
|
|
Entitlement: entitlementSnapshot,
|
|
EntitlementRecord: entitlementRecord,
|
|
Reservation: reservation,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, ports.ErrRaceNameConflict) && service.telemetry != nil {
|
|
service.telemetry.RecordRaceNameReservationConflict(ctx, "ensure_by_email")
|
|
}
|
|
if errors.Is(err, ports.ErrConflict) {
|
|
continue
|
|
}
|
|
return EnsureByEmailResult{}, shared.ServiceUnavailable(err)
|
|
}
|
|
if err := ensureResult.Validate(); err != nil {
|
|
return EnsureByEmailResult{}, shared.InternalError(err)
|
|
}
|
|
|
|
result = EnsureByEmailResult{
|
|
Outcome: string(ensureResult.Outcome),
|
|
}
|
|
if !ensureResult.UserID.IsZero() {
|
|
result.UserID = ensureResult.UserID.String()
|
|
userIDString = result.UserID
|
|
}
|
|
if !ensureResult.BlockReasonCode.IsZero() {
|
|
result.BlockReasonCode = ensureResult.BlockReasonCode.String()
|
|
}
|
|
outcome = result.Outcome
|
|
|
|
if result.Outcome == string(ports.EnsureByEmailOutcomeCreated) {
|
|
service.publishInitializedEvents(ctx, accountRecord, entitlementSnapshot)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
return EnsureByEmailResult{}, shared.ServiceUnavailable(fmt.Errorf("ensure-by-email conflict retry limit exceeded"))
|
|
}
|
|
|
|
func (service *Ensurer) publishInitializedEvents(
|
|
ctx context.Context,
|
|
accountRecord account.UserAccount,
|
|
entitlementSnapshot entitlement.CurrentSnapshot,
|
|
) {
|
|
occurredAt := accountRecord.UpdatedAt.UTC()
|
|
|
|
service.publishProfileChanged(ctx, ports.ProfileChangedEvent{
|
|
UserID: accountRecord.UserID,
|
|
OccurredAt: occurredAt,
|
|
Source: initialEntitlementSource,
|
|
Operation: ports.ProfileChangedOperationInitialized,
|
|
RaceName: accountRecord.RaceName,
|
|
})
|
|
service.publishSettingsChanged(ctx, ports.SettingsChangedEvent{
|
|
UserID: accountRecord.UserID,
|
|
OccurredAt: occurredAt,
|
|
Source: initialEntitlementSource,
|
|
Operation: ports.SettingsChangedOperationInitialized,
|
|
PreferredLanguage: accountRecord.PreferredLanguage,
|
|
TimeZone: accountRecord.TimeZone,
|
|
})
|
|
service.publishEntitlementChanged(ctx, ports.EntitlementChangedEvent{
|
|
UserID: entitlementSnapshot.UserID,
|
|
OccurredAt: occurredAt,
|
|
Source: initialEntitlementSource,
|
|
Operation: ports.EntitlementChangedOperationInitialized,
|
|
PlanCode: entitlementSnapshot.PlanCode,
|
|
IsPaid: entitlementSnapshot.IsPaid,
|
|
StartsAt: entitlementSnapshot.StartsAt,
|
|
EndsAt: entitlementSnapshot.EndsAt,
|
|
ReasonCode: entitlementSnapshot.ReasonCode,
|
|
Actor: entitlementSnapshot.Actor,
|
|
UpdatedAt: entitlementSnapshot.UpdatedAt,
|
|
})
|
|
}
|
|
|
|
func (service *Ensurer) publishProfileChanged(ctx context.Context, event ports.ProfileChangedEvent) {
|
|
if service.profilePublisher == nil {
|
|
return
|
|
}
|
|
if err := service.profilePublisher.PublishProfileChanged(ctx, event); err != nil {
|
|
if service.telemetry != nil {
|
|
service.telemetry.RecordEventPublicationFailure(ctx, ports.ProfileChangedEventType)
|
|
}
|
|
shared.LogEventPublicationFailure(service.logger, ctx, ports.ProfileChangedEventType, err,
|
|
"use_case", "ensure_by_email",
|
|
"user_id", event.UserID.String(),
|
|
"source", event.Source.String(),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (service *Ensurer) publishSettingsChanged(ctx context.Context, event ports.SettingsChangedEvent) {
|
|
if service.settingsPublisher == nil {
|
|
return
|
|
}
|
|
if err := service.settingsPublisher.PublishSettingsChanged(ctx, event); err != nil {
|
|
if service.telemetry != nil {
|
|
service.telemetry.RecordEventPublicationFailure(ctx, ports.SettingsChangedEventType)
|
|
}
|
|
shared.LogEventPublicationFailure(service.logger, ctx, ports.SettingsChangedEventType, err,
|
|
"use_case", "ensure_by_email",
|
|
"user_id", event.UserID.String(),
|
|
"source", event.Source.String(),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (service *Ensurer) publishEntitlementChanged(ctx context.Context, event ports.EntitlementChangedEvent) {
|
|
if service.entitlementPublisher == nil {
|
|
return
|
|
}
|
|
if err := service.entitlementPublisher.PublishEntitlementChanged(ctx, event); err != nil {
|
|
if service.telemetry != nil {
|
|
service.telemetry.RecordEventPublicationFailure(ctx, ports.EntitlementChangedEventType)
|
|
}
|
|
shared.LogEventPublicationFailure(service.logger, ctx, ports.EntitlementChangedEventType, err,
|
|
"use_case", "ensure_by_email",
|
|
"user_id", event.UserID.String(),
|
|
"source", event.Source.String(),
|
|
"reason_code", event.ReasonCode.String(),
|
|
"actor_type", event.Actor.Type.String(),
|
|
"actor_id", event.Actor.ID.String(),
|
|
)
|
|
}
|
|
}
|
|
|
|
// ExistsByUserIDInput stores one auth-facing existence check request.
|
|
type ExistsByUserIDInput struct {
|
|
// UserID stores the caller-supplied stable user identifier.
|
|
UserID string
|
|
}
|
|
|
|
// ExistsByUserIDResult stores one auth-facing existence check response.
|
|
type ExistsByUserIDResult struct {
|
|
// Exists reports whether the supplied user identifier currently exists.
|
|
Exists bool
|
|
}
|
|
|
|
// ExistenceChecker executes the auth-facing exists-by-user-id use case.
|
|
type ExistenceChecker struct {
|
|
store ports.AuthDirectoryStore
|
|
}
|
|
|
|
// NewExistenceChecker returns one exists-by-user-id use case instance.
|
|
func NewExistenceChecker(store ports.AuthDirectoryStore) (*ExistenceChecker, error) {
|
|
if store == nil {
|
|
return nil, fmt.Errorf("authdirectory existence checker: auth directory store must not be nil")
|
|
}
|
|
|
|
return &ExistenceChecker{store: store}, nil
|
|
}
|
|
|
|
// Execute reports whether one stable user identifier exists.
|
|
func (service *ExistenceChecker) Execute(ctx context.Context, input ExistsByUserIDInput) (ExistsByUserIDResult, error) {
|
|
if ctx == nil {
|
|
return ExistsByUserIDResult{}, shared.InvalidRequest("context must not be nil")
|
|
}
|
|
|
|
userID, err := shared.ParseUserID(input.UserID)
|
|
if err != nil {
|
|
return ExistsByUserIDResult{}, err
|
|
}
|
|
|
|
exists, err := service.store.ExistsByUserID(ctx, userID)
|
|
if err != nil {
|
|
return ExistsByUserIDResult{}, shared.ServiceUnavailable(err)
|
|
}
|
|
|
|
return ExistsByUserIDResult{Exists: exists}, nil
|
|
}
|
|
|
|
// BlockByUserIDInput stores one auth-facing block-by-user-id request.
|
|
type BlockByUserIDInput struct {
|
|
// UserID stores the stable account identifier that must be blocked.
|
|
UserID string
|
|
|
|
// ReasonCode stores the machine-readable block reason.
|
|
ReasonCode string
|
|
}
|
|
|
|
// BlockByEmailInput stores one auth-facing block-by-email request.
|
|
type BlockByEmailInput struct {
|
|
// Email stores the exact normalized e-mail subject that must be blocked.
|
|
Email string
|
|
|
|
// ReasonCode stores the machine-readable block reason.
|
|
ReasonCode string
|
|
}
|
|
|
|
// BlockResult stores one auth-facing block response.
|
|
type BlockResult struct {
|
|
// Outcome reports whether the current call created a new block.
|
|
Outcome string
|
|
|
|
// UserID stores the resolved account when the blocked subject belongs to an
|
|
// existing user.
|
|
UserID string
|
|
}
|
|
|
|
// BlockByUserIDService executes the auth-facing block-by-user-id use case.
|
|
type BlockByUserIDService struct {
|
|
store ports.AuthDirectoryStore
|
|
clock ports.Clock
|
|
}
|
|
|
|
// NewBlockByUserIDService returns one block-by-user-id use case instance.
|
|
func NewBlockByUserIDService(store ports.AuthDirectoryStore, clock ports.Clock) (*BlockByUserIDService, error) {
|
|
switch {
|
|
case store == nil:
|
|
return nil, fmt.Errorf("authdirectory block-by-user-id service: auth directory store must not be nil")
|
|
case clock == nil:
|
|
return nil, fmt.Errorf("authdirectory block-by-user-id service: clock must not be nil")
|
|
default:
|
|
return &BlockByUserIDService{store: store, clock: clock}, nil
|
|
}
|
|
}
|
|
|
|
// Execute blocks one account addressed by stable user identifier.
|
|
func (service *BlockByUserIDService) Execute(ctx context.Context, input BlockByUserIDInput) (BlockResult, error) {
|
|
if ctx == nil {
|
|
return BlockResult{}, shared.InvalidRequest("context must not be nil")
|
|
}
|
|
|
|
userID, err := shared.ParseUserID(input.UserID)
|
|
if err != nil {
|
|
return BlockResult{}, err
|
|
}
|
|
reasonCode, err := shared.ParseReasonCode(input.ReasonCode)
|
|
if err != nil {
|
|
return BlockResult{}, err
|
|
}
|
|
|
|
result, err := service.store.BlockByUserID(ctx, ports.BlockByUserIDInput{
|
|
UserID: userID,
|
|
ReasonCode: reasonCode,
|
|
BlockedAt: service.clock.Now().UTC(),
|
|
})
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, ports.ErrNotFound):
|
|
return BlockResult{}, shared.SubjectNotFound()
|
|
default:
|
|
return BlockResult{}, shared.ServiceUnavailable(err)
|
|
}
|
|
}
|
|
if err := result.Validate(); err != nil {
|
|
return BlockResult{}, shared.InternalError(err)
|
|
}
|
|
|
|
response := BlockResult{Outcome: string(result.Outcome)}
|
|
if !result.UserID.IsZero() {
|
|
response.UserID = result.UserID.String()
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
// BlockByEmailService executes the auth-facing block-by-email use case.
|
|
type BlockByEmailService struct {
|
|
store ports.AuthDirectoryStore
|
|
clock ports.Clock
|
|
}
|
|
|
|
// NewBlockByEmailService returns one block-by-email use case instance.
|
|
func NewBlockByEmailService(store ports.AuthDirectoryStore, clock ports.Clock) (*BlockByEmailService, error) {
|
|
switch {
|
|
case store == nil:
|
|
return nil, fmt.Errorf("authdirectory block-by-email service: auth directory store must not be nil")
|
|
case clock == nil:
|
|
return nil, fmt.Errorf("authdirectory block-by-email service: clock must not be nil")
|
|
default:
|
|
return &BlockByEmailService{store: store, clock: clock}, nil
|
|
}
|
|
}
|
|
|
|
// Execute blocks one exact normalized e-mail subject.
|
|
func (service *BlockByEmailService) Execute(ctx context.Context, input BlockByEmailInput) (BlockResult, error) {
|
|
if ctx == nil {
|
|
return BlockResult{}, shared.InvalidRequest("context must not be nil")
|
|
}
|
|
|
|
email, err := shared.ParseEmail(input.Email)
|
|
if err != nil {
|
|
return BlockResult{}, err
|
|
}
|
|
reasonCode, err := shared.ParseReasonCode(input.ReasonCode)
|
|
if err != nil {
|
|
return BlockResult{}, err
|
|
}
|
|
|
|
result, err := service.store.BlockByEmail(ctx, ports.BlockByEmailInput{
|
|
Email: email,
|
|
ReasonCode: reasonCode,
|
|
BlockedAt: service.clock.Now().UTC(),
|
|
})
|
|
if err != nil {
|
|
return BlockResult{}, shared.ServiceUnavailable(err)
|
|
}
|
|
if err := result.Validate(); err != nil {
|
|
return BlockResult{}, shared.InternalError(err)
|
|
}
|
|
|
|
response := BlockResult{Outcome: string(result.Outcome)}
|
|
if !result.UserID.IsZero() {
|
|
response.UserID = result.UserID.String()
|
|
}
|
|
|
|
return response, nil
|
|
}
|