v1.1.0: session guard on all game routes + register + sign-in screen (ISS-107)
CI / build + unit + e2e (pull_request) Failing after 26m20s
CI / build + unit + e2e (pull_request) Failing after 26m20s
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## v1.1.0 — 2026-09-26 14:39
|
||||
- **Multiplayer step 3/12 (ISS-107): every game route is session-guarded; new accounts can register.** `lib/auth.ts` grows a shared guard on top of the v0.71.0 session plumbing: `requireAuth(req)` (X-Flight-Session header) and `requireAuthSse(req)` (header OR `?session=` query — EventSource cannot set headers), both returning either `{ok, inst, user, player}` or a ready-made 401/500 response. The guard joins `players` on the account's username, so the seeded admin resolves to the v1.0.0 local player row; an account with no players row is a data error → 500. Coverage: `/api/state`, all eight action routes (one `performAction(req, fn)` call site in `lib/game/actions.ts` guards them all), and the SSE stream (`/api/events`, validated at connect — ponytail comment names the mid-stream ceiling). Exempt by design: `/api/version`, `/api/openapi`, `/api/airports`, `/api/login`, `/api/register`, `/api/logout`, `/api/me`, the admin CRUD (own guard), and every `/api/test/*` hook.
|
||||
- **POST /api/register.** Strict body validation (exactly `username` + `password`; unknown fields 400); username lowercased, then 3–20 chars of `[a-z0-9_-]` else 400; password ≥8 chars else 400; 409 when taken. Creates the users row (scrypt via the same `hashPassword()` login verifies) AND a players row with the starter economy (`STARTING_CASH`/`STARTING_REPUTATION`) — one randomUUID shared by both rows — then mints a session immediately and returns `{sessionId, user}` like `/api/login`, so sign-up needs no second round-trip.
|
||||
- **Sign-in screen replaces the shell until authenticated.** New `components/SignInScreen.tsx`: full-screen dark panel with two forms (sign in / create account) sharing one submit path; errors surface in a single `role=alert` line. `app/page.tsx` gates on it: stored session → `/api/me` check on mount (stale id cleared, network error falls through to sign-in), then the game shell mounts only when authenticated — so an unauthenticated browser never polls `/api/state`. New client module `lib/authClient.ts` owns the localStorage key (`flight.session`) and builds the `X-Flight-Session` header; every app fetch (state poll, SSE URL, action POSTs, fly/queue in Map) now carries it. Admin panel logout drops back to the sign-in screen via a new `onLogout` prop (page → Map → AdminPanel).
|
||||
- **E2E session hook + spec updates.** New test-only `POST /api/test/mint-player-session` (FLIGHT_TEST_HOOKS=1, 404 elsewhere — like its `/api/test/*` siblings): creates the users+players rows for a username if missing and mints a session, idempotent across container restarts since the CI DB volume persists. `e2e/auth.ts` mints + applies it per test (context extraHTTPHeaders for spec-side requests, localStorage init script for the app's own fetches); every guarded-route spec authenticates in a beforeEach; events.spec uses the real client path (`?session=`); api.spec/openapi tests track the new eighteenth endpoint. Admin spec reworked: unauthenticated visitors see the sign-in screen (no shell), wrong credentials error there, and logout returns to it.
|
||||
- **Tests.** Unit: new `lib/auth.test.ts` (hash round-trip; 401 on missing/unknown/expired session; admin→local player join; 500 on missing players row), register route tests (validation matrix, shared-id rows + starter economy + minted session, case-insensitive 409) and mint-hook tests (gate off → 404 even with CI's FLIGHT_TEST_HOOKS=1 env — stubbed explicitly, default-username creation, idempotent re-mint, bad usernames). All existing state/events/action route tests now mint a session for the seeded admin user in beforeAll. OpenAPI: `/api/register` documented (RegisterRequest schema; 200/400/409) — spec is now eighteen paths.
|
||||
|
||||
## v1.0.0 — 2026-09-24 14:20
|
||||
- **Multiplayer step 1/12 (ISS-105): `players` table replaces the singleton `account`.** New `players` table (`id TEXT PK`, `provider`, `username`, `cash REAL`, `reputation REAL`) in the raw DDL (`lib/db/client.ts`) and a matching Drizzle entry (`lib/db/schema.ts`; `AccountRow` → `PlayerRow`). **One-time migration**: on every `open()`, if the legacy `account` table still exists (sqlite_master check, same guard style as the v0.12.0 contracts migration) AND `players` is empty, its single row is copied into a seeded local player — id `local`, provider `local`, username `kamilkrzywda` (so a later step can join players↔users on username) with cash/reputation carried over — and `account` is dropped. Idempotent: fresh DBs never have `account`, already-migrated files no longer do either, so it runs exactly once per legacy file. **Fresh worlds**: the DDL no longer creates `account`; first-run seeding (empty-check flipped from `COUNT(*) FROM account` to `players`) inserts the local player row directly with the same starting cash/reputation as before.
|
||||
- **No behavior change — temporary single-player helper.** Every pre-multiplayer read/write of the singleton economy now goes through ONE small module, `lib/db/localPlayer.ts`: `getLocalPlayer(inst)` (resolves the `players` row where id = 'local', throws if missing) and `updateLocalPlayerEconomy(inst, {cash?, reputation?})`. Call sites converted: snapshot (`lib/game/state.ts`), all five actions (`flyPlane` refuel, abandon penalty, buy/sell plane, repair — `lib/game/actions.ts`), the engine's delivery awards + chain top-up + spawn reputation read (`lib/sim/engine.ts`), and the three test hooks (add-cash / set-cash / create-package). The `/api/state` snapshot keeps its exact `account: {cash, reputation}` field name and values, so OpenAPI is untouched — 17 paths, same schemas.
|
||||
|
||||
Reference in New Issue
Block a user