264 lines
8.5 KiB
Go
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
|
|
}
|