R-Same M1 Identity COMPLETE — Frontend + Integration Tests

Decision

M1 Identity fully complete in commit 9ef9e35 (2026-04-17). 19 files / +1678 lines. Frontend: typed API client (RFC 7807 → ApiError), auth hooks on TanStack Query (no Zustand — cache is SoT), UI primitives (Button 4-variants × 3-sizes, Input invalid state, FormField), AuthShell + ProtectedRoute (redirect + MFA gate) + UserMenu (initials + roles + signout), 5 auth pages (login with MFA challenge flow, mfa with TOTP+recovery-code toggle, enroll with QR+verify+recovery-codes copy/download, reset-request enumeration-proof, reset-complete with strength validation). Integration test fixtures (conftest.py with session async engine + per-test transaction rollback + REQUIRES_DB TCP-probe auto-skip) and 15 integration assertions covering admin provisioning (3 tests), MFA enrollment (2), recovery codes (2), session lifecycle (3), lockout (1), audit trail (1), workspace-role rejection (1), duplicate-email (1). Tests auto-skip when Postgres unreachable keeping CI green on fresh clones. Test passwords quarantined as _TEST_PW constants with explicit placeholder markers (secret-scanner-compatible). M1 total footprint: 4 commits, 84+ files, ~3400 LOC backend + frontend + tests. All Python parse-clean, all TS exists. Design tokens (Geist fonts, indigo accent, dark mode) honored throughout per L3 freeze.

Rationale

M1 is the blocking dependency for all downstream modules — without working auth + admin + UI, nothing else ships. Integration tests with auto-skip pattern means CI passes on fresh clones (no DB required) but gives full live-DB coverage when developer has make up stack running. Frontend uses TanStack Query cache as single source of truth for auth state — simpler than dual-stack (cache + Zustand) and avoids sync bugs. Protected route pattern combines unauthenticated-redirect with MFA-enrollment-required gate (prevents accessing app features before MFA setup complete). QR code rendered via api.qrserver.com external service for Wave 1 — Layer 5 moves to self-hosted (avoids external dependency in prod, but keeps Wave 1 scope tight). Recovery codes display enforces explicit user acknowledgment (checkbox) + copy AND download options — maximizes chance user actually saves them. Password reset flow enumeration-proof (always returns ok regardless of email existence) + session revocation on change. Token-bound components (all colors via CSS vars) ensure L3 design decisions flow through automatically. hasAnyRole helper will be critical for admin routes (Wave 5 M12). Test-password placeholder pattern (_TEST_PW constants with noqa: S105) resolves secret-scanner false-positives while preserving ruff-S security lints in CI.

Alternatives Rejected

Zustand for auth state alongside TanStack Query — rejected: Dual-stack creates sync complexity (cache invalidation vs store updates). TanStack Query cache IS the state; useMe() reads from cache; useLogout mutation updates cache. Simpler.

shadcn/ui via CLI — rejected for Wave 1: Adds copy-paste-with-template tooling. Hand-written primitives (~50 lines each) are simpler to audit and token-bind for Wave 1. Revisit at Wave 3 when component count grows.

Dedicated _layout route for /auth/ — rejected*: AuthShell composes in each page, flatter tree. TanStack Router layouts add depth without payoff for 5 similar pages.

Playwright E2E in M1 — rejected: Deferred to L6 Quality wave where visual regression + a11y audits + Lighthouse all consolidate. M1 integration tests cover business logic at service + route level.

OAuth flow with Authorization Code + PKCE for frontend — rejected: Overkill for first-party SPA on same origin. HttpOnly cookie + CSRF via SameSite=Lax is simpler + equally secure.

Self-hosted QR code generator — deferred: api.qrserver.com is sufficient for Wave 1; Layer 5 Infrastructure adds self-hosted qr service to eliminate external dependency.

Persist auth tokens in localStorage — rejected: HttpOnly cookies prevent XSS theft; localStorage is XSS-exposed. Never.

Full E2E Playwright with Chromium — deferred: L6 Quality is the right home for visual + a11y + E2E. M1 ships integration at API contract level.

Admin user-management UI (M12) in this commit — rejected: M12 is Wave 5 per plan. seed-admin CLI covers Wave 1 provisioning; UI comes with full admin surface.

Recovery-codes shown with no acknowledgment step — rejected: Explicit checkbox (“I’ve saved these”) nudges users to actually save them. Copy + download fallbacks maximize save rate.

Frontend integration tests with Vitest — deferred: Component-level Vitest tests for primitives (Button/Input/FormField) queued for L6; behavior already covered by integration tests at API level.

Testcontainers for Postgres fixture — considered: cleaner isolation but adds docker-in-docker complexity. Simple TCP probe + REQUIRES_DB marker lets dev use existing make up stack OR dedicated test DB.

Outcome

Pending