--- phase: quick-260606-tv8 plan: "01" subsystem: auth-entry tags: [auth, pwa, oidc, redirect, one-shot-guard] dependency_graph: requires: [] provides: [AUTH-ENTRY-01] affects: [apps/api/src/index.ts, apps/pwa/src/lib/loginRedirect.ts, apps/pwa/src/components/CalendarShell.tsx] tech_stack: added: [] patterns: - sessionStorage one-shot redirect guard for OIDC re-auth - top-level nav to /api/login bypasses XHR CORS block on Authelia 302 key_files: created: - apps/api/tests/routes/login.test.ts - apps/pwa/src/lib/loginRedirect.ts - apps/pwa/src/lib/loginRedirect.test.ts modified: - apps/api/src/index.ts - apps/pwa/src/api/client.ts - apps/pwa/src/components/CalendarShell.tsx decisions: - "GET /api/login placed after OIDC guard with bare app.get (not app.route) — single redirect, no router needed" - "sessionStorage chosen over localStorage for the one-shot flag — flag is per-tab and clears on tab close, preventing stale block across sessions" - "useEffect dependencies are meQuery.isError and meQuery.isSuccess (not the query object) — stable boolean flags, no unnecessary re-invocations" metrics: duration: "5 minutes" completed_date: "2026-06-07" tasks_completed: 3 files_changed: 6 --- # Quick Task 260606-tv8: Fix Missing Sign-in Redirect in the PWA — Summary **One-liner:** Restored the OIDC auth entry path by adding a guarded `/api/login` backend route and a sessionStorage one-shot redirect helper wired into CalendarShell, so unauthenticated PWA loads trigger a full-page navigation instead of a dead-end "Sign-in required" message. ## What Was Built ### Task 1: GET /api/login backend route + tests (commit 237ec49) Added `app.get('/api/login', (c) => c.redirect('/'))` in `apps/api/src/index.ts`, placed after the OIDC guard block. When an unauthenticated user navigates to `/api/login` as a top-level browser request: 1. The guard intercepts and 302s to Authelia. 2. After login, Authelia POSTs to `/callback`; the middleware sets a `continue` cookie pointing to `/api/login`. 3. The browser follows the cookie back to `/api/login` (now authenticated); the handler 302s to `/` and the SPA boots. Under `DEV_AUTH_BYPASS=true`, the guard is not mounted so the handler fires directly and redirects to `/`. Created `apps/api/tests/routes/login.test.ts` (3 tests, mirrors `me.test.ts` pattern) covering both the bypass path and the OIDC-passthrough path. ### Task 2: One-shot login-redirect helper + tests (commit 6dc9ccd) Created `apps/pwa/src/lib/loginRedirect.ts` exporting: - `maybeRedirectToLogin()` — sets `sessionStorage['familysync.loginRedirectAttempted']` and assigns `window.location.href = '/api/login'` on first call; returns `false` on subsequent calls (loop guard). Guarded against `window`/`sessionStorage` unavailability. - `clearLoginRedirect()` — removes the flag, allowing future re-auth redirects. Created `apps/pwa/src/lib/loginRedirect.test.ts` (5 tests) covering first-call redirect, one-shot no-op, and clear+retry. Updated `apps/pwa/src/api/client.ts` to remove the false claim that "the browser will follow the 302 redirect to Authelia automatically" — fetch/XHR cannot follow cross-origin 302s to Authelia (CORS-blocked). The comment now accurately describes that top-level navigation via `/api/login` is required. ### Task 3: CalendarShell meQuery wiring (commit c2e0ab1) Added two `useEffect` hooks in `CalendarShell.tsx`: - On `meQuery.isError`: calls `maybeRedirectToLogin()`. If first attempt, page navigates away. If already attempted, falls through to the existing "Sign-in required" branch. - On `meQuery.isSuccess`: calls `clearLoginRedirect()` so a later session expiry can redirect again. The existing `if (meQuery.isError) { return }` block is retained as the fall-through for the already-attempted case. ## Deviations from Plan None — plan executed exactly as written. ## Verification Results - Backend: `vitest run` → 95/95 tests pass (13 test files); `tsc --noEmit` clean. - Frontend: `vitest run` → 131/131 tests pass (11 test files); `tsc --noEmit` clean. - Grep confirms `/api/login` appears in `apps/api/src/index.ts` (route) and `apps/pwa/src/lib/loginRedirect.ts` (helper). ## Known Stubs None. ## Threat Flags None — no new network endpoints (the `/api/login` route is behind the existing `/api/*` OIDC guard), no new auth paths (redirect target is Authelia, same as before), no schema changes. ## Self-Check: PASSED Files exist: - apps/api/src/index.ts (modified) - apps/api/tests/routes/login.test.ts (created) - apps/pwa/src/lib/loginRedirect.ts (created) - apps/pwa/src/lib/loginRedirect.test.ts (created) - apps/pwa/src/api/client.ts (modified) - apps/pwa/src/components/CalendarShell.tsx (modified) Commits: - 237ec49: feat(260606-tv8-01): add guarded GET /api/login route + tests - 6dc9ccd: feat(260606-tv8-01): add one-shot login-redirect helper + tests; fix client.ts comment - c2e0ab1: feat(260606-tv8-01): wire login redirect into CalendarShell meQuery handling