A new Admin button on the right of the top bar opens a reusable modal (components/Modal.tsx — z-index 60 overlay above the z-50 toasts; role="dialog", closes on Escape/backdrop) hosting components/AdminPanel.tsx. No new URL routes.
Changes
users + sessions tables (raw DDL in lib/db/client.ts + typed Drizzle entries in lib/db/schema.ts). Idempotent seeded admin inserted on every open() — user id admin-kamilkrzywda, username kamilkrzywda, admin=1, hash-only (WHERE NOT EXISTS guard; safe on the existing production DB). The users table is also the foundation for multiplayer auth (#105/#107).
Auth API — POST /api/login (scrypt + timingSafeEqual; unknown usernames run one full scrypt against a fixed dummy digest so timing stays uniform), POST /api/logout (idempotent), GET /api/me. Sessions are 7-day UUID rows sent as the X-Flight-Session header; the client stores the id in localStorage (flight.session).
Generic whole-database CRUD behind the admin flag — lib/auth.ts guard (401 no/expired session, 403 non-admin) runs before any input is touched on GET /api/admin/tables and GET|PUT|POST|DELETE /api/admin/tables/{name}/rows. Rows carry their rowid as __rowid so the PK-less tables (account, sim_state) are editable. SQL safety: table names quoted only after sqlite_master validation, columns validated against PRAGMA table_info, values always bound as parameters.
UI — login form / "not an admin" notice / grid (table list → editable rows with per-row Save/Delete, "+ Add row", Refresh; logout in the header area). Deliberately plain dev-tool surface.
Test hook — POST /api/test/admin-session (FLIGHT_TEST_HOOKS=1 gate) mints a session for the seeded admin server-side so e2e never needs the real password.
OpenAPI — five new paths + schemas; endpoint count twelve → seventeen in every assertion (lib/openapi.test.ts, e2e/api.spec.ts).
Tests
Unit: idempotent admin seed (fresh DB / reopen / legacy pre-v0.71.0 file), login success/wrong-password/unknown-username with an in-test scrypt hash for a self-created user, logout idempotency, /api/me matrix, guard ladder (401/401/403), full CRUD validation matrix (rows incl. __rowid, PUT verified by re-fetch, POST+DELETE round-trip, 404 unknown table, 400 unknown column/bad limit).
E2E (e2e/admin.spec.ts, CI-only): modal → login form → wrong-creds error → hook-minted session via localStorage → table list incl. packages/users → edit a cell + Save verified by an independent re-fetch → add + delete a users row → logout back to the login form.
Local verification: tsc --noEmit clean, vitest run 410/410 (38 files), playwright test --list 64 tests in 17 files. Full e2e runs in CI only per AGENTS.md.
## What
A new **Admin** button on the right of the top bar opens a reusable modal (`components/Modal.tsx` — z-index 60 overlay above the z-50 toasts; `role="dialog"`, closes on Escape/backdrop) hosting `components/AdminPanel.tsx`. No new URL routes.
## Changes
- **users + sessions tables** (raw DDL in `lib/db/client.ts` + typed Drizzle entries in `lib/db/schema.ts`). Idempotent seeded admin inserted on every `open()` — user id `admin-kamilkrzywda`, username `kamilkrzywda`, admin=1, hash-only (`WHERE NOT EXISTS` guard; safe on the existing production DB). The users table is also the foundation for multiplayer auth (#105/#107).
- **Auth API** — `POST /api/login` (scrypt + timingSafeEqual; unknown usernames run one full scrypt against a fixed dummy digest so timing stays uniform), `POST /api/logout` (idempotent), `GET /api/me`. Sessions are 7-day UUID rows sent as the `X-Flight-Session` header; the client stores the id in localStorage (`flight.session`).
- **Generic whole-database CRUD behind the admin flag** — `lib/auth.ts` guard (401 no/expired session, 403 non-admin) runs before any input is touched on `GET /api/admin/tables` and `GET|PUT|POST|DELETE /api/admin/tables/{name}/rows`. Rows carry their rowid as `__rowid` so the PK-less tables (account, sim_state) are editable. SQL safety: table names quoted only after sqlite_master validation, columns validated against PRAGMA table_info, values always bound as parameters.
- **UI** — login form / "not an admin" notice / grid (table list → editable rows with per-row Save/Delete, "+ Add row", Refresh; logout in the header area). Deliberately plain dev-tool surface.
- **Test hook** — `POST /api/test/admin-session` (FLIGHT_TEST_HOOKS=1 gate) mints a session for the seeded admin server-side so e2e never needs the real password.
- **OpenAPI** — five new paths + schemas; endpoint count twelve → seventeen in every assertion (`lib/openapi.test.ts`, `e2e/api.spec.ts`).
## Tests
- Unit: idempotent admin seed (fresh DB / reopen / legacy pre-v0.71.0 file), login success/wrong-password/unknown-username with an in-test scrypt hash for a self-created user, logout idempotency, /api/me matrix, guard ladder (401/401/403), full CRUD validation matrix (rows incl. `__rowid`, PUT verified by re-fetch, POST+DELETE round-trip, 404 unknown table, 400 unknown column/bad limit).
- E2E (`e2e/admin.spec.ts`, CI-only): modal → login form → wrong-creds error → hook-minted session via localStorage → table list incl. packages/users → edit a cell + Save verified by an independent re-fetch → add + delete a users row → logout back to the login form.
Local verification: `tsc --noEmit` clean, `vitest run` 410/410 (38 files), `playwright test --list` 64 tests in 17 files. Full e2e runs in CI only per AGENTS.md.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What
A new Admin button on the right of the top bar opens a reusable modal (
components/Modal.tsx— z-index 60 overlay above the z-50 toasts;role="dialog", closes on Escape/backdrop) hostingcomponents/AdminPanel.tsx. No new URL routes.Changes
lib/db/client.ts+ typed Drizzle entries inlib/db/schema.ts). Idempotent seeded admin inserted on everyopen()— user idadmin-kamilkrzywda, usernamekamilkrzywda, admin=1, hash-only (WHERE NOT EXISTSguard; safe on the existing production DB). The users table is also the foundation for multiplayer auth (#105/#107).POST /api/login(scrypt + timingSafeEqual; unknown usernames run one full scrypt against a fixed dummy digest so timing stays uniform),POST /api/logout(idempotent),GET /api/me. Sessions are 7-day UUID rows sent as theX-Flight-Sessionheader; the client stores the id in localStorage (flight.session).lib/auth.tsguard (401 no/expired session, 403 non-admin) runs before any input is touched onGET /api/admin/tablesandGET|PUT|POST|DELETE /api/admin/tables/{name}/rows. Rows carry their rowid as__rowidso the PK-less tables (account, sim_state) are editable. SQL safety: table names quoted only after sqlite_master validation, columns validated against PRAGMA table_info, values always bound as parameters.POST /api/test/admin-session(FLIGHT_TEST_HOOKS=1 gate) mints a session for the seeded admin server-side so e2e never needs the real password.lib/openapi.test.ts,e2e/api.spec.ts).Tests
__rowid, PUT verified by re-fetch, POST+DELETE round-trip, 404 unknown table, 400 unknown column/bad limit).e2e/admin.spec.ts, CI-only): modal → login form → wrong-creds error → hook-minted session via localStorage → table list incl. packages/users → edit a cell + Save verified by an independent re-fetch → add + delete a users row → logout back to the login form.Local verification:
tsc --noEmitclean,vitest run410/410 (38 files),playwright test --list64 tests in 17 files. Full e2e runs in CI only per AGENTS.md.