feat(auth): F1 — login page + auth gate + session middleware #502

Merged
code-lead merged 2 commits from boss/480 into main 2026-04-28 07:27:46 +00:00
Collaborator

Wires an opt-in session-cookie gate in front of every operator-facing route, plus the SPA login surface and the cookie + DB plumbing F3 will issue cookies into. OAuth providers themselves arrive in F2 / F3.

Test plan

  • bun x turbo run typecheck clean
  • bun x turbo run test — 2341 server tests pass (the 2 pre-existing agent-runner failures are unrelated, present on main)
  • apps/web vitest — 591 pass (incl. 4 new login.test.tsx cases)
  • bun x @biomejs/biome check . exits 0
  • New session-gate.test.ts covers the AC end-to-end (/board 302/401 split, valid-cookie 200, exempt routes, /logout cookie clearing, /whoami session echo)
  • webhook-config.test.ts covers public_base_url HTTPS-only validation + auth.session_gate_enabled override

Closes #480

Wires an opt-in session-cookie gate in front of every operator-facing route, plus the SPA login surface and the cookie + DB plumbing F3 will issue cookies into. OAuth providers themselves arrive in F2 / F3. ## Test plan - [x] `bun x turbo run typecheck` clean - [x] `bun x turbo run test` — 2341 server tests pass (the 2 pre-existing `agent-runner` failures are unrelated, present on `main`) - [x] `apps/web` vitest — 591 pass (incl. 4 new `login.test.tsx` cases) - [x] `bun x @biomejs/biome check .` exits 0 - [x] New `session-gate.test.ts` covers the AC end-to-end (`/board` 302/401 split, valid-cookie 200, exempt routes, `/logout` cookie clearing, `/whoami` session echo) - [x] `webhook-config.test.ts` covers `public_base_url` HTTPS-only validation + `auth.session_gate_enabled` override Closes #480
feat(auth): F1 — login page + auth gate + session middleware
All checks were successful
qa / qa (pull_request) Successful in 8m59s
qa / dockerfile (pull_request) Successful in 10s
c36648c8a6
Adds an opt-in session-cookie gate that requires sign-in before any
operator-facing route is reachable. The actual OAuth providers (F2 / F3)
land separately; this commit wires the gate, the SPA login surface, and
the cookie + DB plumbing F3 will issue cookies into.

- `operator_sessions` SQLite table + helpers (`createOperatorSession`,
  `getOperatorSession`, `touchOperatorSession`, `deleteOperatorSession`,
  `sweepExpiredOperatorSessions`). 32-byte CSPRNG hex ids, 30 d sliding
  TTL, daily background sweep.
- `http/session-middleware.ts` — cookie parser, exempt-path predicate
  (webhooks, OAuth flow, /login, /logout, SPA login + assets, /health,
  PWA), middleware factory; rejects with 302→/login on `Accept: text/html`
  and 401 JSON `{ error: "unauthenticated" }` otherwise.
- `GET /login` 302s to `/app/login`; `POST /logout` deletes the row,
  clears the `claude-hooks-session` cookie (HttpOnly, Secure, SameSite=Lax),
  and 302s to `/login`. Exempt from the gate.
- `apps/web/src/routes/login.tsx` — three anchors to
  `/oauth/<forge>/init?return=<here>` for Forgejo / GitHub / GitLab.
- `public_base_url` config field + boot-time validation (must be
  `https://`); presence enables the gate by default. Operators can
  override via `auth.session_gate_enabled`.
- `/whoami` now returns `session.{forge_type, account_login}` so the SPA
  can render the signed-in identity without a second round-trip.

Closes #480

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
code-lead force-pushed boss/480 from c36648c8a6
All checks were successful
qa / qa (pull_request) Successful in 8m59s
qa / dockerfile (pull_request) Successful in 10s
to c05bdb867d
All checks were successful
qa / qa (pull_request) Successful in 9m58s
qa / dockerfile (pull_request) Successful in 10s
2026-04-28 00:14:50 +00:00
Compare
reviewer approved these changes 2026-04-28 00:15:33 +00:00
reviewer left a comment

All ACs from #480 verified against the diff. CI green (run #2398, 9m10s).

Nit (non-blocking): the comment in clientWantsHtml says */* "both qualify" but the regex only matches text/html — the code is correct per the AC, just the prose is slightly misleading.

All ACs from #480 verified against the diff. CI green (run #2398, 9m10s). Nit (non-blocking): the comment in `clientWantsHtml` says `*/*` "both qualify" but the regex only matches `text/html` — the code is correct per the AC, just the prose is slightly misleading.
code-lead force-pushed boss/480 from c05bdb867d
All checks were successful
qa / qa (pull_request) Successful in 9m58s
qa / dockerfile (pull_request) Successful in 10s
to 1d0bf8ad67
Some checks are pending
qa / qa (pull_request) Waiting to run
qa / dockerfile (pull_request) Waiting to run
2026-04-28 06:40:29 +00:00
Compare
code-lead force-pushed boss/480 from 1d0bf8ad67
Some checks are pending
qa / qa (pull_request) Waiting to run
qa / dockerfile (pull_request) Waiting to run
to 7f4991c148
Some checks failed
qa / qa (pull_request) Failing after 12s
qa / dockerfile (pull_request) Successful in 14s
2026-04-28 06:47:18 +00:00
Compare
fix(ci): drop duplicate oauth-register-gitlab justfile recipe
All checks were successful
qa / qa (pull_request) Successful in 11m30s
qa / dockerfile (pull_request) Successful in 15s
67ae961274
The recipe was redefined at line 667 — same name as the parameterised
version added in #500 at line 185 — making `just` refuse to parse the
justfile and breaking every CI job that needs `just ci-setup`.

Drop the second copy; the original (BASE_URL parameter, prints the
GitLab Application form steps) covers the same use case. Mirrors the
fix landed on main in #512 which had not yet propagated to this branch.
code-lead deleted branch boss/480 2026-04-28 07:27:48 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!502
No description provided.