identity

package
v0.2.0-dev Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 30, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

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

View Source
const (
	AccountStatusActive    = "active"
	AccountStatusSuspended = "suspended"
	AccountStatusDeleted   = "deleted"
)

AccountStatus values for GlobalAccount.

View Source
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
View Source
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.

View Source
const LoginSessionTTL = 15 * time.Minute

LoginSessionTTL is the maximum lifetime of a login session.

View Source
const PollIntervalSeconds = 3

PollIntervalSeconds is the recommended polling interval hint returned to the CLI.

View Source
const RefreshTokenTTL = 30 * 24 * time.Hour

RefreshTokenTTL is the maximum lifetime of an issued refresh token.

Variables

View Source
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:

  1. Validate activation token and session state.
  2. Find or create the GlobalAccount (no password — OAuth-only).
  3. Idempotently reconcile OrgSeats by email.
  4. Generate the fallback login_token and mark session approved_pending_claim.

func (*CLILoginService) Begin

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

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

func NewLogMailer(logger *slog.Logger) *LogMailer

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

type LoginParams struct {
	Email    string
	Password string
}

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

type RegisterParams struct {
	Email    string
	Password string // plaintext; hashed before storage
}

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) GetByID

func (s *Service) GetByID(ctx context.Context, accountID string) (*GlobalAccount, error)

GetByID fetches an account by ID; returns ErrNotFound if absent.

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

type StartParams struct {
	Email         string
	ClientName    string
	ClientVersion string
	OSPlatform    string
}

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 {
	ReconcileVKShareStatusByEmail(ctx context.Context, email string) (int, error)
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL