phase 7: auth flow UI (email-code login + session resume + revocation)
Implements ui/PLAN.md Phase 7 end-to-end: - /login two-step form (email -> code) over the gateway public REST surface; /lobby placeholder issues the first authenticated user.account.get and renders the decoded display name. - SessionStore (Svelte 5 runes) with loading / unsupported / anonymous / authenticated states; layout-level route guard, browser-not-supported blocker, and a minimal SubscribeEvents revocation watcher that closes the active client within 1s on a clean stream end or Unauthenticated. - VITE_GATEWAY_BASE_URL + VITE_GATEWAY_RESPONSE_PUBLIC_KEY config plus AuthError taxonomy in api/auth.ts. - Vitest (auth-api, session-store, login-page) and Playwright e2e (auth-flow.spec.ts) on the four configured projects, with a fixture Ed25519 keypair forging Connect-Web JSON responses. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+94
-36
@@ -677,54 +677,112 @@ Targeted tests:
|
||||
including non-deterministic WebKit), and that
|
||||
`clearDeviceSession` forces a fresh keypair on next load.
|
||||
|
||||
## Phase 7. Auth Flow UI
|
||||
## ~~Phase 7. Auth Flow UI~~
|
||||
|
||||
Status: pending.
|
||||
Status: done.
|
||||
|
||||
Goal: implement the full email-code login flow with device session
|
||||
registration and post-login redirect to a placeholder lobby.
|
||||
|
||||
Artifacts:
|
||||
Decisions taken with the project owner before implementation:
|
||||
|
||||
- `ui/frontend/src/routes/login` two-step form (email → code)
|
||||
- `ui/frontend/src/api/auth.ts` wraps `public.auth.send_email_code` and
|
||||
`public.auth.confirm_email_code`, registers the public key, persists
|
||||
via `KeyStore`
|
||||
- `ui/frontend/src/lib/session-store.ts` Svelte store exposing the
|
||||
current session state
|
||||
- `ui/frontend/src/routes/+layout.svelte` redirect to `/login` for
|
||||
unauthenticated routes; redirect to `/lobby` on successful confirm
|
||||
- placeholder `ui/frontend/src/routes/lobby/+page.svelte` rendering
|
||||
`you are logged in`
|
||||
- topic doc `ui/docs/auth-flow.md` describing error UX, code
|
||||
resend, expired challenge handling, and re-login on revocation
|
||||
1. **Playwright e2e against a mocked gateway.** `page.route(...)`
|
||||
intercepts the public auth REST surface and the Connect-Web
|
||||
`ExecuteCommand` / `SubscribeEvents` URLs; a fixture Ed25519 key in
|
||||
`tests/e2e/fixtures/gateway-key.ts` signs the forged responses so
|
||||
`GalaxyClient.verifyResponse` accepts them under the matching
|
||||
public key the dev server picks up via
|
||||
`VITE_GATEWAY_RESPONSE_PUBLIC_KEY`. The wire-contract path is
|
||||
already covered by the Go integration suite
|
||||
(`integration/auth_flow_test.go`).
|
||||
2. **Build-time gateway response public key delivery.** The browser
|
||||
reads `VITE_GATEWAY_RESPONSE_PUBLIC_KEY` (standard base64 of the
|
||||
raw 32-byte key) on module load. A future phase may switch to a
|
||||
`/api/v1/public/well-known/...` endpoint when prod distribution is
|
||||
wired up; Phase 7 stops at the env-var.
|
||||
3. **Minimal SubscribeEvents-based revocation watcher.** The lobby
|
||||
layout opens a long-running stream and treats two outcomes as
|
||||
revocation: a clean end-of-stream (the gateway closing after a
|
||||
`session_invalidation` event) and a Connect `Unauthenticated`
|
||||
error. Network errors and `Canceled` aborts stay silent so a
|
||||
flaky connection or page navigation does not bounce the user. The
|
||||
per-event dispatch path lands in Phase 24.
|
||||
4. **Browser-not-supported blocker.** The root layout runs a one-time
|
||||
`crypto.subtle.generateKey({name:"Ed25519"}, ...)` probe on boot
|
||||
and renders a blocker page when the probe rejects. This closes
|
||||
Phase 6's "no JS Ed25519 fallback" follow-up.
|
||||
|
||||
Artifacts (delivered):
|
||||
|
||||
- `ui/frontend/src/routes/login/+page.svelte` (+ `+page.ts` with
|
||||
`prerender = false; ssr = false;`) — two-step form (email → code)
|
||||
with resend and change-email affordances.
|
||||
- `ui/frontend/src/routes/lobby/+page.svelte` (+ `+page.ts`) —
|
||||
placeholder lobby that issues the first authenticated
|
||||
`user.account.get` through `GalaxyClient` and surfaces the decoded
|
||||
display name.
|
||||
- `ui/frontend/src/routes/+layout.svelte` — boot-time session init,
|
||||
route guard (anonymous → `/login`, authenticated on `/login` →
|
||||
`/lobby`), browser-not-supported blocker, and the revocation
|
||||
watcher lifecycle. `+layout.ts` puts the whole tree into SPA mode
|
||||
(`ssr = false; prerender = false;`).
|
||||
- `ui/frontend/src/api/auth.ts` — `sendEmailCode`,
|
||||
`confirmEmailCode`, and the `AuthError` taxonomy over
|
||||
`/api/v1/public/auth/*`.
|
||||
- `ui/frontend/src/lib/env.ts` — `GATEWAY_BASE_URL`,
|
||||
`GATEWAY_RESPONSE_PUBLIC_KEY` (decoded once on module load).
|
||||
- `ui/frontend/src/lib/session-store.svelte.ts` — `SessionStore`
|
||||
singleton (Svelte 5 runes); states `loading | unsupported |
|
||||
anonymous | authenticated`; `init`, `signIn`, `signOut("user" |
|
||||
"revoked")`.
|
||||
- `ui/frontend/src/lib/revocation-watcher.ts` — opens
|
||||
`SubscribeEvents` against the gateway, signs the envelope through
|
||||
`Core.signRequest`, treats clean stream end / `Unauthenticated` as
|
||||
revocation.
|
||||
- `ui/frontend/.env.example` — `VITE_GATEWAY_BASE_URL`,
|
||||
`VITE_GATEWAY_RESPONSE_PUBLIC_KEY`.
|
||||
- Topic doc `ui/docs/auth-flow.md`; cross-references from
|
||||
`ui/docs/storage.md` and `ui/README.md`.
|
||||
- Vitest: `tests/auth-api.test.ts`, `tests/session-store.test.ts`,
|
||||
`tests/login-page.test.ts`.
|
||||
- Playwright: `tests/e2e/auth-flow.spec.ts` (4 cases × 4 projects)
|
||||
with the fixture key plumbing in
|
||||
`tests/e2e/fixtures/{gateway-key,canon,sign-response}.ts`.
|
||||
- Pre-existing `tests/e2e/landing.spec.ts` was deleted; the landing
|
||||
surface is no longer reachable in the auth-gated app and the
|
||||
Vitest unit test on `routes/+page.svelte` retains the version
|
||||
footer assertion.
|
||||
|
||||
Dependencies: Phase 6.
|
||||
|
||||
Acceptance criteria:
|
||||
Acceptance criteria (met):
|
||||
|
||||
- a fresh browser completes login end-to-end against a local
|
||||
gateway+backend stack;
|
||||
- the first authenticated Connect call after login (e.g.
|
||||
`user.account.read`) succeeds end-to-end through `WasmCore` →
|
||||
`GalaxyClient` → ConnectRPC → gateway, with the response signature
|
||||
verified and the payload decoded back to JSON. This bullet
|
||||
subsumes the gateway-acceptance check originally listed in
|
||||
Phase 6; the Phase 6 storage layer cannot pass it without
|
||||
persisting and signing correctly;
|
||||
- a returning browser resumes the session without re-login;
|
||||
- gateway-side session revocation closes the active client immediately
|
||||
and routes back to `/login`.
|
||||
- A fresh browser completes login end-to-end via the mocked gateway
|
||||
in all four Playwright projects; the first authenticated Connect
|
||||
call (`user.account.get`) succeeds end-to-end through `WasmCore` →
|
||||
`GalaxyClient` → ConnectRPC and the response signature is verified
|
||||
under `VITE_GATEWAY_RESPONSE_PUBLIC_KEY`. This bullet subsumes the
|
||||
gateway-acceptance check originally listed in Phase 6.
|
||||
- A returning browser resumes the session without re-login (covered
|
||||
by `tests/e2e/auth-flow.spec.ts::"returning user lands on the
|
||||
lobby without re-login"`).
|
||||
- Gateway-side session revocation closes the active client within one
|
||||
second and routes back to `/login` (covered by
|
||||
`tests/e2e/auth-flow.spec.ts::"server-side revocation closes the
|
||||
active client within one second"`).
|
||||
|
||||
Targeted tests:
|
||||
Targeted tests (delivered):
|
||||
|
||||
- Vitest component tests for the login forms with mocked
|
||||
`GalaxyClient`;
|
||||
- Playwright e2e test driving the full flow against a local stack in
|
||||
desktop and mobile viewports, asserting the first authenticated
|
||||
Connect call returns successfully after login;
|
||||
- regression test for revocation: server-side revoke causes client
|
||||
redirect within one second.
|
||||
- Vitest component tests for the login form with mocked `auth.ts`
|
||||
(six cases: email step, error mapping, code step, expired-code
|
||||
bounce, resend, change-email).
|
||||
- Vitest tests for `SessionStore` (init, signIn/signOut, support
|
||||
probe, idempotency) and for the auth REST wrappers (URL/body
|
||||
shape, base64 public key, `AuthError` mapping).
|
||||
- Playwright e2e suite (`auth-flow.spec.ts`) on
|
||||
chromium-desktop / webkit-desktop / chromium-mobile-iphone-13 /
|
||||
chromium-mobile-pixel-5: fresh login, returning user, revocation
|
||||
within one second, browser-not-supported blocker.
|
||||
|
||||
## Phase 8. Lobby UI
|
||||
|
||||
|
||||
Reference in New Issue
Block a user