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

163 lines
4.6 KiB
Go

// Package devicesession defines the source-of-truth domain model for one
// authenticated device session.
package devicesession
import (
"errors"
"fmt"
"strings"
"time"
"galaxy/authsession/internal/domain/common"
)
// Status identifies the coarse lifecycle state of one device session.
type Status string
const (
// StatusActive reports that the session may be used for authenticated
// request verification.
StatusActive Status = "active"
// StatusRevoked reports that the session has been revoked and must no
// longer authenticate requests.
StatusRevoked Status = "revoked"
)
// RevokeReasonDeviceLogout reports that one device logged itself out.
const RevokeReasonDeviceLogout common.RevokeReasonCode = "device_logout"
// RevokeReasonLogoutAll reports that the session was revoked by a
// user-scoped logout-all action.
const RevokeReasonLogoutAll common.RevokeReasonCode = "logout_all"
// RevokeReasonAdminRevoke reports that the session was revoked
// administratively.
const RevokeReasonAdminRevoke common.RevokeReasonCode = "admin_revoke"
// RevokeReasonUserBlocked reports that the session was revoked because future
// auth flow for the user or e-mail was blocked.
const RevokeReasonUserBlocked common.RevokeReasonCode = "user_blocked"
// IsKnown reports whether Status is one of the device-session states
// supported by the current domain model.
func (s Status) IsKnown() bool {
switch s {
case StatusActive, StatusRevoked:
return true
default:
return false
}
}
// CanTransitionTo reports whether the current device-session Status may move
// to next under the Stage-2 lifecycle rules.
func (s Status) CanTransitionTo(next Status) bool {
return s == StatusActive && next == StatusRevoked
}
// IsKnownRevokeReasonCode reports whether code is one of the built-in revoke
// reasons fixed by the Stage-2 domain model.
func IsKnownRevokeReasonCode(code common.RevokeReasonCode) bool {
switch code {
case RevokeReasonDeviceLogout,
RevokeReasonLogoutAll,
RevokeReasonAdminRevoke,
RevokeReasonUserBlocked:
return true
default:
return false
}
}
// Revocation stores the audit metadata recorded when a session is revoked.
type Revocation struct {
// At reports when the revoke took effect.
At time.Time
// ReasonCode stores one machine-readable revoke reason code.
ReasonCode common.RevokeReasonCode
// ActorType stores one machine-readable initiator type.
ActorType common.RevokeActorType
// ActorID optionally stores a stable initiator identifier.
ActorID string
}
// Validate reports whether Revocation contains all metadata required for a
// revoked session.
func (r Revocation) Validate() error {
if r.At.IsZero() {
return errors.New("session revocation time must not be zero")
}
if err := r.ReasonCode.Validate(); err != nil {
return fmt.Errorf("session revocation reason code: %w", err)
}
if err := r.ActorType.Validate(); err != nil {
return fmt.Errorf("session revocation actor type: %w", err)
}
if strings.TrimSpace(r.ActorID) != r.ActorID {
return errors.New("session revocation actor id must not contain surrounding whitespace")
}
return nil
}
// Session is the minimal source-of-truth aggregate shape fixed by Stage 2.
type Session struct {
// ID identifies the device session.
ID common.DeviceSessionID
// UserID identifies the durable user linkage for the session.
UserID common.UserID
// ClientPublicKey stores the validated device public key in parsed form.
ClientPublicKey common.ClientPublicKey
// Status reports the coarse lifecycle state of the session.
Status Status
// CreatedAt reports when the session was created.
CreatedAt time.Time
// Revocation is present only when Status is StatusRevoked.
Revocation *Revocation
}
// Validate reports whether Session satisfies the Stage-2 structural and
// lifecycle invariants.
func (s Session) Validate() error {
if err := s.ID.Validate(); err != nil {
return fmt.Errorf("session id: %w", err)
}
if err := s.UserID.Validate(); err != nil {
return fmt.Errorf("session user id: %w", err)
}
if err := s.ClientPublicKey.Validate(); err != nil {
return fmt.Errorf("session client public key: %w", err)
}
if !s.Status.IsKnown() {
return fmt.Errorf("session status %q is unsupported", s.Status)
}
if s.CreatedAt.IsZero() {
return errors.New("session creation time must not be zero")
}
switch s.Status {
case StatusActive:
if s.Revocation != nil {
return errors.New("active session must not contain revocation metadata")
}
case StatusRevoked:
if s.Revocation == nil {
return errors.New("revoked session must contain revocation metadata")
}
if err := s.Revocation.Validate(); err != nil {
return fmt.Errorf("session revocation: %w", err)
}
}
return nil
}