package auth import ( "context" "time" "github.com/google/uuid" "go.uber.org/zap" ) // LoginCodeMailer is the publisher contract auth uses to deliver a // one-time login code to a user's mailbox. The canonical // implementation lives in `backend/internal/mail`; tests can use // `NewNoopLoginCodeMailer` to record the outbound code without wiring // SMTP. type LoginCodeMailer interface { EnqueueLoginCode(ctx context.Context, email, code string, ttl time.Duration) error } // SessionInvalidator emits the gRPC push session_invalidation event // when auth revokes one or more device sessions. The canonical // implementation lives in `backend/internal/push`; tests can use // `NewNoopSessionInvalidator` for an in-memory log-only fallback. type SessionInvalidator interface { PublishSessionInvalidation(ctx context.Context, deviceSessionID, userID uuid.UUID, reason string) } // UserEnsurer binds a confirmed email to an `accounts.user_id`. The // canonical implementation is `*user.Service`; tests can swap in a // recording fake. type UserEnsurer interface { EnsureByEmail(ctx context.Context, email, preferredLanguage, timeZone, declaredCountry string) (uuid.UUID, error) } // GeoService provides the geo helpers auth needs at confirm-email-code: // a country lookup for the `preferred_language` fallback and a // post-commit write of `accounts.declared_country`. Both methods are // best-effort — auth never blocks the registration flow on geo failures. type GeoService interface { LookupCountry(sourceIP string) string LanguageForIP(sourceIP string) string SetDeclaredCountryAtRegistration(ctx context.Context, userID uuid.UUID, sourceIP string) error } // NewNoopLoginCodeMailer returns a LoginCodeMailer that logs the // outbound code at info level and returns nil. The wiring code uses // the real `mail.Service`; this constructor exists for tests and for // local smoke runs that do not want to bring up an SMTP relay. func NewNoopLoginCodeMailer(logger *zap.Logger) LoginCodeMailer { if logger == nil { logger = zap.NewNop() } return &noopLoginCodeMailer{logger: logger.Named("auth.mail.noop")} } type noopLoginCodeMailer struct { logger *zap.Logger } func (m *noopLoginCodeMailer) EnqueueLoginCode(_ context.Context, email, code string, ttl time.Duration) error { m.logger.Info("auth login code (noop publisher)", zap.String("email", email), zap.String("code", code), zap.Duration("ttl", ttl), ) return nil } // NewNoopSessionInvalidator returns a SessionInvalidator that logs // every invalidation at info level and never blocks. The wiring code // uses the real `push.Service`; this constructor exists for tests // that need a callable surface without bringing up gRPC. func NewNoopSessionInvalidator(logger *zap.Logger) SessionInvalidator { if logger == nil { logger = zap.NewNop() } return &noopSessionInvalidator{logger: logger.Named("auth.push.noop")} } type noopSessionInvalidator struct { logger *zap.Logger } func (p *noopSessionInvalidator) PublishSessionInvalidation(_ context.Context, deviceSessionID, userID uuid.UUID, reason string) { p.logger.Info("session invalidation (noop publisher)", zap.String("device_session_id", deviceSessionID.String()), zap.String("user_id", userID.String()), zap.String("reason", reason), ) }