---
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 <Sign-in required /> }` 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
