Files
galaxy-game/authsession/internal/ports/user_directory.go
T
2026-04-09 09:00:06 +02:00

264 lines
8.5 KiB
Go

package ports
import (
"context"
"errors"
"fmt"
"strings"
"galaxy/authsession/internal/domain/common"
"galaxy/authsession/internal/domain/userresolution"
)
// UserDirectory provides the auth/session boundary to user ownership,
// registration, and block-policy decisions.
type UserDirectory interface {
// ResolveByEmail returns the current resolution state for email without
// creating any new user record.
ResolveByEmail(ctx context.Context, email common.Email) (userresolution.Result, error)
// ExistsByUserID reports whether userID currently identifies a stored user
// record.
ExistsByUserID(ctx context.Context, userID common.UserID) (bool, error)
// EnsureUserByEmail returns an existing user for email, creates a new user
// when registration is allowed, or reports a blocked outcome when the
// address may not continue through confirm flow.
EnsureUserByEmail(ctx context.Context, input EnsureUserInput) (EnsureUserResult, error)
// BlockByUserID applies a block state to the user identified by
// input.UserID. Implementations must wrap ErrNotFound when input.UserID does
// not exist.
BlockByUserID(ctx context.Context, input BlockUserByIDInput) (BlockUserResult, error)
// BlockByEmail applies a block state to input.Email, even when no user
// record currently exists for that e-mail address.
BlockByEmail(ctx context.Context, input BlockUserByEmailInput) (BlockUserResult, error)
}
// EnsureUserInput describes one user-directory ensure request keyed by the
// normalized e-mail address.
type EnsureUserInput struct {
// Email identifies the normalized e-mail address that should resolve to an
// existing user, a newly created user, or a blocked outcome.
Email common.Email
// RegistrationContext carries create-only user initialization fields. The
// user directory must ignore this context for existing users.
RegistrationContext *RegistrationContext
}
// Validate reports whether EnsureUserInput contains a complete request.
func (i EnsureUserInput) Validate() error {
if err := i.Email.Validate(); err != nil {
return fmt.Errorf("ensure user input email: %w", err)
}
if i.RegistrationContext != nil {
if err := i.RegistrationContext.Validate(); err != nil {
return fmt.Errorf("ensure user input registration context: %w", err)
}
}
return nil
}
// RegistrationContext describes create-only user initialization fields
// forwarded from the public confirm-email-code flow.
type RegistrationContext struct {
// PreferredLanguage stores the BCP 47 language tag that should initialize a
// newly created user. During the current rollout phase Auth / Session
// Service sends a temporary `"en"` default until gateway geoip derivation is
// deployed.
PreferredLanguage string
// TimeZone stores the client-selected IANA time zone name that should
// initialize a newly created user.
TimeZone string
}
// Validate reports whether RegistrationContext contains complete create-only
// initialization metadata.
func (c RegistrationContext) Validate() error {
if strings.TrimSpace(c.PreferredLanguage) == "" {
return errors.New("preferred language must not be empty")
}
if strings.TrimSpace(c.PreferredLanguage) != c.PreferredLanguage {
return errors.New("preferred language must not contain surrounding whitespace")
}
if strings.TrimSpace(c.TimeZone) == "" {
return errors.New("time zone must not be empty")
}
if strings.TrimSpace(c.TimeZone) != c.TimeZone {
return errors.New("time zone must not contain surrounding whitespace")
}
return nil
}
// EnsureUserOutcome identifies the coarse outcome of ensuring a user record
// for one normalized e-mail address.
type EnsureUserOutcome string
const (
// EnsureUserOutcomeExisting reports that the e-mail already belonged to a
// stored user.
EnsureUserOutcomeExisting EnsureUserOutcome = "existing"
// EnsureUserOutcomeCreated reports that a new user was created for the
// e-mail address.
EnsureUserOutcomeCreated EnsureUserOutcome = "created"
// EnsureUserOutcomeBlocked reports that the e-mail cannot be used for login
// or registration.
EnsureUserOutcomeBlocked EnsureUserOutcome = "blocked"
)
// IsKnown reports whether EnsureUserOutcome is supported by the current
// user-directory contract.
func (o EnsureUserOutcome) IsKnown() bool {
switch o {
case EnsureUserOutcomeExisting, EnsureUserOutcomeCreated, EnsureUserOutcomeBlocked:
return true
default:
return false
}
}
// EnsureUserResult describes the stable outcome returned by UserDirectory
// after one ensure-user attempt.
type EnsureUserResult struct {
// Outcome reports whether the user already existed, was created, or is
// blocked by policy.
Outcome EnsureUserOutcome
// UserID is present when Outcome is EnsureUserOutcomeExisting or
// EnsureUserOutcomeCreated.
UserID common.UserID
// BlockReasonCode is present only when Outcome is EnsureUserOutcomeBlocked.
BlockReasonCode userresolution.BlockReasonCode
}
// Validate reports whether EnsureUserResult satisfies the user-directory
// contract invariants.
func (r EnsureUserResult) Validate() error {
if !r.Outcome.IsKnown() {
return fmt.Errorf("ensure user result outcome %q is unsupported", r.Outcome)
}
switch r.Outcome {
case EnsureUserOutcomeExisting, EnsureUserOutcomeCreated:
if err := r.UserID.Validate(); err != nil {
return fmt.Errorf("ensure user result user id: %w", err)
}
if !r.BlockReasonCode.IsZero() {
return errors.New("ensure user result must not contain block reason code for existing or created outcomes")
}
case EnsureUserOutcomeBlocked:
if !r.UserID.IsZero() {
return errors.New("ensure user result must not contain user id for blocked outcome")
}
if err := r.BlockReasonCode.Validate(); err != nil {
return fmt.Errorf("ensure user result block reason code: %w", err)
}
}
return nil
}
// BlockUserByIDInput describes one block mutation targeted by stable user id.
type BlockUserByIDInput struct {
// UserID identifies the user that should be blocked.
UserID common.UserID
// ReasonCode stores the machine-readable block reason to apply.
ReasonCode userresolution.BlockReasonCode
}
// Validate reports whether BlockUserByIDInput contains a complete block
// request.
func (i BlockUserByIDInput) Validate() error {
if err := i.UserID.Validate(); err != nil {
return fmt.Errorf("block user by id input user id: %w", err)
}
if err := i.ReasonCode.Validate(); err != nil {
return fmt.Errorf("block user by id input reason code: %w", err)
}
return nil
}
// BlockUserByEmailInput describes one block mutation targeted by normalized
// e-mail address.
type BlockUserByEmailInput struct {
// Email identifies the e-mail address that should be blocked.
Email common.Email
// ReasonCode stores the machine-readable block reason to apply.
ReasonCode userresolution.BlockReasonCode
}
// Validate reports whether BlockUserByEmailInput contains a complete block
// request.
func (i BlockUserByEmailInput) Validate() error {
if err := i.Email.Validate(); err != nil {
return fmt.Errorf("block user by email input email: %w", err)
}
if err := i.ReasonCode.Validate(); err != nil {
return fmt.Errorf("block user by email input reason code: %w", err)
}
return nil
}
// BlockUserOutcome identifies the coarse outcome of blocking one user or
// e-mail subject.
type BlockUserOutcome string
const (
// BlockUserOutcomeBlocked reports that the current mutation applied a new
// block state.
BlockUserOutcomeBlocked BlockUserOutcome = "blocked"
// BlockUserOutcomeAlreadyBlocked reports that the target subject had already
// been blocked before the current mutation.
BlockUserOutcomeAlreadyBlocked BlockUserOutcome = "already_blocked"
)
// IsKnown reports whether BlockUserOutcome is supported by the current
// user-directory contract.
func (o BlockUserOutcome) IsKnown() bool {
switch o {
case BlockUserOutcomeBlocked, BlockUserOutcomeAlreadyBlocked:
return true
default:
return false
}
}
// BlockUserResult describes the stable outcome returned by UserDirectory after
// one block attempt.
type BlockUserResult struct {
// Outcome reports whether the current mutation applied a new block state.
Outcome BlockUserOutcome
// UserID optionally stores the stable user identifier resolved for the
// blocked subject when one exists.
UserID common.UserID
}
// Validate reports whether BlockUserResult satisfies the user-directory
// contract invariants.
func (r BlockUserResult) Validate() error {
if !r.Outcome.IsKnown() {
return fmt.Errorf("block user result outcome %q is unsupported", r.Outcome)
}
if !r.UserID.IsZero() {
if err := r.UserID.Validate(); err != nil {
return fmt.Errorf("block user result user id: %w", err)
}
}
return nil
}