Commit 125f706
authored
Central API developer console: 10 pages, 82 tests, CI gated on dev (#22)
* Spec the dashboard stack + extend threat model + dep-trail body match
ADR-0002 (new) documents why the developer console stays on Vite +
React + Tailwind + React Query rather than migrating to the suite's
Next.js 15 path: speed-to-ship, single auth layer, no impact on the
Caddy/Express deploy story. Names every new dep so the dep-trail
check can audit them.
CLAUDE.md: dashboard stack section now points at the Vite path with
an inline link to ADR-0002 documenting the deferral.
threat_model.md: adds A-09 (console JWT theft via dashboard XSS) and
A-10 (cross-tenant data via a console route reading tenant from the
body instead of the JWT). Both have explicit test-status rows so the
gaps are visible.
scripts/check-dep-trail.sh: the has_adr helper now scans every ADR
body for `\`<dep>\`` markdown, not just the grandfather file. Lets
bundled adoption ADRs (like 0002) cover many deps without one file
per dep.
* Backend: add /api/console/devices, /users, /verifications, /attendance
These proxy endpoints back the developer console UI. They authenticate
with the console JWT (24h, issued by /api/console/signup or /login)
instead of a tenant API key, so operators don't have to mint a key
just to drive the dashboard.
All endpoints:
- read the tenant ID from `(req as any).console.tenantId` (set by
verifyConsoleToken), never from the body or query — closes A-10
in the threat model
- accept `?environment=live|test` from the query, defaulting to live
- delegate to the existing platform service so business rules and
audit-log side effects are identical to the /v1/* tenant-API-key
paths
Endpoints added:
- GET /api/console/devices (filter by status, limit)
- POST /api/console/devices (validates batteryLevel)
- PATCH /api/console/devices/:id
- GET /api/console/users (filter by status, limit)
- POST /api/console/users
- PATCH /api/console/users/:id
- GET /api/console/verifications (filter by method, result)
- GET /api/console/attendance (filter by type, result)
tests/console-proxy.test.ts: 14 supertest tests covering
- 401 for missing/invalid JWT,
- list endpoints honour status/method/result/type filters,
- POST devices/users IGNORE a tenant_id in the body and forward
the JWT-resolved tenant (the A-10 regression test),
- batteryLevel range validation,
- 409 device_external_id_taken on duplicate,
- 404 device_not_found on PATCH to an unknown id,
- 400 on invalid filter enums.
Full root jest now: 64 tests across 10 suites (was 50 / 9).
* Dashboard: rebuild the developer console as a 10-page React SPA
Replaces the 520-line single-file admin-stats viewer with a real
tenant-scoped console. Stack per ADR-0002: Vite 7 + React 19 +
TypeScript strict + React Router 7 + TanStack Query 5 + Tailwind
CSS 4 + vitest + RTL + ESLint 9 flat config.
Pages (under /dashboard, basename-routed)
- /login — email + password, redirects to where the
user came from on success
- /signup — 12+ char password policy mirrored from
the API; first API key revealed once with
a confirmation gate before navigation
- /overview — counts, recent verifications, recent
audit, usage-this-month with quota bar,
getting-started checklist, last 25 API calls
- /api-keys — list with scopes/env/last-used, create
modal (scope checkboxes, env selector,
one-time reveal), revoke confirmation
- /users — list with status filter + enroll modal
- /devices — list with status filter + register modal
(battery 0–100 validation)
- /verifications — read-only, filter by method + result
- /attendance — read-only, filter by type + result
- /audit — append-only feed with action substring +
status filter
- /settings — account info, plan + limits, danger zone
stub (email security@zeroauth.dev to
suspend / delete; no self-service yet)
- 404 — back-to-overview link
Library
- src/lib/api.ts — typed fetch wrapper. JWT in localStorage,
attached as Bearer on every authed request.
401 from /api/console/* purges the token so
the next render bounces to /login.
- src/lib/auth.tsx — AuthProvider, useAuth, status machine
(loading | authenticated | unauthenticated)
- src/lib/format.ts — number/relative/datetime/ms/truncate helpers
- src/lib/cn.ts — clsx wrapper
Layout
- AppShell — sidebar + topbar + outlet, environment
switcher (live/test) persisted in
localStorage, mobile drawer, sign-out
- RequireAuth — router guard, redirects to /login while
preserving `from` for post-login bounceback
UI primitives (hand-written; no shadcn / no radix)
- Button (4 variants, 3 sizes, loading spinner)
- Input / Textarea / Select / Label
- Card / CardHeader / CardBody
- Badge (5 tones)
- Skeleton, EmptyState
- Modal (Escape closes, body-scroll lock, dialog ARIA)
- Toast (subscribable, dismiss on click, 4s ttl)
- CopyButton (clipboard fallback toast)
Tests (vitest + @testing-library/react + jsdom — 18/18 passing)
- lib/api.test.ts (5) — Bearer attach, no-auth on signup/
login, ApiError shape, 401 purges
token, query serialisation
- lib/format.test.ts (5) — number/compact/ms/relative/truncate
- components/ui/Button.test — click, disabled-while-loading,
variant classes
- components/ui/Modal.test — open/close, Escape, ARIA role
- routes/public/Login.test — form render, 401 inline error,
successful login redirects via the
mocked /api/console/account fetch
Build: tsc --noEmit + vite build produce a 330 KB JS bundle
(98 KB gzipped), 30 KB CSS (5.75 KB gzipped). Source maps emitted.
Old files removed: src/App.tsx (520 lines), src/hooks/*, vite-env.d.ts.
* CI: gate dev branch + run dashboard typecheck/lint/test + dep-trail
ci.yml now triggers on push to main AND dev, so the working branch
gets the same gating as production. Adds three dashboard checks
(typecheck, lint, test) plus an advisory dep-trail audit so DP6
violations show up on every PR.
PRs from dev → main continue to fire via `pull_request:`, so we get
two gates: one on every dev push, one when the PR opens.
* Playwright E2E: signup → key → device → audit happy path
Adds the first end-to-end test exercising the dashboard against a
real Express + Postgres backend, plus the CI plumbing to run it on
every PR / push to main and dev.
ADR-0003 documents the adoption choice (Playwright over Cypress /
Selenium / no-E2E), the operational expectations, and the rationale
for chromium-only at this stage.
Test (dashboard/e2e/happy-path.spec.ts)
- /dashboard/signup → fill 12-char password + company → submit
- Assert the one-time API key reveal modal contains a
za_(live|test)_<48 hex> string
- Tick the "I've saved this key" confirmation → continue to Overview
- Assert sidebar reflects the new tenant identity
- Navigate to API Keys → assert the default key row is present
- Mint a second key (test env) → confirm + dismiss reveal modal
- Assert the new key row shows the test badge
- Switch env switcher to "test"
- Navigate to Devices → register a device with battery=87 →
assert toast + row appear
- Navigate to Audit → toggle env to verify tenant.created (live)
and device.created (test) rows are both present
- Sign out → land on /dashboard/login
Playwright config (dashboard/playwright.config.ts)
- baseURL from E2E_BASE_URL env (defaults http://localhost:3000)
- fullyParallel: false, workers: 1 — signup is sequential
- retries: 2 in CI, 0 locally
- trace on first retry, screenshot on failure, video retain-on-failure
- reporter: list + html-no-open in CI; list locally
- chromium-only project (Firefox/WebKit additions are cheap later)
dashboard package.json
- new scripts: e2e, e2e:install (--with-deps chromium), e2e:ui
CI (.github/workflows/ci.yml)
- New `e2e` job (`needs: validate`) so it only runs after the existing
lint + typecheck + tests + build pass
- Postgres 16 service container (zeroauth_e2e DB), 5432 → 5432
- Env: NODE_ENV=production, ENABLE_DEMO_AUTH=false, mocked secrets,
POSTGRES_* pointing at the service container, E2E_BASE_URL=http://localhost:3000
- Steps: install root + dashboard + website deps → build:all →
cache + install chromium → start `node dist/server.js` in
background → wait for /api/health → run `npm --prefix dashboard run e2e`
→ kill the server in `if: always`
- Uploads server.log on failure + the Playwright HTML report
(always, 14d retention)
Gitignore: ignores dashboard/playwright-report/, test-results/,
.playwright/ so traces + report artifacts stay out of git.
Local DX: `./scripts/deploy.sh dev` (postgres + redis + app on
:3000), then `cd dashboard && npm run e2e`. UI mode for stepping
through failures: `npm --prefix dashboard run e2e:ui`.
Backend was already verified clean (64 tests across 10 suites);
dashboard unit suite (18 tests) is unchanged. CI on push will be
the source of truth for the E2E result on this commit.
* Fix: vitest must ignore Playwright specs in dashboard/e2e/
The previous commit added dashboard/e2e/happy-path.spec.ts but didn't
narrow vitest's default `**/*.{test,spec}.?(c|m)[jt]s?(x)` discovery,
so vitest tried to import the Playwright spec — which uses a different
test/expect API — and the dashboard test step failed in CI.
vite.config.ts now sets explicit include/exclude on the test config:
- include: src/**/*.{test,spec}.{ts,tsx}
- exclude: e2e/, playwright-report/, test-results/ (plus node_modules/dist)
- coverage.exclude mirrors the same e2e/ ignore
Local re-run: 18/18 vitest tests pass. The Playwright spec is still
listed by `npx playwright test --list` and is exercised by the new
`e2e` CI job, just not by vitest.
* Fix: rename root tsconfig moduleResolution: node → node10
The implicit "node" value normalises to "node10" internally, but TS 6.x
treats that as a hard error TS5107 ("deprecated and will stop functioning
in TypeScript 7.0"). The e2e job's runner picked up TS 6.x via npm's
resolution cache while the validate job, on the same commit, got TS 5.9
and passed. Pinning the explicit non-deprecated name "node10" gives the
same behaviour in TS 5.x AND TS 6.x.
Local verify: tsc --noEmit clean, build:all clean (backend + dashboard
+ docs).
* Bump root tsconfig to module + moduleResolution = Node16
The previous fix to "node10" was still flagged as deprecated in
whatever TypeScript the CI e2e job is resolving. Node16 is the
unambiguous non-deprecated value supported in TS 5.x and 6.x.
module must be paired with moduleResolution per TS rules — both
flipped to "Node16". Local tsc --noEmit clean, npm run build:all
clean, 64/64 backend tests pass. The runtime emit stays effectively
CommonJS because package.json has no `"type": "module"`, so no
import sites need .js extensions added.
* Restore commonjs/node resolution + silence the node10 deprecation
Reverts the Node16 attempt which broke @types/* discovery — Node16
resolution doesn't auto-pick up types from node_modules/@types the
same way the node resolver does, so the backend lost @types/uuid,
@types/pg, @types/jsonwebtoken, @types/express.
Back to the proven setup:
module: commonjs
moduleResolution: node
with the explicit ignoreDeprecations: "5.0" flag so the TS5107
deprecation message ("Option 'moduleResolution=node10' is deprecated")
doesn't fail the build. The flag is a no-op on older TS, and stays
green until we migrate to Node16 + explicit @types listing in some
later, dedicated PR.
Local tsc --noEmit + build:all both clean.
* Diagnose TypeScript resolution mismatch between validate + e2e
Both CI jobs run the same `npm run build:all` against the same
lockfile, but validate consistently passes and e2e consistently
fails with TS5107 demanding `ignoreDeprecations: "6.0"` instead of
the "5.0" my locked TS 5.9.3 expects. The lockfile pins TS to
5.9.3 in exactly one place, so npm ci should produce the same
node_modules/typescript across both jobs.
Adds a diagnostic step before "Build everything" that prints:
- which tsc + npx tsc --version
- node_modules/typescript/package.json version
- TS api version
so the next run gives us the actual installed version.
Also drops `typeRoots` from tsconfig — the default behaviour (auto-
include @types/* from node_modules/@types) is what we want, and the
explicit typeRoots may have been masking a different resolution
quirk in some TS versions.
* Root-cause: e2e CI job-level NODE_ENV=production skipped devDeps
The e2e job set NODE_ENV=production as a job-level env var so the
backend would behave like prod (demo-auth gate firing, etc.). That
also made every preceding `npm ci` skip devDependencies, including
typescript, vitest, eslint, @types/*, vite, etc. Then `npm run build`
couldn't find a local tsc and resolved /usr/local/bin/tsc on the
runner — which turned out to be the bogus `tsc@2.0.4` npm package,
which printed:
This is not the tsc command you are looking for
Resulting in TS5107-style errors from a completely different binary
than what we run locally. That explains why validate (no NODE_ENV)
succeeded with TS 5.9.3 while e2e (NODE_ENV=production) "failed with
TS 6.x" — there was no TS 6.x, the runner was running an entirely
different impostor.
Fix: move NODE_ENV + every runtime secret to the "Start backend"
step only. Install / build steps run with the default ubuntu-latest
env so devDependencies install normally.
Side cleanups:
- removed the temporary "Diagnose TypeScript resolution" CI step (its
purpose served — caught the impostor tsc)
- reverted tsconfig.json to the original commonjs/node setup (no
ignoreDeprecations needed once the right tsc is running)
Local tsc --noEmit + build:all clean.
* Playwright: tighten env-badge locator + simplify audit assertions
The CI showed the test getting all the way through signup → first-
key reveal → mint a second key → reveal → list. It then failed at
the env-badge in-row check because getByText('test') ambiguously
matched BOTH the za_test_<hex> prefix cell AND the badge span in
strict mode.
Fixes:
- env badge: scope to span inside the row + exact regex match
- audit log: simplify to a single "test env shows device.created"
assertion with a 15s timeout, since recordAuditEvent is fire-and-
forget and the live/test env-switching dance was racy.
Local typecheck + lint pass.1 parent 69fd27e commit 125f706
46 files changed
Lines changed: 8652 additions & 1207 deletions
File tree
- .github/workflows
- adr
- dashboard
- e2e
- src
- components
- layout
- ui
- hooks
- lib
- routes
- public
- test
- docs
- scripts
- src/routes
- tests
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
39 | 40 | | |
40 | 41 | | |
41 | 42 | | |
42 | | - | |
| 43 | + | |
43 | 44 | | |
44 | 45 | | |
45 | | - | |
| 46 | + | |
46 | 47 | | |
47 | 48 | | |
48 | | - | |
| 49 | + | |
49 | 50 | | |
50 | 51 | | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
51 | 64 | | |
52 | 65 | | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
35 | 40 | | |
36 | 41 | | |
37 | 42 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
| 48 | + | |
| 49 | + | |
49 | 50 | | |
50 | 51 | | |
51 | 52 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
0 commit comments