Pivot from the hardware/integrity layer to the parking operation. All wiki-only; no code yet. Core principle throughout: business entities are projections over the signed append-only event log, never mutable tables. New concepts: parking-session, tariff (composable/versioned, FX-ready), shift (manned-only Z-report), capacity-occupancy, validation-discounts, reporting-analytics, clock-integrity, ticket-encoding, anti-passback. New entities: permit, opencv-anpr-service, blocklist. Decisions: session-model, vision-service (host-side ANPR + vehicle verification; scoped AGPL exception for the isolated service). Updates: append-only-event-chain (new event types + vision witness), local-jwt-auth (drop 8h expiry -> until logout; code change pending), lpr-camera (host-side recognition supersedes edge-AI), standing-decisions (AGPL exception), open-questions (+FX, +pay-station money corners, backup). Deferred + flagged: intercom/help-call, receipts/refunds/change, FX engine, lane topology (#1).
2.8 KiB
type, tags, sources, updated
| type | tags | sources | updated | |||||
|---|---|---|---|---|---|---|---|---|
| entity |
|
|
2026-06-15 |
Local JWT Auth
Authentication and authorization, kept fully local — a direct consequence of offline-first (an air-gapped park cannot reach an external identity provider; see logto-zitadel-oidc for the rejected alternative). (See parking-system-architecture §2.)
@fastify/jwtsigns tokens with a local secret (symmetric HMAC). The server refuses to start without a strongJWT_SECRET(≥32 chars, no placeholder) — there is deliberately no insecure default.- Session lifetime: valid until explicit logout — no time expiry (decision 2026-06-15).
Booth reality breaks any fixed clock: relief arrives late, fails to show, or one operator is
forced to work two shifts in a row — a token that expired mid-duty would strand an active
operator. So the login persists until logout; a shift is a separate, explicit boundary,
not tied to token lifetime. (Superseded the earlier "8h expiry, bound to a shift" assumption.)
⚠️ Code still mints an 8h-expiry token — this page records the decided design; the server change (drop
expiresIn, persist until logout) is pending. - A
userstable in sqlite holds bcrypt password hashes plus a role column. The first admin is seeded viapnpm --filter @parking/server seed-admin(no bootstrap endpoint). - Authorization = a simple
preHandlerrole guard per route: admin / operator / cashier / readonly. No Casbin or full RBAC engine needed at this scale.
Cookie session (browser auth)
The SPA never sees the JWT. Login (POST /api/auth/login) verifies bcrypt and sets two cookies:
parking_token— the JWT, HttpOnly + SameSite=Strict (+SecurewhenNODE_ENV=production). JS can't read it;@fastify/jwtreads it from the cookie, not theAuthorizationheader.parking_csrf— a random token, readable by JS. The JWT also carries a matchingcsrfclaim. On every mutation the SPA echoes the cookie in theX-CSRF-Tokenheader; the guard requires header == cookie == the signed claim (double-submit CSRF). Safe reads are exempt.
Routes: login, logout (clears cookies), me (bootstraps SPA session on load). The dev
react-vite-spa proxy and the prod nginx reverse proxy keep the SPA and API
same-origin, so the cookies work without CORS. (This replaced an earlier dev-only
SETUP_AUTH_BYPASS shim, now removed.)
Open decision: moving from the symmetric secret to an asymmetric key (RS256/EdDSA) so verifying hosts hold only a public key — open-questions #7. Relevant before any multi-host/multi-lane deployment.
Part of the technology-stack. License: MIT.