package session import ( "context" "time" "github.com/google/uuid" ) // Service mints, resolves, and revokes sessions over the store and the // write-through cache. The gateway is its only caller (from a later stage); the // HTTP surface is wired then. type Service struct { store *Store cache *Cache } // NewService constructs a Service over store and cache. func NewService(store *Store, cache *Cache) *Service { return &Service{store: store, cache: cache} } // Warm hydrates the cache from the store. Call once before serving traffic. func (svc *Service) Warm(ctx context.Context) error { return svc.cache.Warm(ctx, svc.store) } // Ready reports whether the session cache has been warmed. func (svc *Service) Ready() bool { return svc.cache.Ready() } // Create mints a new active session for accountID and returns the plaintext // token (shown to the caller once) together with the persisted session. func (svc *Service) Create(ctx context.Context, accountID uuid.UUID) (string, Session, error) { token, tokenHash, err := GenerateToken() if err != nil { return "", Session{}, err } sess, err := svc.store.Insert(ctx, accountID, tokenHash) if err != nil { return "", Session{}, err } svc.cache.Add(sess) return token, sess, nil } // Resolve maps a presented token to its active session, consulting the cache // first and falling back to the store (repopulating the cache on a hit). // Returns ErrNotFound when no active session matches. func (svc *Service) Resolve(ctx context.Context, token string) (Session, error) { hash := HashToken(token) if sess, ok := svc.cache.Get(hash); ok { return sess, nil } sess, err := svc.store.FindActiveByTokenHash(ctx, hash) if err != nil { return Session{}, err } svc.cache.Add(sess) return sess, nil } // Revoke revokes the session for the presented token. It is idempotent: // revoking an unknown or already-revoked token returns nil. func (svc *Service) Revoke(ctx context.Context, token string) error { hash := HashToken(token) if _, _, err := svc.store.RevokeByTokenHash(ctx, hash, time.Now().UTC()); err != nil { return err } svc.cache.Remove(hash) return nil } // RevokeAllForAccount revokes every active session of accountID and evicts them // from the cache. The account-merge flow calls it to retire a secondary account // (Stage 11). It is idempotent. func (svc *Service) RevokeAllForAccount(ctx context.Context, accountID uuid.UUID) error { if _, err := svc.store.RevokeAllForAccount(ctx, accountID, time.Now().UTC()); err != nil { return err } svc.cache.RemoveByAccount(accountID) return nil }