feat: authsession service

This commit is contained in:
Ilia Denisov
2026-04-08 16:23:07 +02:00
committed by GitHub
parent 28f04916af
commit 86a68ed9d0
174 changed files with 31732 additions and 112 deletions
@@ -0,0 +1,361 @@
// Package userservice provides runtime user-directory adapters for the
// auth/session service.
package userservice
import (
"context"
"fmt"
"sync"
"galaxy/authsession/internal/domain/common"
"galaxy/authsession/internal/domain/userresolution"
"galaxy/authsession/internal/ports"
)
type entry struct {
userID common.UserID
blockReasonCode userresolution.BlockReasonCode
}
// StubDirectory is a concurrency-safe in-process UserDirectory stub intended
// for development, local integration, and explicit stub-based tests.
//
// The zero value is ready to use. Unknown e-mail addresses resolve as
// creatable, unknown user identifiers do not exist, and EnsureUserByEmail
// creates deterministic user ids such as "user-1", "user-2", and so on.
type StubDirectory struct {
mu sync.Mutex
byEmail map[common.Email]entry
emailByUserID map[common.UserID]common.Email
createdUserIDs []common.UserID
nextUserNumber int
}
// ResolveByEmail returns the current coarse user-resolution state for email
// without creating any new user record.
func (d *StubDirectory) ResolveByEmail(ctx context.Context, email common.Email) (userresolution.Result, error) {
if err := validateContext(ctx, "resolve by email"); err != nil {
return userresolution.Result{}, err
}
if err := email.Validate(); err != nil {
return userresolution.Result{}, fmt.Errorf("resolve by email: %w", err)
}
d.mu.Lock()
defer d.mu.Unlock()
result, err := d.resolveLocked(email)
if err != nil {
return userresolution.Result{}, fmt.Errorf("resolve by email: %w", err)
}
return result, nil
}
// ExistsByUserID reports whether userID currently identifies a stored user
// record.
func (d *StubDirectory) ExistsByUserID(ctx context.Context, userID common.UserID) (bool, error) {
if err := validateContext(ctx, "exists by user id"); err != nil {
return false, err
}
if err := userID.Validate(); err != nil {
return false, fmt.Errorf("exists by user id: %w", err)
}
d.mu.Lock()
defer d.mu.Unlock()
_, ok := d.emailByUserID[userID]
return ok, nil
}
// EnsureUserByEmail returns an existing user for email, creates a new user
// when registration is allowed, or reports a blocked outcome.
func (d *StubDirectory) EnsureUserByEmail(ctx context.Context, email common.Email) (ports.EnsureUserResult, error) {
if err := validateContext(ctx, "ensure user by email"); err != nil {
return ports.EnsureUserResult{}, err
}
if err := email.Validate(); err != nil {
return ports.EnsureUserResult{}, fmt.Errorf("ensure user by email: %w", err)
}
d.mu.Lock()
defer d.mu.Unlock()
d.ensureMapsLocked()
stored, ok := d.byEmail[email]
if ok {
if !stored.blockReasonCode.IsZero() {
result := ports.EnsureUserResult{
Outcome: ports.EnsureUserOutcomeBlocked,
BlockReasonCode: stored.blockReasonCode,
}
if err := result.Validate(); err != nil {
return ports.EnsureUserResult{}, fmt.Errorf("ensure user by email: %w", err)
}
return result, nil
}
result := ports.EnsureUserResult{
Outcome: ports.EnsureUserOutcomeExisting,
UserID: stored.userID,
}
if err := result.Validate(); err != nil {
return ports.EnsureUserResult{}, fmt.Errorf("ensure user by email: %w", err)
}
return result, nil
}
userID, err := d.nextCreatedUserIDLocked()
if err != nil {
return ports.EnsureUserResult{}, fmt.Errorf("ensure user by email: %w", err)
}
d.byEmail[email] = entry{userID: userID}
d.emailByUserID[userID] = email
result := ports.EnsureUserResult{
Outcome: ports.EnsureUserOutcomeCreated,
UserID: userID,
}
if err := result.Validate(); err != nil {
return ports.EnsureUserResult{}, fmt.Errorf("ensure user by email: %w", err)
}
return result, nil
}
// BlockByUserID applies a block state to the user identified by input.UserID.
// Unknown user ids wrap ports.ErrNotFound.
func (d *StubDirectory) BlockByUserID(ctx context.Context, input ports.BlockUserByIDInput) (ports.BlockUserResult, error) {
if err := validateContext(ctx, "block by user id"); err != nil {
return ports.BlockUserResult{}, err
}
if err := input.Validate(); err != nil {
return ports.BlockUserResult{}, fmt.Errorf("block by user id: %w", err)
}
d.mu.Lock()
defer d.mu.Unlock()
email, ok := d.emailByUserID[input.UserID]
if !ok {
return ports.BlockUserResult{}, fmt.Errorf("block by user id %q: %w", input.UserID, ports.ErrNotFound)
}
stored := d.byEmail[email]
if !stored.blockReasonCode.IsZero() {
result := ports.BlockUserResult{
Outcome: ports.BlockUserOutcomeAlreadyBlocked,
UserID: input.UserID,
}
if err := result.Validate(); err != nil {
return ports.BlockUserResult{}, fmt.Errorf("block by user id: %w", err)
}
return result, nil
}
stored.blockReasonCode = input.ReasonCode
d.byEmail[email] = stored
result := ports.BlockUserResult{
Outcome: ports.BlockUserOutcomeBlocked,
UserID: input.UserID,
}
if err := result.Validate(); err != nil {
return ports.BlockUserResult{}, fmt.Errorf("block by user id: %w", err)
}
return result, nil
}
// BlockByEmail applies a block state to input.Email even when no user record
// currently exists for that e-mail address.
func (d *StubDirectory) BlockByEmail(ctx context.Context, input ports.BlockUserByEmailInput) (ports.BlockUserResult, error) {
if err := validateContext(ctx, "block by email"); err != nil {
return ports.BlockUserResult{}, err
}
if err := input.Validate(); err != nil {
return ports.BlockUserResult{}, fmt.Errorf("block by email: %w", err)
}
d.mu.Lock()
defer d.mu.Unlock()
d.ensureMapsLocked()
stored := d.byEmail[input.Email]
if !stored.blockReasonCode.IsZero() {
result := ports.BlockUserResult{
Outcome: ports.BlockUserOutcomeAlreadyBlocked,
UserID: stored.userID,
}
if err := result.Validate(); err != nil {
return ports.BlockUserResult{}, fmt.Errorf("block by email: %w", err)
}
return result, nil
}
stored.blockReasonCode = input.ReasonCode
d.byEmail[input.Email] = stored
if !stored.userID.IsZero() {
d.emailByUserID[stored.userID] = input.Email
}
result := ports.BlockUserResult{
Outcome: ports.BlockUserOutcomeBlocked,
UserID: stored.userID,
}
if err := result.Validate(); err != nil {
return ports.BlockUserResult{}, fmt.Errorf("block by email: %w", err)
}
return result, nil
}
// SeedExisting preloads one existing unblocked user record into the runtime
// stub.
func (d *StubDirectory) SeedExisting(email common.Email, userID common.UserID) error {
if err := email.Validate(); err != nil {
return fmt.Errorf("seed existing email: %w", err)
}
if err := userID.Validate(); err != nil {
return fmt.Errorf("seed existing user id: %w", err)
}
d.mu.Lock()
defer d.mu.Unlock()
d.ensureMapsLocked()
d.byEmail[email] = entry{userID: userID}
d.emailByUserID[userID] = email
return nil
}
// SeedBlockedEmail preloads one blocked e-mail address that does not
// necessarily belong to an existing user record.
func (d *StubDirectory) SeedBlockedEmail(email common.Email, reasonCode userresolution.BlockReasonCode) error {
if err := email.Validate(); err != nil {
return fmt.Errorf("seed blocked email: %w", err)
}
if err := reasonCode.Validate(); err != nil {
return fmt.Errorf("seed blocked email reason code: %w", err)
}
d.mu.Lock()
defer d.mu.Unlock()
d.ensureMapsLocked()
d.byEmail[email] = entry{blockReasonCode: reasonCode}
return nil
}
// SeedBlockedUser preloads one blocked existing user record into the runtime
// stub.
func (d *StubDirectory) SeedBlockedUser(email common.Email, userID common.UserID, reasonCode userresolution.BlockReasonCode) error {
if err := d.SeedExisting(email, userID); err != nil {
return err
}
d.mu.Lock()
defer d.mu.Unlock()
stored := d.byEmail[email]
stored.blockReasonCode = reasonCode
d.byEmail[email] = stored
return nil
}
// QueueCreatedUserIDs appends deterministic user identifiers that
// EnsureUserByEmail consumes before falling back to generated ids.
func (d *StubDirectory) QueueCreatedUserIDs(userIDs ...common.UserID) error {
for index, userID := range userIDs {
if err := userID.Validate(); err != nil {
return fmt.Errorf("queue created user id %d: %w", index, err)
}
}
d.mu.Lock()
defer d.mu.Unlock()
d.createdUserIDs = append(d.createdUserIDs, userIDs...)
return nil
}
func (d *StubDirectory) ensureMapsLocked() {
if d.byEmail == nil {
d.byEmail = make(map[common.Email]entry)
}
if d.emailByUserID == nil {
d.emailByUserID = make(map[common.UserID]common.Email)
}
}
func (d *StubDirectory) resolveLocked(email common.Email) (userresolution.Result, error) {
stored, ok := d.byEmail[email]
if !ok {
result := userresolution.Result{Kind: userresolution.KindCreatable}
if err := result.Validate(); err != nil {
return userresolution.Result{}, err
}
return result, nil
}
if !stored.blockReasonCode.IsZero() {
result := userresolution.Result{
Kind: userresolution.KindBlocked,
BlockReasonCode: stored.blockReasonCode,
}
if err := result.Validate(); err != nil {
return userresolution.Result{}, err
}
return result, nil
}
result := userresolution.Result{
Kind: userresolution.KindExisting,
UserID: stored.userID,
}
if err := result.Validate(); err != nil {
return userresolution.Result{}, err
}
return result, nil
}
func (d *StubDirectory) nextCreatedUserIDLocked() (common.UserID, error) {
if len(d.createdUserIDs) > 0 {
userID := d.createdUserIDs[0]
d.createdUserIDs = d.createdUserIDs[1:]
return userID, nil
}
d.nextUserNumber++
userID := common.UserID(fmt.Sprintf("user-%d", d.nextUserNumber))
if err := userID.Validate(); err != nil {
return "", err
}
return userID, nil
}
func validateContext(ctx context.Context, operation string) error {
if ctx == nil {
return fmt.Errorf("%s: nil context", operation)
}
if err := ctx.Err(); err != nil {
return fmt.Errorf("%s: %w", operation, err)
}
return nil
}
var _ ports.UserDirectory = (*StubDirectory)(nil)