feat: user service
This commit is contained in:
@@ -0,0 +1,614 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user