11 KiB
11 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-260606-tv8 | 01 | execute | 1 |
|
true |
|
|
Two coordinated changes:
- Backend: add a guarded
GET /api/loginroute that redirects to/. A TOP-LEVEL browser navigation (not fetch) to this guarded route triggers the full OIDC login flow and returns to the app cleanly — no CORS problem (Authelia 302 is followed at the document level). - Frontend: when
/api/mefails because the user is unauthenticated, perform a full-page navigation to/api/login(one-shot, loop-guarded via sessionStorage) instead of the dead-end message.
Purpose: Restore the auth entry path for the Phase 03 PWA (Gate 2 live-verification gap). Output: Backend login route + tests; frontend redirect helper + wiring + tests.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/STATE.md @./CLAUDE.md @apps/api/src/index.ts @apps/api/src/auth/middleware.ts @apps/pwa/src/api/client.ts @apps/pwa/src/components/CalendarShell.tsx @apps/api/tests/routes/me.test.ts @apps/pwa/src/components/CalendarShell.test.tsx Task 1: Add guarded GET /api/login backend route + test apps/api/src/index.ts, apps/api/tests/routes/login.test.ts - GET /api/login returns a redirect (302) to '/' when the request reaches the handler (i.e. the OIDC guard has already let it through, or dev-bypass is active). - The route is mounted AFTER the `app.use('/api/*', oidcAuthMiddleware())` guard so that an unauthenticated top-level nav is intercepted by the guard first (guard 302 → Authelia → /callback → middleware sets `continue` back to /api/login → handler 302 → /). - Under dev-bypass (DEV_AUTH_BYPASS=true), the guard is not mounted, so /api/login still reaches the handler and redirects to '/'. In apps/api/src/index.ts, register `app.get('/api/login', (c) => c.redirect('/'))` in the protected-routes block AFTER the `if (!devBypassActive) { app.use('/api/*', oidcAuthMiddleware()) }` guard and alongside the existing `app.route('/api/me', ...)` etc. mounts (currently ~lines 48-51). Add a short comment explaining the flow: top-level nav → guard 302 → Authelia → /callback → `continue` cookie returns to /api/login (now authenticated) → redirect to /. Do NOT place it before the guard, and do NOT use `app.route` (it is a single bare GET, not a router).Create apps/api/tests/routes/login.test.ts mirroring apps/api/tests/routes/me.test.ts:
- Reuse the same hoisted DB mock and the oidcAuthMiddleware passthrough mock pattern from me.test.ts.
- Test under DEV_AUTH_BYPASS='true' (set process.env BEFORE importing app; vi.resetModules per test):
`await app.request('/api/login')` returns status 302 and the `location` header equals '/'.
- Test under no DEV_AUTH_BYPASS with the oidcAuthMiddleware passthrough mock: `/api/login`
reaches the handler and returns 302 → '/' (passthrough lets it through, mirroring me.test's
OIDC-path block).
Use `res.headers.get('location')` to assert the redirect target.
Update the stale/misleading comment block at apps/pwa/src/api/client.ts top (lines 1-10) and
the inline comment at the fetchMe failure path (~line 30): replace the false claim that "the
browser will follow the 302 redirect to Authelia automatically" — for XHR/fetch the cross-origin
302 to Authelia is CORS-blocked, so re-auth requires a TOP-LEVEL navigation to /api/login (see
loginRedirect.ts). Keep fetchMe itself a pure data fetch (still throws on non-ok); do NOT put the
redirect inside fetchMe.
Create apps/pwa/src/lib/loginRedirect.test.ts (vitest + jsdom):
- beforeEach: clear sessionStorage; stub window.location with a writable href (e.g.
Object.defineProperty(window, 'location', { value: { href: '' }, writable: true }) or
vi.stubGlobal as the existing tests do).
- maybeRedirectToLogin() sets href to '/api/login', sets the flag, returns true on first call.
- A second maybeRedirectToLogin() call does NOT change href again and returns false (one-shot).
- clearLoginRedirect() removes the flag; a subsequent maybeRedirectToLogin() redirects again.
Add two useEffects (place near the existing eventsService sync effect, ~line 171):
- On meQuery.isError: call maybeRedirectToLogin(). If it returns true (a redirect was triggered),
the page is navigating away — the "Sign-in required" branch will unmount; if it returns false
(already attempted), let the existing dead-end branch render. Depend on [meQuery.isError].
- On meQuery.isSuccess: call clearLoginRedirect() so a later session expiry can redirect again.
Depend on [meQuery.isSuccess].
Leave the existing `if (meQuery.isError) { return <Sign-in required /> }` block as the fall-through
for the already-attempted case (do not delete it). Do NOT call window.location directly in the
component — go through the helper so the one-shot guard is centralized and tested.
Verify the CalendarShell.test.tsx smoke test still passes (it mocks fetchMe success, so neither
redirect path fires); if the test environment lacks window.location/sessionStorage stubs and the
success path now calls clearLoginRedirect, ensure the helper's guards keep it a no-op (handled in
Task 2). If the existing test mocks meQuery.isError anywhere, confirm it still renders without an
unhandled navigation (the helper redirect is guarded and href assignment is inert under jsdom).
<success_criteria>
GET /api/loginis mounted behind the OIDC guard and redirects authenticated requests to '/'.- Unauthenticated PWA load triggers a single full-page navigation to '/api/login' (no CORS-blocked XHR, no infinite loop).
- A genuine backend error after one redirect falls through to "Sign-in required" instead of looping.
- The stale comment in client.ts no longer claims fetch follows the Authelia 302 automatically.
- All unit tests pass; both apps type-check clean. </success_criteria>
OUT OF SCOPE (orchestrator handles after merge, in main tree): docker compose rebuild, browser/playwright re-test, tunnel deploy. Do NOT run docker or playwright in any task.