session

package
v0.3.2 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 14 Imported by: 0

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

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

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

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

func ClientIP(r *http.Request) string

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

func HashID(raw string) string

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

func Middleware(svc *Service) func(http.Handler) http.Handler

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

func MiddlewareOrAPIKey(sessSvc *Service, keySvc *auth.Service) func(http.Handler) http.Handler

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

type CookieConfig struct {
	Domain   string
	Secure   bool
	SameSite http.SameSite
	Path     string
}

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

type IssueInput struct {
	UserID    string
	TenantID  string
	Livemode  bool
	UserAgent string
	IP        string
}

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

func NewService(store Store) *Service

NewService wires defaults: real wall clock, DefaultTTL.

func (*Service) Issue

func (s *Service) Issue(ctx context.Context, in IssueInput) (rawID string, sess Session, err error)

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

func (s *Service) Resolve(ctx context.Context, rawID string) (Session, error)

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

func (s *Service) Revoke(ctx context.Context, rawID string) error

Revoke marks the session row as revoked. Idempotent — revoking an already-revoked or non-existent row is a no-op.

func (*Service) RevokeAllForUser

func (s *Service) RevokeAllForUser(ctx context.Context, userID string) error

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

func (s *Service) SetLivemode(ctx context.Context, rawID string, livemode bool) error

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.

func (Session) IsActive

func (s Session) IsActive(now time.Time) bool

IsActive reports whether the session can authenticate a request: not revoked and not yet expired. The DB query already filters revoked rows out for the active path; this guard catches expiry without a clock-keyed index lookup on every request.

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

func NewPostgresStore(db *postgres.DB) Store

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.

Jump to

Keyboard shortcuts

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