Files
galaxy-game/authsession/internal/ports/session_store.go
T
2026-04-08 16:23:07 +02:00

215 lines
7.4 KiB
Go

package ports
import (
"context"
"errors"
"fmt"
"galaxy/authsession/internal/domain/common"
"galaxy/authsession/internal/domain/devicesession"
)
// SessionStore provides source-of-truth persistence for device sessions
// without exposing storage-specific encoding or transaction primitives.
type SessionStore interface {
// Get returns the stored session for deviceSessionID. Implementations must
// wrap ErrNotFound when deviceSessionID does not exist.
Get(ctx context.Context, deviceSessionID common.DeviceSessionID) (devicesession.Session, error)
// ListByUserID returns every stored session for userID in newest-first
// order. Implementations must return an empty slice, not ErrNotFound, when
// userID has no stored sessions.
ListByUserID(ctx context.Context, userID common.UserID) ([]devicesession.Session, error)
// CountActiveByUserID returns the number of active sessions currently stored
// for userID.
CountActiveByUserID(ctx context.Context, userID common.UserID) (int, error)
// Create persists record as a new device session. Implementations must wrap
// ErrConflict when record.ID already exists.
Create(ctx context.Context, record devicesession.Session) error
// Revoke stores a revoked view of one target session. Implementations must
// wrap ErrNotFound when input.DeviceSessionID does not exist.
Revoke(ctx context.Context, input RevokeSessionInput) (RevokeSessionResult, error)
// RevokeAllByUserID stores revoked views for all currently active sessions
// owned by input.UserID.
RevokeAllByUserID(ctx context.Context, input RevokeUserSessionsInput) (RevokeUserSessionsResult, error)
}
// RevokeSessionInput describes one single-session revoke mutation requested
// from SessionStore.
type RevokeSessionInput struct {
// DeviceSessionID identifies the session that should be revoked.
DeviceSessionID common.DeviceSessionID
// Revocation stores the audit metadata that must be attached to the revoked
// session.
Revocation devicesession.Revocation
}
// Validate reports whether RevokeSessionInput contains a complete revoke
// request.
func (i RevokeSessionInput) Validate() error {
if err := i.DeviceSessionID.Validate(); err != nil {
return fmt.Errorf("revoke session input device session id: %w", err)
}
if err := i.Revocation.Validate(); err != nil {
return fmt.Errorf("revoke session input revocation: %w", err)
}
return nil
}
// RevokeSessionOutcome identifies the coarse outcome of revoking one device
// session.
type RevokeSessionOutcome string
const (
// RevokeSessionOutcomeRevoked reports that an active session was moved to
// the revoked state by the current mutation.
RevokeSessionOutcomeRevoked RevokeSessionOutcome = "revoked"
// RevokeSessionOutcomeAlreadyRevoked reports that the requested session had
// already been revoked before the current mutation.
RevokeSessionOutcomeAlreadyRevoked RevokeSessionOutcome = "already_revoked"
)
// IsKnown reports whether RevokeSessionOutcome is supported by the current
// session-store contract.
func (o RevokeSessionOutcome) IsKnown() bool {
switch o {
case RevokeSessionOutcomeRevoked, RevokeSessionOutcomeAlreadyRevoked:
return true
default:
return false
}
}
// RevokeSessionResult describes the stable outcome returned by SessionStore
// after a single-session revoke attempt.
type RevokeSessionResult struct {
// Outcome reports whether the session was revoked just now or had already
// been revoked.
Outcome RevokeSessionOutcome
// Session stores the current source-of-truth session state after the revoke
// attempt.
Session devicesession.Session
}
// Validate reports whether RevokeSessionResult satisfies the session-store
// contract invariants.
func (r RevokeSessionResult) Validate() error {
if !r.Outcome.IsKnown() {
return fmt.Errorf("revoke session result outcome %q is unsupported", r.Outcome)
}
if err := r.Session.Validate(); err != nil {
return fmt.Errorf("revoke session result session: %w", err)
}
if r.Session.Status != devicesession.StatusRevoked {
return errors.New("revoke session result session must be revoked")
}
return nil
}
// RevokeUserSessionsInput describes one bulk user-session revoke mutation
// requested from SessionStore.
type RevokeUserSessionsInput struct {
// UserID identifies the owner whose active sessions should be revoked.
UserID common.UserID
// Revocation stores the audit metadata that must be attached to every
// revoked session.
Revocation devicesession.Revocation
}
// Validate reports whether RevokeUserSessionsInput contains a complete bulk
// revoke request.
func (i RevokeUserSessionsInput) Validate() error {
if err := i.UserID.Validate(); err != nil {
return fmt.Errorf("revoke user sessions input user id: %w", err)
}
if err := i.Revocation.Validate(); err != nil {
return fmt.Errorf("revoke user sessions input revocation: %w", err)
}
return nil
}
// RevokeUserSessionsOutcome identifies the coarse outcome of revoking all
// active sessions of one user.
type RevokeUserSessionsOutcome string
const (
// RevokeUserSessionsOutcomeRevoked reports that one or more active sessions
// were revoked by the current mutation.
RevokeUserSessionsOutcomeRevoked RevokeUserSessionsOutcome = "revoked"
// RevokeUserSessionsOutcomeNoActiveSessions reports that the target user did
// not currently own any active sessions.
RevokeUserSessionsOutcomeNoActiveSessions RevokeUserSessionsOutcome = "no_active_sessions"
)
// IsKnown reports whether RevokeUserSessionsOutcome is supported by the
// current session-store contract.
func (o RevokeUserSessionsOutcome) IsKnown() bool {
switch o {
case RevokeUserSessionsOutcomeRevoked, RevokeUserSessionsOutcomeNoActiveSessions:
return true
default:
return false
}
}
// RevokeUserSessionsResult describes the stable outcome returned by
// SessionStore after one bulk revoke attempt.
type RevokeUserSessionsResult struct {
// Outcome reports whether at least one active session was revoked.
Outcome RevokeUserSessionsOutcome
// UserID identifies the owner whose sessions were evaluated.
UserID common.UserID
// Sessions stores the current source-of-truth session states for every
// session affected by the bulk revoke operation.
Sessions []devicesession.Session
}
// Validate reports whether RevokeUserSessionsResult satisfies the bulk
// session-store contract invariants.
func (r RevokeUserSessionsResult) Validate() error {
if !r.Outcome.IsKnown() {
return fmt.Errorf("revoke user sessions result outcome %q is unsupported", r.Outcome)
}
if err := r.UserID.Validate(); err != nil {
return fmt.Errorf("revoke user sessions result user id: %w", err)
}
for index, session := range r.Sessions {
if err := session.Validate(); err != nil {
return fmt.Errorf("revoke user sessions result session %d: %w", index, err)
}
if session.Status != devicesession.StatusRevoked {
return fmt.Errorf("revoke user sessions result session %d must be revoked", index)
}
if session.UserID != r.UserID {
return fmt.Errorf("revoke user sessions result session %d belongs to %q, want %q", index, session.UserID, r.UserID)
}
}
switch r.Outcome {
case RevokeUserSessionsOutcomeRevoked:
if len(r.Sessions) == 0 {
return errors.New("revoke user sessions result must include sessions when outcome is revoked")
}
case RevokeUserSessionsOutcomeNoActiveSessions:
if len(r.Sessions) != 0 {
return errors.New("revoke user sessions result must not include sessions when outcome is no_active_sessions")
}
}
return nil
}