feat: backend service

This commit is contained in:
Ilia Denisov
2026-05-06 10:14:55 +03:00
committed by GitHub
parent 3e2622757e
commit f446c6a2ac
1486 changed files with 49720 additions and 266401 deletions
+90
View File
@@ -0,0 +1,90 @@
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),
)
}