Documentation
¶
Overview ¶
Package api wires the HTTP surface for Rivolt. It assembles routes, middleware, and handler dependencies into a single chi Mux.
Index ¶
- Constants
- func MilestoneLease(pool *sql.DB) func(ctx context.Context, uid, vehicle uuid.UUID) milestones.LeaseTerm
- func MilestoneLedgerInput(pool *sql.DB, sf *settings.Factory) ...
- func New(d Deps) http.Handler
- func RunIdPDeleteRetrier(ctx context.Context, d *sql.DB, ac idp.UserProvider, log *slog.Logger, ...)
- func ValidAppID(id string) bool
- type AppAuth
- type ArchiveRestorer
- type Deps
- type InactivePurge
- type PurgeResult
- type SessionsAPI
- type SessionsStore
Constants ¶
const ( // AppSessionTTL is an app session's lifetime (sliding; sessions.AppTTL). AppSessionTTL = sessions.AppTTL )
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.
Variables ¶
This section is empty.
Functions ¶
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 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 ValidAppID ¶ added in v0.38.11
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 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
// 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
// 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
// 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 *sql.DB
// 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
// 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
// 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
// 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
}
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 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.
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 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)
}
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.
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.
Source Files
¶
- aasa.go
- account_delete.go
- activity.go
- admin.go
- admin_archive.go
- admin_audit.go
- admin_disabled.go
- admin_health.go
- admin_notify.go
- admin_only.go
- ai_budget_mw.go
- ai_consent.go
- ambient_temp.go
- api.go
- app_write_allowlist.go
- appauth.go
- auth_mw.go
- bodylimit.go
- changelog.go
- charge_overrides.go
- charge_split.go
- chargers_along.go
- charges.go
- charges_to_confirm.go
- chart_points.go
- cold_range.go
- data.go
- demo.go
- demo_cache.go
- demo_clientip.go
- demo_trips.go
- drain.go
- drive_efficiency.go
- drive_trailer.go
- drive_weather.go
- drives.go
- engagement_settings.go
- events.go
- export.go
- far_future.go
- flags.go
- funnel.go
- health.go
- hydra.go
- inactive_purge.go
- insights.go
- iplimit.go
- lease.go
- ledger.go
- ledger_csv.go
- ledger_place.go
- ledger_savings.go
- ledger_trips.go
- live.go
- maps_limit.go
- membership_terms.go
- milestones.go
- now.go
- onboarding_email.go
- ota_notes.go
- overnight_candidates.go
- plan_legs.go
- plan_validate.go
- planner.go
- push.go
- push_apns.go
- receipts_optin.go
- rivian.go
- rivian_actions.go
- sampletrips.go
- seo.go
- sessions_api.go
- settings_ai.go
- settings_charging.go
- settings_flatrate.go
- settings_offpeak.go
- signup.go
- signup_requests.go
- spa.go
- statement_email.go
- terms.go
- trip_multiday.go
- trip_plan_actual.go
- trip_resolve.go
- trip_saved_active.go
- trip_saved_multiday.go
- tripplan.go
- ttlcache.go
- user_extras.go
- vehicle_filter.go
- vehicle_mw.go
- vehicle_position.go
- vehicle_trend.go
- vehicles.go
- weekly_recap.go