feat: authsession service
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user