Files
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
2026-06-11 20:35:18 -04:00

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
apps/api/src/index.ts
apps/api/tests/routes/login.test.ts
apps/pwa/src/api/client.ts
apps/pwa/src/lib/loginRedirect.ts
apps/pwa/src/lib/loginRedirect.test.ts
apps/pwa/src/components/CalendarShell.tsx
true
AUTH-ENTRY-01
truths artifacts key_links
An unauthenticated top-level browser nav to /api/login completes the OIDC login and lands back on the app, logged in
An unauthenticated PWA load redirects the browser to /api/login instead of showing a dead-end 'Sign-in required'
A genuine /api/me failure does NOT cause an infinite login redirect loop (one-shot guard)
path provides contains
apps/api/src/index.ts Guarded GET /api/login route that redirects to / /api/login
path provides
apps/pwa/src/lib/loginRedirect.ts One-shot sessionStorage-guarded login-redirect helper
path provides
apps/api/tests/routes/login.test.ts Backend test: /api/login redirects to /
path provides
apps/pwa/src/lib/loginRedirect.test.ts Frontend test: unauth redirect + one-shot loop guard
from to via pattern
apps/pwa/src/components/CalendarShell.tsx apps/pwa/src/lib/loginRedirect.ts useEffect on meQuery.isError calls maybeRedirectToLogin() maybeRedirectToLogin
from to via pattern
apps/pwa/src/components/CalendarShell.tsx apps/pwa/src/lib/loginRedirect.ts meQuery.isSuccess clears the one-shot flag clearLoginRedirect
Fix the missing sign-in redirect: unauthenticated visitors to the PWA currently hit a dead-end "Sign-in required" message with no way to log in, because the SPA reaches the API only via `fetch()`, and the OIDC guard's 302 to Authelia is CORS-blocked for XHR.

Two coordinated changes:

  1. Backend: add a guarded GET /api/login route 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).
  2. Frontend: when /api/me fails 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.
cd apps/api && pnpm exec vitest run tests/routes/login.test.ts && pnpm exec tsc --noEmit GET /api/login redirects to '/' under both bypass and OIDC-passthrough paths; login.test.ts passes; apps/api tsc clean. Task 2: Add one-shot login-redirect helper + tests apps/pwa/src/lib/loginRedirect.ts, apps/pwa/src/lib/loginRedirect.test.ts, apps/pwa/src/api/client.ts - maybeRedirectToLogin(): if the sessionStorage flag 'familysync.loginRedirectAttempted' is NOT set, set it and assign window.location.href = '/api/login' (returns true = redirected). If the flag IS already set, do nothing and return false (caller falls through to the "Sign-in required" message — we already bounced through login and still can't authenticate). - clearLoginRedirect(): removes the sessionStorage flag (called on a successful /api/me load). - Helper must guard against `typeof window === 'undefined'` / missing sessionStorage so it is safe under SSR/test environments. Create apps/pwa/src/lib/loginRedirect.ts exporting: - const LOGIN_REDIRECT_KEY = 'familysync.loginRedirectAttempted' - maybeRedirectToLogin(): boolean — implements the one-shot guard above using sessionStorage.getItem/setItem and `window.location.href = '/api/login'`. Guard with a window/sessionStorage availability check; return false if unavailable. - clearLoginRedirect(): void — sessionStorage.removeItem(LOGIN_REDIRECT_KEY) (guarded).
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.
cd apps/pwa && pnpm exec vitest run src/lib/loginRedirect.test.ts && pnpm exec tsc --noEmit loginRedirect helper enforces one-shot redirect; tests pass; client.ts comment corrected; apps/pwa tsc clean. Task 3: Wire redirect into CalendarShell meQuery handling apps/pwa/src/components/CalendarShell.tsx In apps/pwa/src/components/CalendarShell.tsx, import maybeRedirectToLogin and clearLoginRedirect from '../lib/loginRedirect.js'.
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).
cd apps/pwa && pnpm exec vitest run src/components/CalendarShell.test.tsx && pnpm exec tsc --noEmit CalendarShell redirects to /api/login on unauthenticated meQuery error (one-shot) and clears the flag on success; CalendarShell smoke test + apps/pwa tsc clean. - Backend: `cd apps/api && pnpm exec vitest run && pnpm exec tsc --noEmit` — login.test.ts green, no type errors. - Frontend: `cd apps/pwa && pnpm exec vitest run && pnpm exec tsc --noEmit` — loginRedirect + CalendarShell tests green, no type errors. - Grep confirms the previously-missing redirect now exists: `grep -rn "api/login" apps/pwa/src` returns the helper, and `grep -n "/api/login" apps/api/src/index.ts` returns the route.

<success_criteria>

  • GET /api/login is 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>
Create `.planning/quick/260606-tv8-fix-missing-sign-in-redirect-in-the-pwa-/260606-tv8-SUMMARY.md` when done.

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.