Documentation
¶
Overview ¶
Package session implements dashboard sessions minted by the email+password login flow. The credential is the operator's password (verified via bcrypt); the browser-side artefact is an httpOnly cookie tied to the user — see ADR-011 for the full rationale and what changed from the previous (key-bound) shape.
The package deliberately exposes only the surface the dashboard auth flow needs: Issue (mint a session for an authenticated user), Get (resolve a cookie value to its session row), Revoke (logout). Everything is keyed by sha256(raw_id); the raw id only ever lives in the cookie.
Index ¶
- Constants
- Variables
- func ClientIP(r *http.Request) string
- func HashID(raw string) string
- func Middleware(svc *Service) func(http.Handler) http.Handler
- func MiddlewareOrAPIKey(sessSvc *Service, keySvc *auth.Service) func(http.Handler) http.Handler
- type CookieConfig
- type IssueInput
- type Service
- func (s *Service) Issue(ctx context.Context, in IssueInput) (rawID string, sess Session, err error)
- func (s *Service) Resolve(ctx context.Context, rawID string) (Session, error)
- func (s *Service) Revoke(ctx context.Context, rawID string) error
- func (s *Service) RevokeAllForUser(ctx context.Context, userID string) error
- func (s *Service) SetLivemode(ctx context.Context, rawID string, livemode bool) error
- type Session
- type Store
Constants ¶
const CookieName = "velox_session"
CookieName is the HTTP cookie that carries the raw session id. Kept constant so middleware, login, and logout all reference the same name without risk of drift.
const DefaultTTL = 7 * 24 * time.Hour
DefaultTTL is the default session lifetime. 7 days matches GitHub's auth cookie default and the prior (ADR-008) value; long enough that operators don't see a daily login prompt, short enough that a stolen cookie has bounded lifetime even if the operator never realises and the cookie isn't revoked server-side.
Variables ¶
var ErrNotFound = errors.New("session: not found")
ErrNotFound signals that a session id resolves to no row, or to a row that has been revoked or expired. Callers funnel all three into 401 to deny enumeration of session ids.
Functions ¶
func ClientIP ¶
ClientIP pulls the caller's IP, preferring X-Forwarded-For (set by the RealIP middleware upstream). The stored value is informational — session validation isn't bound to IP for usability reasons (mobile network switching, VPN flips, etc.).
func HashID ¶
HashID returns the storage form of a raw session id. Exported so the logout handler can hash the cookie value before calling Revoke.
func Middleware ¶
Middleware accepts a `velox_session` httpOnly cookie. On hit it resolves the session, projects the parent key's tenant context onto the request, and forwards. On miss it 401s without falling back to API-key auth — use MiddlewareOrAPIKey for routes that should accept either credential.
func MiddlewareOrAPIKey ¶
MiddlewareOrAPIKey accepts either a session cookie OR an `Authorization: Bearer <api_key>` header. The dashboard rides the cookie path; SDKs and curl callers ride the API-key path. Cookie takes precedence when both are present so a browser tab with a stale Authorization header doesn't accidentally bypass session revocation.
Types ¶
type CookieConfig ¶
CookieConfig centralises cookie attributes. Secure is pinned by APP_ENV: off for local (HTTP), on for staging/production (HTTPS). SameSite=Lax keeps the cookie attached across top-level navigation (correct for a first-party dashboard) while blocking most cross-site CSRF.
func DefaultCookieConfig ¶
func DefaultCookieConfig() CookieConfig
DefaultCookieConfig reads APP_ENV and returns a sensible default. Tests override fields directly.
func (CookieConfig) ClearCookie ¶
func (c CookieConfig) ClearCookie(w http.ResponseWriter)
ClearCookie writes a cleared (Max-Age=-1) cookie on the response. Used by the logout handler.
func (CookieConfig) SetCookie ¶
func (c CookieConfig) SetCookie(w http.ResponseWriter, raw string, expires time.Time)
SetCookie writes the session cookie on the response. Exported so the auth login handler in internal/userauth can call it after Issue without depending on internals here.
type IssueInput ¶
IssueInput is the contract for Issue. Callers (the login handler) pass everything needed to mint a session row — the user has already been authenticated by user.Service.Authenticate.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the auth-flow facade over Store. Issue mints a session for an authenticated user (caller has already verified the password); Resolve looks up a raw cookie value and returns the Session if it's still active; Revoke handles logout.
func NewService ¶
NewService wires defaults: real wall clock, DefaultTTL.
func (*Service) Issue ¶
Issue creates a session row and returns the raw cookie value the caller should set on the response. The raw value is shown to the caller exactly once; the DB stores sha256(raw) so a snapshot can't be replayed.
func (*Service) Resolve ¶
Resolve looks up a raw cookie value and returns the Session if it's active. Revoked or expired rows return ErrNotFound — the middleware collapses both into 401 to deny session-id enumeration.
func (*Service) Revoke ¶
Revoke marks the session row as revoked. Idempotent — revoking an already-revoked or non-existent row is a no-op.
func (*Service) RevokeAllForUser ¶
RevokeAllForUser revokes every active session belonging to a user. The password-reset flow calls this after the new password is set so a session minted from a stolen cookie can't outlive the credential change. Idempotent — no active sessions is a no-op.
func (*Service) SetLivemode ¶
SetLivemode flips the active mode (test/live) on the cookie session. Same operator switches between modes without re-authenticating; every downstream request inherits the new mode via session.Resolve.
type Session ¶
type Session struct {
IDHash string
UserID string
TenantID string
Livemode bool
CreatedAt time.Time
LastSeenAt time.Time
ExpiresAt time.Time
RevokedAt *time.Time
UserAgent string
IP string
}
Session is the domain row. id_hash is sha256(raw); the raw id never leaves the cookie. UserID identifies the operator the session was minted for; sessions are user-bound (ADR-011), not key-bound. Livemode is captured at session-issue time and stays static — sessions don't toggle modes; a mode flip would mint a new session.
type Store ¶
type Store interface {
Insert(ctx context.Context, s Session) error
GetByIDHash(ctx context.Context, idHash string) (Session, error)
Revoke(ctx context.Context, idHash string) error
RevokeAllForUser(ctx context.Context, userID string) error
UpdateLivemode(ctx context.Context, idHash string, livemode bool) error
}
Store is the persistence interface for dashboard_sessions rows. Narrow on purpose — the dashboard auth flow only needs Insert, GetByIDHash, Revoke. Any further surface (pruning expired rows, listing for an ops UI) belongs on a sibling type when it ships, not here.
Pre-ADR-011 included RevokeAllForKey for the API-key-revoke fan-out path; that's gone now that sessions are user-bound and independent of API key lifecycle.
func NewPostgresStore ¶
NewPostgresStore wires the postgres-backed implementation. Sessions query by id_hash (PK) so RLS isn't strictly necessary — there's no cross-tenant overlap on the id space — but inserts and tenant-scoped reads still run inside TxBypass since the session id is the auth boundary itself.