Help us shape the future of Go! Take the 2026 Go Developer Survey

api

package
v0.40.13 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 2026 License: MIT Imports: 107 Imported by: 0

Documentation

Overview

Package api wires the HTTP surface for Rivolt. It assembles routes, middleware, and handler dependencies into a single chi Mux.

Index

Constants

View Source
const (

	// AppSessionTTL is an app session's lifetime (sliding; sessions.AppTTL).
	AppSessionTTL = sessions.AppTTL
)
View Source
const ImpersonateHeader = "X-Rivolt-Impersonate"

ImpersonateHeader is the header an admin sets to view the app exactly as another user sees it — read-only, for support and debugging (e.g. confirming what a user's vehicle card actually shows them). Trusted ONLY when the caller already carries role=admin via the real cookie/OIDC session established earlier in the chain; see impersonationMW.

View Source
const SecureAccountLabel = "Reset password and remove all passkeys"

SecureAccountLabel is the one name of the "secure the account" action wherever a person reads it: the Settings → Security button (web, ux) and the "passkey added" email. They must be the same string. The code calls the action "secure".

View Source
const TripRecapKind = "trip_recap"

TripRecapKind is the engage kind of the saved-trip recap notification.

Variables

View Source
var ErrNoAppleRevoker = errors.New("apple revoke: no Apple provider configured")

ErrNoAppleRevoker is the queued error when no Apple provider is configured to revoke with.

Functions

func FailLimiterKey added in v0.40.8

func FailLimiterKey(secret []byte) []byte

FailLimiterKey derives the HMAC key for the failure limiter's Redis key names from the server's secret (a purpose-labelled subkey, never the secret itself). nil for an empty secret.

func MilestoneLease added in v0.37.5

func MilestoneLease(pool *sql.DB) func(ctx context.Context, uid, vehicle uuid.UUID) milestones.LeaseTerm

MilestoneLease says how far through its lease a vehicle is (Total 0: none).

func MilestoneLedgerInput added in v0.37.5

func MilestoneLedgerInput(pool *sql.DB, sf *settings.Factory) func(ctx context.Context, uid uuid.UUID, rivianID string) (ledger.Input, error)

MilestoneLedgerInput loads a vehicle's whole ledger input for the milestone checker: the same code path as the Money tab.

func New

func New(d Deps) http.Handler

New builds the root mux with all routes mounted. It panics if the request pool is on but a system-side store is missing (a startup error: main builds the router once, before it listens).

func RoutePatternFromContext added in v0.39.7

func RoutePatternFromContext(ctx context.Context) string

RoutePatternFromContext is the chi route pattern of the request in ctx ("/api/vehicles/{vehicleID}/now"), for the label of a metric: never the path, whose ids would make the series unbounded. "none" when ctx is not a request's (a job, a background goroutine) and "unmatched" when no route matched (a 404). Once routing has run, the pattern is on the context object the handler's own context carries, so a query made from a handler sees it.

func RunAppleRevokeRetrier added in v0.39.3

func RunAppleRevokeRetrier(ctx context.Context, a *AppleRevocation, interval time.Duration)

RunAppleRevokeRetrier retries queued Apple revocations every interval until ctx ends, and drops the ones past 30 days or 20 attempts. Safe on every replica (rows are claimed with SKIP LOCKED). No-op without a database.

func RunIdPDeleteRetrier added in v0.32.0

func RunIdPDeleteRetrier(ctx context.Context, d *sql.DB, ac idp.UserProvider, log *slog.Logger, interval time.Duration)

RunIdPDeleteRetrier retries recorded IdP deletion failures every interval until ctx ends, with per-row exponential backoff (see db.ClaimDueIdPDeleteFailures). Safe to run on every replica: rows are claimed with SKIP LOCKED. No-op without a database or an enabled IdP.

func TripRecapBody added in v0.40.9

func TripRecapBody(c ledger.PlanComparison) (string, bool)

TripRecapBody is the notification text: the arrival charge and the cost, planned against actual, both marked as estimates, or whichever of the two is known. It reports false when neither is.

func ValidAppID added in v0.38.11

func ValidAppID(id string) bool

ValidAppID reports whether id looks like "TEAMID.bundle.id".

Types

type AppAuth added in v0.38.10

type AppAuth struct {
	Store *appauth.Store
	// NewSession creates a kind=app session for uid and returns its raw
	// token and expiry. (sessions.Store.Create with Kind "app".)
	NewSession func(ctx context.Context, uid uuid.UUID, r *http.Request, deviceLabel string) (string, time.Time, error)
	// CallbackURL is where finish sends the browser: the app's fixed
	// callback (a Universal Link on rivolt.dev, rivolt://auth for a
	// self-hosted server). Never taken from the request (DA, condition 1).
	CallbackURL string
	// SecureCookie marks the state cookie Secure (as the session cookie).
	SecureCookie bool
	// MinIOSBuild, when > 0, is the oldest app build this server accepts;
	// /api/app/config reports it so the app can ask for an update.
	MinIOSBuild int
}

AppAuth is the server side of the iPhone app's sign-in (internal/appauth, iOS design §2). Mounted under /api/auth/app plus /api/app/config when set; never on the admin deployment.

type AppleRevocation added in v0.39.3

type AppleRevocation struct {
	DB *sql.DB
	// Token reads the account's kept refresh token ("" when it has none): appleid.Store.RefreshToken.
	Token func(ctx context.Context, uid uuid.UUID) (string, error)
	// Revoke revokes a refresh token at Apple: oidc.Service.AppleRevoke. nil (no Apple provider) queues every
	// revoke, which then expires unless Apple is configured again within 30 days.
	Revoke func(ctx context.Context, refreshToken string) error
	Sealer crypto.Sealer
	Log    *slog.Logger
}

AppleRevocation is the Sign in with Apple step of every account deletion (siwa-design.md, DA conditions): before the account's rows go, its Apple refresh token is revoked at Apple, which also ends the person's authorization of Rivolt in their Apple ID settings. When Apple can't be reached, the revoke is queued in idp_delete_failures with the token sealed under the KEK envelope (the deleted account's id as associated data), so it still opens after the cascade removed user_secrets; RunAppleRevokeRetrier retries it, and drops it after 30 days or 20 attempts. Only when both the revoke and the queueing fail does the deletion stop (the database is down then).

The token is never logged, and neither is any Apple identifier.

func (*AppleRevocation) BeforeDelete added in v0.39.3

func (a *AppleRevocation) BeforeDelete(ctx context.Context, uid uuid.UUID) error

BeforeDelete revokes uid's Apple token, or queues the revoke, and returns an error only when neither worked (the caller must then not delete the account). nil-safe: without an AppleRevocation there is nothing to do.

func (*AppleRevocation) Cancel added in v0.39.3

func (a *AppleRevocation) Cancel(ctx context.Context, uid uuid.UUID)

Cancel drops a queued revoke for an account that turned out not to be deleted (the inactive purge's re-check). The revoke may already have happened at Apple; the person's next Apple sign-in authorizes Rivolt again.

type ArchiveRestorer added in v0.37.8

type ArchiveRestorer interface {
	Restore(ctx context.Context, uid, vid uuid.UUID, month time.Time, dryRun, wait bool) (archive.RestoreResult, error)
}

ArchiveRestorer is archive.Restorer as the admin API uses it.

type Deps

type Deps struct {
	// SampleRetentionMonths is RIVOLT_SAMPLE_RETENTION_MONTHS (0 keeps raw
	// samples forever). A restore skips samples older than the cutoff, which
	// the next retention sweep would delete anyway.
	SampleRetentionMonths int
	// ArchiveRows reads the telemetry archive (N19) so the backup download
	// carries full detail for months thinning has reduced; nil when the
	// archive is off, and the backup is the database alone.
	ArchiveRows samples.ArchiveRows
	// ArchiveRestorer puts one archived vehicle-month back in full detail
	// (POST /api/admin/archive/restore); nil when the archive is off.
	ArchiveRestorer ArchiveRestorer

	Rivian rivian.Client
	// Accounts hands out per-user *rivian.LiveClient instances. The
	// /api/settings/rivian sign-in surface resolves the request user
	// to its private client via Accounts.For(uid), so concurrent
	// Login/Restore from different sessions can no longer corrupt
	// each other's tokens. nil when the stub client is in use
	// (nothing to sign into).
	Accounts rivian.AccountRegistry
	// Monitors hands out per-user *rivian.StateMonitor instances
	// that own the websocket subscription + recorder for that user's
	// vehicles. nil in mock/stub modes (no recorder needed there).
	Monitors    *rivian.MonitorRegistry
	PushService *push.Service
	// Per-user data-plane factories. The router resolves req.user →
	// uid → factory.For(uid) once per request, so handlers always
	// see stores scoped to the caller. nil-safe: handlers gate on
	// the resolved *Store being non-nil before reading.
	Drives   *drives.Factory
	Charges  *charges.Factory
	Samples  *samples.Factory
	Settings *settings.Factory
	Push     *push.Factory
	Trips    *trips.Factory
	// PackHealth is the per-vehicle derived-pack-capacity store.
	// Not a factory because samples are scoped by vehicle_id, not
	// user_id; the handler authorizes via the vehicleScoped
	// ownership middleware before reading. Nil-safe: the endpoint
	// returns 503 when unset.
	PackHealth *packhealth.Store
	// SettingsMgr exposes install-wide AI provider config (keys,
	// default models). Install-wide because the deployer pays the
	// LLM bill for every user. May be nil; the admin track
	// repopulates it from app_settings and gates the handlers
	// behind requireAdmin.
	SettingsMgr *settings.Manager
	// Auth, when non-nil, gates /api/* behind a session cookie
	// or trusted-proxy header. Whether unauthenticated requests
	// are 401'd is governed by AuthEnforced below; with Auth
	// non-nil but AuthEnforced false, sessions still resolve into
	// request context but the API stays open (legacy single-tenant
	// UX so docker-compose upgrades don't lock anyone out).
	Auth *auth.Service
	// Sessions, when set, mounts the signed-in user's device list under
	// /api/auth/sessions (list, revoke one, sign out everywhere else).
	Sessions *SessionsAPI
	// AuthEnforced flips on the requireUser middleware for /api/*.
	// True when at least one real issuer is configured (OIDC,
	// trusted-proxy, or the debug bypass).
	AuthEnforced bool
	// TermsLookup replaces the database read of a user's accepted Terms in the server-side Terms gate (tests).
	TermsLookup func(ctx context.Context, uid uuid.UUID) (db.Terms, error)
	// ImpersonationDisabled mirrors $RIVOLT_IMPERSONATION_DISABLED.
	// true makes the impersonation middleware ignore
	// X-Rivolt-Impersonate outright (regardless of caller role) and
	// is surfaced via /api/config so the SPA hides the admin
	// "View as" control instead of offering a dead button.
	ImpersonationDisabled bool
	// AdminDisabled mirrors $RIVOLT_ADMIN_DISABLED: the public site
	// serves no admin surface. /admin* and /api/admin* answer 404 (even
	// for an admin session), /api/auth/me reports an admin as "user" so
	// the SPA hides the link, and impersonation is off. Off by default;
	// for the split to admin.rivolt.dev (scratchpad admin-split.md).
	AdminDisabled bool
	// AdminOnly is RIVOLT_ADMIN_ONLY: the admin deployment. Every request
	// must carry a valid Cloudflare Access JWT (Access; nil denies all),
	// and only the admin surface exists under /api.
	AdminOnly bool
	// AdminHealth feeds GET /api/admin/health-summary (nil: every section reads unavailable).
	AdminHealth *adminhealth.Service
	// PlanLegs caches live planner calls and caps them per user (nil: neither).
	PlanLegs *planleg.Cache
	Access   *cfaccess.Verifier
	// AdminDebugProbes mirrors $RIVOLT_ADMIN_DEBUG_PROBES: mounts the raw
	// Rivian probes under /api/admin (trips/plan/raw, trips/plan/diag,
	// gql/type, gql/enum, gql/raw), which send arbitrary queries to Rivian
	// as the admin. Off by default; an operator turns it on for a session.
	AdminDebugProbes bool
	// OIDC, when non-nil, mounts /api/auth/oidc/* — the third
	// auth issuer alongside static creds and trusted-proxy
	// header. nil disables the social-login button row in the
	// SPA but doesn't affect any other code path.
	OIDC *oidc.Service
	// AppleNotifications, when non-nil, is POST /api/auth/apple/notifications: Apple's
	// server-to-server notifications (consent revoked, Apple account deleted, relay email
	// off or on). Public (Apple calls it, no cookie), behind a body cap and a per-IP limit.
	AppleNotifications http.Handler
	// Passkeys, when non-nil, adds WebAuthn sign-in to Rivolt's email accounts: the management routes under /api/auth/passkeys
	// and, with Hydra and Kratos wired, the passkey sign-in on the Hydra login page. nil: no passkey route exists.
	Passkeys *passkeys.Service
	// PasskeyStore is the passkey tables on the system pool, present whenever there is a database, whether or not WebAuthn is
	// configured. "Secure my account" and support's reset delete credentials through it: a deployment with passkeys off (the admin
	// site, or prod before the enable) can still hold rows, and must not skip the deletion because the sign-in service is not built.
	PasskeyStore passkeys.Store
	// PasskeyAccount replaces the database read of the signed-in account's email and name for the passkey routes (tests).
	PasskeyAccount func(ctx context.Context, uid uuid.UUID) (mail, display string, err error)
	// PasskeyMailer replaces the mailer for the "a passkey was added" notice (tests).
	PasskeyMailer interface {
		Send(ctx context.Context, m email.Message) error
	}
	// IOSAppIDs ("TEAMID.bundle.id", RIVOLT_IOS_APP_IDS) publishes the
	// iPhone app's associated domain: the AASA file and /app/callback. Empty:
	// neither is served.
	IOSAppIDs []string
	// AppAuth, when non-nil (with OIDC), mounts the iPhone app's sign-in
	// under /api/auth/app. nil: /api/app/config reports app_login=false.
	AppAuth *AppAuth
	// Hydra, when non-nil along with Kratos, mounts the custom
	// login + consent handlers under /api/auth/hydra. This makes
	// Rivolt the bring-your-own-UI for an Ory Hydra OAuth2 / OIDC
	// provider — downstream apps (ArgoCD, Grafana, etc.) federate
	// against Hydra and Hydra calls back into us for the user-
	// facing login prompt.
	Hydra  *hydra.Client
	Kratos *kratos.Client
	// Redis, when set, makes the failed-login limiters count across replicas (RedisPrefix is
	// the install's key prefix). Nil leaves them per pod.
	Redis       *redis.Client
	RedisPrefix string
	// RivianExpiryEmails is true when the sender of the Rivian connection expiry reminders runs
	// (RIVOLT_RIVIAN_EXPIRY_EMAILS=true with a mailer and engagement email not off). The
	// Settings toggle for the reminders is mounted only then, so it never shows as switched on
	// where nothing is sent. The signed unsubscribe links are always mounted.
	RivianExpiryEmails bool
	// FailLimiterKey is the HMAC key for the limiter's Redis key names (FailLimiterKey of the
	// server's secret); without it the limiters stay per pod.
	FailLimiterKey []byte
	// PurgeUserCaches deletes what is held of a user outside Postgres (the Redis live-state
	// snapshots) once their account is deleted. Best effort, nil: nothing to purge.
	PurgeUserCaches func(ctx context.Context, uid uuid.UUID)
	// HydraRememberFor controls how long Hydra remembers the
	// user's login + consent before forcing a fresh prompt. Zero
	// defers to Hydra's login_session lifespan. Sourced from the
	// RIVOLT_OIDC_HYDRA_REMEMBER_FOR env var in main.go.
	HydraRememberFor time.Duration
	WebFS            fs.FS
	Version          string
	// Changelog serves the release notes (GET /api/changelog and
	// /changelog.atom); nil leaves both unmounted.
	Changelog *changelog.Source
	// DB is the shared Postgres pool. Used by request middleware
	// that needs to answer "does this session user own this
	// vehicle?" without round-tripping through a per-user store.
	// Safe to be nil in legacy code paths that predate the
	// ownership middleware; ownership enforcement is only wired
	// when DB is non-nil.
	// DB is what request handling reads and writes: with RIVOLT_REQUEST_DATABASE_URL set it is the request pool (row-level security
	// applies, app.user_id comes from the request's context), otherwise the same pool as SystemDB. A site that forgot to say
	// otherwise therefore fails closed (an empty answer), never open.
	DB *sql.DB
	// SystemDB is the owner-role pool for paths that legitimately span users or run before a user is known: login, session and
	// token lookups, signup, account deletion, admin, signed links, pre-auth GETs. Using it is a decision with a comment at the
	// site (DA's RLS ruling). nil means DB (tests, and every deployment without the request pool).
	SystemDB *sql.DB
	// RequestPoolOn is set by main when DB is a separate request pool (RIVOLT_REQUEST_DATABASE_URL). With it set, New refuses to build
	// unless every system-side store is set too: the fallbacks (sysDB, systemSettings, systemSecrets) would otherwise run the system
	// sites on the request pool, where, for example, account deletion's last-admin count sees only the caller.
	RequestPoolOn bool
	// LoginForgetter forgets Hydra's remembered login for a user at plain sign-out (an IdentityRevoker). nil: nothing to forget.
	LoginForgetter LoginForgetter
	// SessionRevoker revokes every session and app token of a user (sessions.Store.RevokeAll), for support's "secure this account".
	SessionRevoker interface {
		RevokeAll(ctx context.Context, userID uuid.UUID) (int64, error)
	}
	// RememberedRevoker forgets the identity provider's remembered logins for a user (an IdentityRevoker), for the same.
	RememberedRevoker RememberedRevoker
	// SystemSettings is the settings factory on the system pool, for the sweeps that run without a request (the statement and weekly
	// recap emails). nil means Settings (no request pool).
	SystemSettings *settings.Factory
	// AuditKey keys the salted hash of a user id in the "account deleted" log line (derived from the cookie secret; empty
	// means a random key for the process).
	AuditKey []byte
	// Logger is the structured logger used by middleware for
	// infrastructure-class warnings (DB errors on ownership check,
	// etc.). nil is fine; events are dropped.
	Logger *slog.Logger
	// Flags is the operational-flag store (kill switch, future
	// pause_digest / pause_push rows). When nil the /api/admin/*
	// routes return 503 but the server still boots — the flag
	// surface is non-critical to rendering the app.
	Flags *flags.Store
	// AIBudget enforces the per-user daily cap on LLM-backed
	// endpoints (trip advice, drive efficiency). nil disables the
	// gate — the cost backstop fails open so no-DB and test paths
	// keep working.
	AIBudget *aibudget.Store
	// Secrets is the envelope-encrypted per-user blob store
	// (see internal/crypto, internal/secrets). Holds the
	// sealed rivian.Session and, later, AI provider keys and
	// per-user VAPID private keys. nil is tolerated so tests
	// and the mock/stub client don't have to stand up a
	// sealer; the Rivian sign-in surface becomes read-only
	// when it's absent.
	Secrets *secrets.Store
	// SystemSecrets is the secret store on the system pool, for the admin route that refreshes another user's Rivian session.
	// nil means Secrets (no request pool).
	SystemSecrets *secrets.Store
	// Metrics is the Prometheus instrumentation surface. When non-nil
	// the router wires per-handler latency + count tracking and
	// mounts /metrics. nil disables both — useful for tests that
	// don't want the global default registry pollution.
	Metrics *metrics.Metrics
	// Users, when non-nil, provisions user identities in the
	// configured IdP on POST /api/admin/users and DELETE.
	// nil (or a disabled provider) skips IdP provisioning —
	// the rivolt DB row is still created, callers must create
	// the OIDC identity out-of-band. See internal/idp.
	Users idp.UserProvider
	// AppleRevocation revokes (or queues the revoke of) an account's
	// Sign in with Apple token before any account deletion. nil-safe.
	AppleRevocation *AppleRevocation
	// SignupRequests, when non-nil, enables the public-facing
	// "request beta access" form at POST /api/signup/request and
	// the admin review surface at /api/admin/signup-requests/*.
	// Approve mints a magic-link signup token on the request row
	// and (if Email is wired) emails the requester. POST /api/signup
	// redeems that token to create the user.
	SignupRequests *signuprequests.Store
	// Email, when non-nil, sends transactional mail (currently just
	// signup approvals) via the Resend HTTP API. nil disables the
	// approval-email send; the admin still gets the code in the
	// approval response so it can be forwarded manually.
	Email *email.Client
	// StatementSigner signs the one-click unsubscribe links of the
	// monthly statement email (nil disables the unsubscribe routes).
	StatementSigner *statement.Signer
	// WeeklySigner signs the weekly recap's unsubscribe links (a scope of its
	// own, so they never verify as statement links). nil disables the recap.
	WeeklySigner *statement.Signer
	// BaseURL is the install's public origin (e.g. https://rivolt.dev),
	// sourced from $RIVOLT_PUBLIC_BASE_URL, else $RIVOLT_BASE_URL (the
	// admin deployment sets the former: its own host is behind Cloudflare
	// Access, so links it emails must point at the public site). Used
	// for user-facing links in emails: the magic-link signup URL
	// (admin approval and auto-approve) and the monthly statement's
	// links. Safe to leave empty in tests — the signup link falls back
	// to a relative path so copy-paste-into-the-same-tab still works.
	BaseURL string
	// ValhallaProxy, when non-nil, mounts a same-origin reverse
	// proxy at /api/maps/valhalla/* that forwards to a self-hosted
	// Valhalla. /api/config advertises whether the proxy is
	// mounted so the SPA picks the right base URL at boot. nil
	// leaves the route unmounted; the SPA renders raw GPS chords
	// without snapping.
	ValhallaProxy http.Handler
	// Valhalla is the self-hosted router the server itself calls (the overnight search puts a sparse path on the road with it); nil: none.
	Valhalla *maps.Valhalla
	// TilesProxy, when non-nil, mounts a same-origin reverse
	// proxy at /api/maps/tiles/* that forwards to a self-hosted
	// PMTiles file server (nginx serving the .pmtiles bundle
	// with byte-range support). nil leaves the route unmounted;
	// the SPA falls back to CARTO's public dark raster basemap.
	TilesProxy http.Handler
	// ChargersArchive, when non-nil, exposes a server-side
	// chargers-along-corridor query at /api/maps/chargers-along.
	// Backed by an in-memory copy of chargers.pmtiles so the SPA
	// can replace its per-tile HTTP fan-out with one POST.
	ChargersArchive *chargers.Archive
	// Photon is the self-hosted geocoder client. Empty BaseURL on
	// the client disables; /api/geocode falls through to
	// Open-Meteo's city-level service.
	Photon *geocoding.PhotonClient
	// WeatherClient is the shared Open-Meteo HTTP client used by
	// trip-planner weather adjustment. Distinct from the per-callsite
	// weather.NewClient() instances used elsewhere because the
	// planner reuses this in tight loops with MemCache. nil disables
	// weather adjustment (the plan response just omits the field).
	WeatherClient *weather.Client
	// WeatherCache memoises FetchHour results across overlapping plan
	// requests. Keyed on coarsened (lat, lon, hour). nil disables
	// caching; FetchHourCached falls through to the client.
	WeatherCache *weather.MemCache
}

Pools (row-level security, stage 2). DB, Drives, Charges, Samples, Settings, Push, Trips, Secrets, PackHealth, AIBudget and ArchiveRows (the backup's archive reader; a request with no user fails the backup instead of dropping months) are built in main on the request pool when RIVOLT_REQUEST_DATABASE_URL is set (otherwise on the system pool, as before): they serve per-user routes and every table they touch carries the tenant policy. SystemDB, SystemSettings and SystemSecrets are the owner-role counterparts for the few places that must span users or run before a user is known. Every other store below is built on the system pool, each for a reason (DA asked for this list in the S2-2 review):

  • Flags: install-wide switches, no tenant data. Request handlers read an in-memory snapshot; admin writes go through SystemDB.
  • SettingsMgr: app_settings, which the request role cannot read (provider keys).
  • SignupRequests: signup_requests, denied to the request role; signup and admin only.
  • Users: the identity provider's admin API, not a database.
  • Sessions, AppAuth, OIDC, Passkeys, AppleNotifications, AppleRevocation: sign-in, token and session paths. They run before a user is known, or manage the session store the auth middleware itself reads; each statement names the user it acts for.
  • Accounts, Monitors: the per-user Rivian clients and recorders (the monitor and recorder are system by policy).
  • ArchiveRestorer: the admin restore of one user's archived month; admin, so system.
  • Changelog, ChargersArchive, WeatherCache: not tenant data (a feed URL, the charger archive, an in-memory weather cache keyed by place and time).

Deps is the bag of dependencies the API router needs. Keep this small; avoid accumulating a "dependency soup" pattern.

All per-user state (data-plane stores, Rivian client, monitor) is reached through factories/registries keyed by the request user's uid — there is no singleton "current user" anywhere in the API surface.

type ForgetObserver added in v0.39.9

type ForgetObserver func(entry string, err error)

ForgetObserver records the outcome of one clearing for an entry point. nil is a no-op.

func NewForgetObserver added in v0.39.9

func NewForgetObserver(m *metrics.Metrics) ForgetObserver

NewForgetObserver builds the observer over the metrics; nil metrics give a nil (no-op) observer.

type IdentityRevoker added in v0.39.8

type IdentityRevoker struct {
	Hydra  *hydra.Client
	Kratos *kratos.Client
	// Username is the account's username (db.RawUsernameByID on the system pool).
	Username func(ctx context.Context, uid uuid.UUID) (string, error)
}

IdentityRevoker clears, for one user: Hydra's remembered logins, its consent grants (which revokes the OAuth tokens issued under them), and the Kratos sessions (LoginByPassword creates one per sign-in). It finds the Kratos identity from the account's username, which is the email. An account with no Kratos identity (Google, GitHub or Apple only) has nothing remembered here.

func (*IdentityRevoker) ForgetLogin added in v0.39.8

func (r *IdentityRevoker) ForgetLogin(ctx context.Context, uid uuid.UUID) error

ForgetLogin deletes the Hydra login sessions of the user's identity (see LoginForgetter) and the identity's Kratos sessions. The Kratos ones matter because the auth middleware accepts an ory_kratos_session cookie on rivolt.dev BEFORE it looks at Rivolt's own cookie: a browser that holds one is signed in with no Rivolt session at all, and "Sign out" would leave it so. The public Kratos browser flow is reachable through the ingress, so anyone with the password can mint one (30 days). Both steps are tried and every error returned.

func (*IdentityRevoker) RevokeRemembered added in v0.39.8

func (r *IdentityRevoker) RevokeRemembered(ctx context.Context, uid uuid.UUID) error

RevokeRemembered attempts all three steps and returns every error, so one failing does not leave the others undone.

type InactivePurge added in v0.36.8

type InactivePurge struct {
	// contains filtered or unexported fields
}

InactivePurge is the daily job.

func NewInactivePurge added in v0.36.8

func NewInactivePurge(pool *sql.DB, flag func() flags.InactivePurgeState, mailer *email.Client, users idp.UserProvider, baseURL string, logger *slog.Logger) *InactivePurge

NewInactivePurge wires the job over the pool. flag reads the live inactive_account_purge flag; mailer may be nil (nothing is warned or deleted then: a deletion is never allowed without a sent warning).

func (*InactivePurge) Run added in v0.36.8

func (j *InactivePurge) Run(ctx context.Context)

Run sweeps once a day until ctx ends, starting after a delay so a restart does not race boot, with jitter so replicas don't tick together. Every send and delete is claim- or lock-gated, so it is safe on every replica.

func (*InactivePurge) RunOnce added in v0.36.8

func (j *InactivePurge) RunOnce(ctx context.Context) PurgeResult

RunOnce is one pass.

func (*InactivePurge) WithAppleRevocation added in v0.39.3

func (j *InactivePurge) WithAppleRevocation(a *AppleRevocation) *InactivePurge

WithAppleRevocation makes each deletion revoke (or queue the revoke of) the account's Sign in with Apple token first, like every account deletion.

type LoginForgetter added in v0.39.8

type LoginForgetter interface {
	ForgetLogin(ctx context.Context, uid uuid.UUID) error
}

LoginForgetter forgets only the identity provider's remembered LOGIN for a user, which is what a plain sign-out needs: without it, "Sign out" ends Rivolt's session but Hydra still remembers the browser, and the next "Continue with Email" is signed straight back in with no password (the shared-computer case). It leaves the consent grants and the Kratos sessions alone, so signing out does not break an admin's other OAuth sessions.

type PurgeResult added in v0.36.8

type PurgeResult struct {
	Off       bool
	DryRun    bool
	Cancelled int
	Warned    int
	Deleted   int
	// UnwarnableDeleted counts accounts with no verified address deleted at day
	// 120 without a warning; UnwarnableDue is its dry-run count.
	UnwarnableDeleted int
	UnwarnableDue     int // dry run
	WarnDue           int // dry run
	DeleteDue         int // dry run
	WarnedWaiting     int // dry run
}

PurgeResult is what one run did, for logs and tests.

type RememberedRevoker added in v0.39.8

type RememberedRevoker interface {
	RevokeRemembered(ctx context.Context, uid uuid.UUID) error
}

RememberedRevoker forgets what the identity provider remembers about a user. Revoking Rivolt's own sessions is not a lock-out by itself: Hydra remembers an accepted login (RIVOLT_OIDC_HYDRA_REMEMBER_FOR, 24 h on prod) and answers the next login request from that browser with skip=true, so the browser is signed straight back in with no password. The people who revoke (sign out other devices, change or secure the account, support securing an account) mean it to be final.

type RequestStores added in v0.39.8

type RequestStores struct {
	Resolver   *db.VehicleResolverFactory
	Drives     *drives.Factory
	Charges    *charges.Factory
	Samples    *samples.Factory
	Settings   *settings.Factory
	Push       *push.Factory
	Trips      *trips.Factory
	PackHealth *packhealth.Store
	AIBudget   *aibudget.Store
	// Secrets is nil when there is no sealer (the Rivian sign-in surface is then read-only).
	Secrets *secrets.Store
}

RequestStores are the per-user stores request handling uses (row-level security, stage 2): every one of them reads and writes the caller's own rows. main and the pool-on integration tests both build them here, from the request pool req and the system pool sys, so what CI exercises is what production wires; a store added or moved is changed in one place.

sys is for the few statements in these stores that must span users or are DDL, each commented where it is used: APNs token registration (push) and a month's partition (samples).

func NewRequestStores added in v0.39.8

func NewRequestStores(req, sys *sql.DB, sealer rivoltcrypto.Sealer) RequestStores

NewRequestStores builds the request-side stores on req, with sys for their cross-user and DDL statements. sealer may be nil.

type SessionsAPI added in v0.38.12

type SessionsAPI struct {
	Store SessionsStore
	// Current returns the id of the session making the request, when it has one
	// (a cookie or app session; not header/Kratos/bypass auth). nil or false:
	// "current" is not marked and sign-out-everywhere-else is refused, since it
	// could not tell which session to keep.
	Current func(r *http.Request) (uuid.UUID, bool)
	// Remembered, when set, also forgets the identity provider's remembered logins after a revoke, so a revoked browser cannot
	// sign straight back in with no password (see RememberedRevoker). nil: nothing is remembered outside Rivolt (no Hydra).
	Remembered RememberedRevoker
	// Observe counts each clearing and its failures (see ForgetObserver); nil is a no-op.
	Observe ForgetObserver
}

SessionsAPI serves the signed-in user's own sessions: the browsers and the iPhone app that are signed in as them, and "sign out everywhere else" (iOS design §2, DA 5). Never the token hash or the IP address.

func (*SessionsAPI) List added in v0.38.12

func (a *SessionsAPI) List(w http.ResponseWriter, r *http.Request)

List serves GET /api/auth/sessions: {sessions, total, hidden_web}. By default it is the whole list, ordered by last use, and hidden_web is 0. A client that shows "N older browsers not shown · Show all" may ask for ?recent=N: every app session and the current one, plus the N most recently seen other web sessions, with the rest counted in hidden_web. A client that does not show that line must not send it, or an old session would simply not be there to be noticed. Revocation covers every session either way.

func (*SessionsAPI) Revoke added in v0.38.12

func (a *SessionsAPI) Revoke(w http.ResponseWriter, r *http.Request)

Revoke serves DELETE /api/auth/sessions/{id}: one of the caller's own sessions. Revoking the current one signs this device out.

func (*SessionsAPI) RevokeOthers added in v0.38.12

func (a *SessionsAPI) RevokeOthers(w http.ResponseWriter, r *http.Request)

RevokeOthers serves POST /api/auth/sessions/revoke-others: sign out everywhere except the session making the request.

type SessionsStore added in v0.38.12

type SessionsStore interface {
	List(ctx context.Context, userID uuid.UUID) ([]sessions.Session, error)
	RevokeOwned(ctx context.Context, userID, id uuid.UUID) (bool, error)
	RevokeOthers(ctx context.Context, userID, keepID uuid.UUID) (int64, error)
}

SessionsStore is the part of sessions.Store the device list uses.

type TripRecap added in v0.40.9

type TripRecap struct {
	// contains filtered or unexported fields
}

TripRecap sends the "You drove a saved trip" notification (Settings, Trip recap; off until the user turns it on). After a drive closes it compares the user's saved trips with what was driven (the plan-vs-actual matcher) and, for an outing that reached the plan's destination in the last few hours, sends one push with the planned and actual arrival charge and cost. It goes through engage: the account's toggle, quiet hours (09:00 to 20:00 local), the 4-per-30-days budget, and one claim per (saved trip, day, outing), so each outing is announced at most once across replicas and restarts. A recap held back by quiet hours or the budget is tried again only if a later drive closes within the freshness window. The trip's name never goes in the push; the tap opens its day-by-day.

func NewTripRecap added in v0.40.9

func NewTripRecap(pool *sql.DB, tf *trips.Factory, sf *settings.Factory, e *engage.Engage,
	deliver func(ctx context.Context, uid uuid.UUID, p push.Payload) error, log *slog.Logger) *TripRecap

NewTripRecap wires the notifier over the system pool. deliver sends one payload to the user's devices.

func (*TripRecap) OnDriveClose added in v0.40.9

func (n *TripRecap) OnDriveClose(ctx context.Context, uid uuid.UUID)

OnDriveClose is the drive-close hook. Errors are logged, never returned: this is best effort.

Source Files

Jump to

Keyboard shortcuts

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