Documentation
¶
Overview ¶
Package identity covers the GlobalAccount bounded context: platform-wide identity, login, and authentication tokens.
Bounded context: Identity Ubiquitous language: GlobalAccount, account_id, email, account_status
Index ¶
- Constants
- Variables
- func ServeActivationPage(w http.ResponseWriter, result *ActivateResult)
- func ServeLoginPage(w http.ResponseWriter, _ *http.Request)
- type ActivateResult
- type ActivationMessage
- type BeginParams
- type BeginResult
- type CLILoginService
- func (s *CLILoginService) Activate(ctx context.Context, activationToken string) (*ActivateResult, error)
- func (s *CLILoginService) Begin(ctx context.Context, p BeginParams) (*BeginResult, error)
- func (s *CLILoginService) Exchange(ctx context.Context, sessionID, loginToken string) (*OAuthTokenResult, error)
- func (s *CLILoginService) Init(ctx context.Context, clientName, clientVersion, osPlatform string) (*InitResult, error)
- func (s *CLILoginService) Poll(ctx context.Context, sessionID, deviceCode string) (*PollResult, error)
- func (s *CLILoginService) Refresh(ctx context.Context, refreshTokenPlain string) (*OAuthTokenResult, error)
- func (s *CLILoginService) SetReferralRecorder(r ReferralRecorder)
- func (s *CLILoginService) SetVKShareReconciler(r VKShareReconciler)
- func (s *CLILoginService) Start(ctx context.Context, p StartParams) (*StartResult, error)
- type DualMailer
- type GlobalAccount
- type InitResult
- type LogMailer
- type LoginParams
- type LoginSession
- type LoginSessionRepository
- type Mailer
- type OAuthTokenResult
- type PollResult
- type PollStatus
- type ReferralRecorder
- type RefreshToken
- type RefreshTokenRepository
- type RegisterParams
- type Repository
- type SMTPMailer
- type SeatReconciler
- type Service
- type StartParams
- type StartResult
- type VKShareReconciler
Constants ¶
const ( AccountStatusActive = "active" AccountStatusSuspended = "suspended" AccountStatusDeleted = "deleted" )
AccountStatus values for GlobalAccount.
const ( // LoginSessionStatusPendingEmailEntry is the initial state when the CLI has called // Init() but the browser has not yet submitted an email via Begin(). LoginSessionStatusPendingEmailEntry = "pending_email_entry" LoginSessionStatusPendingEmailActivation = "pending_email_activation" LoginSessionStatusApprovedPendingClaim = "approved_pending_claim" LoginSessionStatusTokenIssued = "token_issued" LoginSessionStatusDenied = "denied" LoginSessionStatusExpired = "expired" LoginSessionStatusCancelled = "cancelled" )
Login session status constants — drives the aikey login device flow state machine.
State transitions (web-UI flow):
pending_email_entry → pending_email_activation → approved_pending_claim → token_issued pending_email_entry → expired pending_email_activation → denied | expired | cancelled approved_pending_claim → denied
const LoginResendCooldown = 30 * time.Second
LoginResendCooldown is the minimum gap between two activation emails on the same login session (Begin resend or change-email). Balances anti-spam with legitimate "didn't get the mail" retries.
const LoginSessionTTL = 15 * time.Minute
LoginSessionTTL is the maximum lifetime of a login session.
const PollIntervalSeconds = 3
PollIntervalSeconds is the recommended polling interval hint returned to the CLI.
const RefreshTokenTTL = 30 * 24 * time.Hour
RefreshTokenTTL is the maximum lifetime of an issued refresh token.
Variables ¶
var ErrLoginSessionWrongState = errors.New("login session not in expected state")
ErrLoginSessionWrongState is returned by SetEmail / ResendEmail when the session row exists but is not in the status the method expects. The service layer translates this into the correct domain error for the caller (terminal / expired / cooldown).
Functions ¶
func ServeActivationPage ¶
func ServeActivationPage(w http.ResponseWriter, result *ActivateResult)
ServeActivationPage renders the email-activation result as an HTML page. Called by GET /v1/auth/cli/login/activate after Activate() completes.
func ServeLoginPage ¶
func ServeLoginPage(w http.ResponseWriter, _ *http.Request)
ServeLoginPage renders the browser-side email-entry UI for aikey login. Called by GET /auth/cli/login?s={session_id}&d={device_code}
Types ¶
type ActivateResult ¶
type ActivateResult struct {
Success bool
Email string
LoginToken string // fallback copy-paste token; empty on failure
Message string
RedirectURL string // full URL for post-activation redirect (e.g. http://localhost:3000/user/overview)
}
ActivateResult describes the activation outcome; rendered as an HTML page.
type ActivationMessage ¶
type ActivationMessage struct {
ToEmail string
ActivationURL string
OSPlatform string // darwin/linux/windows; empty ⇒ "unknown device"
SentAt time.Time // used for Subject variation + Date header; caller sets to time.Now().UTC()
LoginSessionID string // used to build an RFC 5322 Message-ID that correlates with server logs
}
ActivationMessage carries the context needed to render and send an activation email. A struct (rather than positional args) keeps the Mailer interface stable as we add more context (device, IP, locale…).
Why: varying Subject per send — on OS/time — lowers the probability that anti-spam engines flag repeated logins as duplicate content, AND gives the user a visible cue to tell successive login emails apart.
type BeginParams ¶
type BeginParams struct {
SessionID string
DeviceCode string
Email string
ReferrerID string // optional: account_id of the user who shared the invite link
}
BeginParams carries the email submitted via the web login page.
type BeginResult ¶
type BeginResult struct {
MaskedEmail string
}
BeginResult is returned to the web login page after the activation email is sent.
type CLILoginService ¶
type CLILoginService struct {
// contains filtered or unexported fields
}
func NewCLILoginService ¶
func NewCLILoginService( identityRepo Repository, sessionRepo LoginSessionRepository, refreshRepo RefreshTokenRepository, reconciler SeatReconciler, tokens *shared.TokenService, mailer Mailer, baseURL string, webBaseURL string, logger *slog.Logger, ) *CLILoginService
NewCLILoginService creates a CLILoginService.
func (*CLILoginService) Activate ¶
func (s *CLILoginService) Activate(ctx context.Context, activationToken string) (*ActivateResult, error)
Activate processes the email-link click.
Steps:
- Validate activation token and session state.
- Find or create the GlobalAccount (no password — OAuth-only).
- Idempotently reconcile OrgSeats by email.
- Generate the fallback login_token and mark session approved_pending_claim.
func (*CLILoginService) Begin ¶
func (s *CLILoginService) Begin(ctx context.Context, p BeginParams) (*BeginResult, error)
Begin attaches an email to a pending_email_entry session and sends the activation email. Called from the browser login page after the user submits their address.
func (*CLILoginService) Exchange ¶
func (s *CLILoginService) Exchange(ctx context.Context, sessionID, loginToken string) (*OAuthTokenResult, error)
Exchange redeems the one-time login_token (shown on the web activation page) for OAuth tokens. Used as a fallback when CLI polling did not complete.
func (*CLILoginService) Init ¶
func (s *CLILoginService) Init(ctx context.Context, clientName, clientVersion, osPlatform string) (*InitResult, error)
Init creates an empty login session (no email yet) and returns the session credentials. The CLI opens the web login page with these credentials so that the user can enter their email in the browser.
func (*CLILoginService) Poll ¶
func (s *CLILoginService) Poll(ctx context.Context, sessionID, deviceCode string) (*PollResult, error)
Poll returns the current login session state and issues tokens when the session has been approved via email activation.
func (*CLILoginService) Refresh ¶
func (s *CLILoginService) Refresh(ctx context.Context, refreshTokenPlain string) (*OAuthTokenResult, error)
Refresh issues a new access token using a valid refresh token. The refresh token itself is not rotated (single-use rotation is a future upgrade).
func (*CLILoginService) SetReferralRecorder ¶
func (s *CLILoginService) SetReferralRecorder(r ReferralRecorder)
SetReferralRecorder attaches an optional referral recorder. Must be called before serving requests. Nil disables referral tracking.
func (*CLILoginService) SetVKShareReconciler ¶
func (s *CLILoginService) SetVKShareReconciler(r VKShareReconciler)
SetVKShareReconciler attaches an optional VK share-status reconciler. When set, Activate() will also transition pending_claim VKs to claimed after seat reconciliation succeeds. Nil disables VK share reconciliation.
func (*CLILoginService) Start ¶
func (s *CLILoginService) Start(ctx context.Context, p StartParams) (*StartResult, error)
Start creates a login session and sends an activation email. The CLI retains device_code and login_session_id for subsequent Poll calls.
type DualMailer ¶
type DualMailer struct {
// contains filtered or unexported fields
}
DualMailer sends activation emails via a primary mailer (e.g. SMTP) and also logs the activation URL for diagnostics. Used by trial-server sandbox where SMTP delivery may be unreliable but logs are always accessible.
func NewDualMailer ¶
func NewDualMailer(primary Mailer, log *LogMailer, logger *slog.Logger) *DualMailer
NewDualMailer creates a mailer that sends via primary AND logs the URL.
func (*DualMailer) SendActivationEmail ¶
func (m *DualMailer) SendActivationEmail(ctx context.Context, msg ActivationMessage) error
type GlobalAccount ¶
type GlobalAccount struct {
AccountID string
Email string
AccountStatus string
PasswordHash string // bcrypt; empty for SSO-only accounts
CreatedAt time.Time
LastLoginAt *time.Time
}
GlobalAccount represents a platform-wide user identity. It is NOT an org member; membership is expressed via OrgSeat.
func (*GlobalAccount) IsActive ¶
func (a *GlobalAccount) IsActive() bool
IsActive returns true if the account may authenticate.
type InitResult ¶
type InitResult struct {
LoginSessionID string
DeviceCode string
PollIntervalSeconds int
ExpiresInSeconds int
}
InitResult is returned immediately after Init — the CLI opens a browser to the login page and begins polling; the browser then calls Begin() with the user's email.
type LogMailer ¶
type LogMailer struct {
// contains filtered or unexported fields
}
LogMailer writes activation URLs to the structured logger — local dev only.
func NewLogMailer ¶
NewLogMailer creates a LogMailer that logs activation URLs instead of sending email.
func (*LogMailer) SendActivationEmail ¶
func (m *LogMailer) SendActivationEmail(_ context.Context, msg ActivationMessage) error
type LoginParams ¶
LoginParams carries credentials for login.
type LoginSession ¶
type LoginSession struct {
LoginSessionID string
DeviceCode string
ActivationToken string // embedded in email link; consumed on first valid use
LoginToken *string // fallback copy-paste token; nil until activation
LoginTokenUsed bool
Email string
ClientName string
ClientVersion string
OSPlatform string
Status string
AccountID *string
ExpiresAt time.Time
CreatedAt time.Time
// LastEmailSentAt tracks when the activation email was last (re-)sent.
// Drives the per-session resend cooldown; nil on freshly-created sessions
// that have not yet entered pending_email_activation.
LastEmailSentAt *time.Time
}
LoginSession represents a single aikey login attempt from a CLI device.
The device_code is held by the CLI and used to poll for status. The activation_token is embedded in the email link and consumed on first click. The login_token is a one-time copy-paste fallback shown on the web activation page.
func (*LoginSession) IsExpired ¶
func (s *LoginSession) IsExpired() bool
IsExpired returns true if the session has passed its expiry time.
type LoginSessionRepository ¶
type LoginSessionRepository interface {
Create(ctx context.Context, session *LoginSession) error
FindByID(ctx context.Context, sessionID string) (*LoginSession, error)
FindByDeviceCode(ctx context.Context, deviceCode string) (*LoginSession, error)
FindByActivationToken(ctx context.Context, token string) (*LoginSession, error)
// SetEmail attaches an email and a fresh activation_token to a pending_email_entry
// session, transitioning it to pending_email_activation so that the activation
// email can be sent. Returns an error (without mutating state) if session_id /
// device_code do not match or the session is not in pending_email_entry state.
// Also stamps last_email_sent_at = now so subsequent Begin calls can enforce
// the resend cooldown.
SetEmail(ctx context.Context, sessionID, deviceCode, email, activationToken string) error
// ResendEmail rotates activation_token + email (email may change) on a
// session that is already in pending_email_activation. Enforces the check
// at SQL level: the UPDATE only matches when status = pending_email_activation
// AND session_id/device_code match. Also updates last_email_sent_at = now.
// Returns a sentinel error (ErrSessionNotInActivation) when no row matches,
// so the service layer can distinguish "race" from "wrong input".
ResendEmail(ctx context.Context, sessionID, deviceCode, email, activationToken string) error
// Approve marks the session approved_pending_claim and records the account_id
// and the one-time login_token for the copy-paste fallback path.
Approve(ctx context.Context, sessionID, loginToken, accountID string) error
// IssueToken marks the session token_issued after OAuth tokens have been sent.
IssueToken(ctx context.Context, sessionID string) error
// MarkLoginTokenUsed prevents a login_token from being exchanged a second time.
MarkLoginTokenUsed(ctx context.Context, sessionID string) error
// Deny marks the session denied (suspended account, unresolvable conflict, etc.).
Deny(ctx context.Context, sessionID string) error
}
LoginSessionRepository stores and retrieves LoginSession records.
func NewLoginSessionRepository ¶
func NewLoginSessionRepository(db *shared.DB) LoginSessionRepository
NewLoginSessionRepository creates a SQL-backed LoginSessionRepository (PostgreSQL or SQLite — dialect handled by shared.DB).
type Mailer ¶
type Mailer interface {
SendActivationEmail(ctx context.Context, msg ActivationMessage) error
}
Mailer sends activation emails. Replace LogMailer with a real SMTP/SES implementation for production.
type OAuthTokenResult ¶
type OAuthTokenResult struct {
AccessToken string
RefreshToken string
TokenType string
ExpiresIn int
AccountID string
Email string
}
OAuthTokenResult is the OAuth token pair returned to the CLI.
type PollResult ¶
type PollResult struct {
Status PollStatus
Token *OAuthTokenResult // non-nil only when Status == PollStatusApproved
}
PollResult is the response to a Poll request.
type PollStatus ¶
type PollStatus string
PollStatus is the status code returned to the CLI on each poll.
const ( PollStatusPending PollStatus = "pending" // waiting for email activation PollStatusApproved PollStatus = "approved" // tokens included in this response PollStatusDenied PollStatus = "denied" // session denied; do not retry PollStatusExpired PollStatus = "expired" // session timed out PollStatusTokenClaimed PollStatus = "token_claimed" // tokens already issued to CLI )
type ReferralRecorder ¶
type ReferralRecorder interface {
RecordReferral(ctx context.Context, referrerAccountID, referredEmail string) error
CompleteReferral(ctx context.Context, referredEmail, referredAccountID string) error
}
CLILoginService orchestrates the aikey login OAuth device flow.
Flow: Start → [email click] → Activate → [CLI polls] → Poll → (token issued)
Fallback: Activate shows login_token → Exchange → (token issued) Renewal: Refresh
ReferralRecorder is an optional side-path interface for recording invite referrals. Errors from this interface must never block the login flow.
type RefreshToken ¶
type RefreshToken struct {
TokenID string
AccountID string
TokenHash string // SHA-256 hex of the plaintext token; plaintext is never stored
LoginSessionID *string // the session that originally issued this token
Revoked bool
ExpiresAt time.Time
CreatedAt time.Time
}
RefreshToken is a long-lived opaque credential stored as a SHA-256 hash. Presenting the original plaintext allows issuing new short-lived access tokens without requiring the user to re-run aikey login.
type RefreshTokenRepository ¶
type RefreshTokenRepository interface {
Create(ctx context.Context, rt *RefreshToken) error
FindByHash(ctx context.Context, hash string) (*RefreshToken, error)
Revoke(ctx context.Context, tokenID string) error
}
RefreshTokenRepository stores and retrieves RefreshToken records.
func NewRefreshTokenRepository ¶
func NewRefreshTokenRepository(db *shared.DB) RefreshTokenRepository
NewRefreshTokenRepository creates a SQL-backed RefreshTokenRepository (PostgreSQL or SQLite — dialect handled by shared.DB).
type RegisterParams ¶
RegisterParams carries the input for account registration.
type Repository ¶
type Repository interface {
Create(ctx context.Context, account *GlobalAccount) error
FindByID(ctx context.Context, accountID string) (*GlobalAccount, error)
FindByEmail(ctx context.Context, email string) (*GlobalAccount, error)
UpdateLastLogin(ctx context.Context, accountID string) error
}
Repository defines the storage contract for GlobalAccount. Implementations live in repository_sql.go (dual-dialect — PostgreSQL or SQLite via shared.DB); mocks in tests.
func NewSQLRepository ¶
func NewSQLRepository(db *shared.DB) Repository
NewSQLRepository creates a Repository backed by either PG or SQLite.
type SMTPMailer ¶
type SMTPMailer struct {
// contains filtered or unexported fields
}
SMTPMailer sends activation emails via SMTP over implicit TLS (port 465). Suitable for Alibaba Enterprise Mail and other providers using SMTPS.
func NewSMTPMailer ¶
func NewSMTPMailer(host string, port int, user, password, from string, logger *slog.Logger) *SMTPMailer
NewSMTPMailer creates a production-ready SMTP mailer.
func (*SMTPMailer) SendActivationEmail ¶
func (m *SMTPMailer) SendActivationEmail(_ context.Context, am ActivationMessage) error
SendActivationEmail sends a styled activation link email to the given address.
type SeatReconciler ¶
type SeatReconciler interface {
// ReconcileByEmail idempotently binds all pending_claim seats with
// invited_email = email to accountID. Returns seats bound (0 = already done).
ReconcileByEmail(ctx context.Context, email, accountID string) (int, error)
}
SeatReconciler links pending OrgSeats to a newly-activated account. Implemented by organization.Service (structural typing — no import cycle).
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service handles identity use cases: registration and login.
TODO(evolution): when moving to half-DDD, split LoginCommand / RegisterCommand into explicit command structs and route through a command bus.
func NewService ¶
func NewService(repo Repository) *Service
NewService creates an identity Service backed by the provided repository.
func (*Service) Login ¶
func (s *Service) Login(ctx context.Context, p LoginParams) (*GlobalAccount, error)
Login validates credentials and returns the account on success.
func (*Service) Register ¶
func (s *Service) Register(ctx context.Context, p RegisterParams) (*GlobalAccount, error)
Register creates a new GlobalAccount. Returns ErrConflict if email is taken.
type StartParams ¶
StartParams carries CLI input for initiating a login session.
type StartResult ¶
type StartResult struct {
LoginSessionID string
DeviceCode string
MaskedEmail string
PollIntervalSeconds int
ExpiresInSeconds int
}
StartResult is returned to the CLI immediately after Start.
type VKShareReconciler ¶
type VKShareReconciler interface {
}
VKShareReconciler transitions VK share_status from pending_claim to claimed after seat reconciliation. Implemented by managedkey.Service (structural typing). Why: when a VK is issued BEFORE the member logs in, share_status stays pending_claim even after the seat becomes active. This reconciler closes that gap.