# Phase 2: Users & Authentication - Context **Gathered:** 2026-05-22 **Status:** Ready for planning ## Phase Boundary Ship a complete authentication and user management system: registration (with Argon2 + breach check), JWT session management (access token in Pinia memory, refresh in httpOnly cookie), TOTP 2FA enrollment and verification, backup code issuance and invalidation, password reset via email (SMTP/Celery), sign-out-all-devices, and an admin panel (user create/deactivate/reset, quota adjustment, AI provider assignment). The existing `/api/documents`, `/api/topics`, and `/api/settings` endpoints remain public in Phase 2 — they gain auth guards in Phase 3 when per-user isolation is enforced. Phase 2 adds only the new `/api/auth/*` and `/api/admin/*` endpoints. The `documents.user_id` nullable D-03 constraint is NOT lifted in Phase 2 (that is Phase 3's migration). The frontend receives a full auth wall: Vue Router guards redirect unauthenticated users to `/login`. A new `/admin` route surfaces the admin panel, visible in the sidebar only for `role === 'admin'` users. ## Implementation Decisions ### Email Transport - **D-01:** Email sent via SMTP configured through env vars: `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_FROM`. Add to `.env.example` with placeholder values and comments. - **D-02:** When `SMTP_HOST` is not set (dev/local), the password reset token/link is logged to backend stdout (visible in `docker compose logs`). The API response is still 202 — no token in response body. - **D-03:** Reset email dispatch is a Celery task (async). The `/api/auth/password-reset` endpoint enqueues the task and returns 202 immediately. Celery + Redis are already wired from Phase 1 (D-08/D-09). ### Admin Bootstrap - **D-04:** First admin account is seeded on startup via `ADMIN_EMAIL` + `ADMIN_PASSWORD` env vars. On app startup (lifespan), if no users exist and both vars are set, create the admin account with `role = 'admin'`. Idempotent — skipped if any users already exist. - **D-05:** If `ADMIN_EMAIL`/`ADMIN_PASSWORD` are not set, log a `WARNING` at startup but do not fail. App starts normally. - **D-06:** Admin bootstrap creates a quota row with the standard 100 MB default (same as regular users). Consistent with the quota model — every `users` row has a `quotas` row. ### API Auth Scope - **D-07:** Phase 2 ships only the new auth and admin endpoints. Existing `/api/documents`, `/api/topics`, `/api/settings` stay public — they gain `get_current_user` guards in Phase 3 when per-user document isolation is enforced. - **D-08:** Admin endpoints live at `/api/admin/*`. Every handler in this router requires `get_current_admin` (not just `get_current_user`). This enforces SEC-07: admin role verified on every admin request. - **D-09:** CORS locked down via `CORS_ORIGINS` env var (comma-separated list). Pydantic Settings parses it as `list[str]`. Default when not set: `["http://localhost:5173"]`. `allow_origins=["*"]` removed in Phase 2. ### Frontend Auth UX - **D-10:** Full auth wall. Vue Router `beforeEach` guard: if no `accessToken` in `useAuthStore` and route is not `/login` or `/register`, redirect to `/login`. After login, redirect back to the originally requested route. - **D-11:** `useAuthStore` holds `{ accessToken, user }` in memory (never localStorage). A centralized fetch wrapper (updating `frontend/src/api/client.js`) adds `Authorization: Bearer` header and handles 401 by calling `/api/auth/refresh` (uses httpOnly cookie), retrying the original request, or redirecting to `/login` on refresh failure. - **D-12:** Admin panel at `/admin` route with a dedicated `AdminView` component. Sidebar link (`AppSidebar.vue`) visible only when `useAuthStore().user.role === 'admin'`. Admin panel has sub-navigation: Users, Quotas, AI Config. - **D-13:** Full TOTP enrollment UI in Phase 2. Enrollment screen: QR code (from provisioning URI), manual secret key display, TOTP code entry to verify enrollment. Backup codes screen: display 8–10 codes with copy-all button, explicit acknowledgment checkbox before enabling. Not deferred to Phase 3. ## Canonical References **Downstream agents MUST read these before planning or implementing.** ### Requirements - `.planning/REQUIREMENTS.md` — AUTH-01 through AUTH-08 (full auth flow), SEC-01/02/03/05/06/07 (security cross-cuts), ADMIN-01 through ADMIN-05/ADMIN-07 (admin capabilities) ### Project Decisions - `.planning/ROADMAP.md` — Phase 2 goal and all 5 success criteria (especially #2 TOTP flow, #4 sign-out-all + family revocation, #5 admin 403 on document access) - `.planning/PROJECT.md` — Key Decisions: JWT httpOnly cookie strategy, HKDF per-user key derivation (not relevant until Phase 5 but sets the precedent), admin impersonation exclusion - `.planning/STATE.md` — Open Questions: PyOTP `valid_window=1` recommendation for ±30s clock drift; "Audit existing codebase for any bcrypt hashes before removing passlib in Phase 2" ### Phase 1 Context (carry-forward decisions) - `.planning/phases/01-infrastructure-foundation/01-CONTEXT.md` — D-03 (documents.user_id nullable, NOT NULL deferred to Phase 3), D-08 (Celery+Redis wired), D-09 (Redis doubles as rate-limit store), D-16 (SECRET_KEY documented in .env.example, not yet read in Phase 1 code) ## Existing Code Insights ### Reusable Assets - `backend/config.py` — Pydantic Settings (`SettingsConfigDict`); extend with `SECRET_KEY` (already in .env.example), `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_FROM`, `ADMIN_EMAIL`, `ADMIN_PASSWORD`, `CORS_ORIGINS` - `backend/db/models.py` — `User`, `RefreshToken`, `Quota` ORM models are fully defined and migrated; no schema changes needed for Phase 2. `AuditLog` model also ready. - `backend/celery_app.py` — Celery app already configured; add `tasks/email_tasks.py` for reset email dispatch (mirrors `tasks/document_tasks.py` pattern) - `backend/deps/db.py` — existing `get_db` dependency; add `get_current_user` and `get_current_admin` FastAPI dependencies here - `backend/main.py` — lifespan already handles MinIO + engine disposal; extend to add admin bootstrap and CORS update - `backend/ai/base.py` + `backend/ai/__init__.py` — ABC + factory pattern to mirror when structuring auth dependency chain ### Established Patterns - **Provider pattern** (`ai/`) — dependency injection via factory; mirrors how `get_current_user` should be structured - **Service layer** (`services/extractor.py`, `services/classifier.py`) — pure Python modules, no FastAPI coupling; new `services/auth.py` and `services/email.py` follow the same boundary - **Pinia-as-Facade** — existing stores (`documents`, `topics`, `settings`) never call API directly; new `useAuthStore` follows same pattern - **`api/` router modules** — `api/documents.py`, `api/topics.py`, `api/settings.py`; add `api/auth.py` and `api/admin.py` following same structure ### Integration Points - `backend/main.py` — include `api/auth.py` and `api/admin.py` routers; update `CORSMiddleware` to use `CORS_ORIGINS`; add admin bootstrap to lifespan - `frontend/src/router/index.js` — add navigation guard + `/login`, `/register`, `/account`, `/admin` routes - `frontend/src/api/client.js` — update `request()` to inject Bearer token and handle 401 auto-refresh - `frontend/src/stores/` — add `auth.js` (useAuthStore); existing stores don't need changes in Phase 2 - `frontend/src/components/layout/AppSidebar.vue` — conditionally show `/admin` link based on `useAuthStore().user?.role` ### Key Constraints from Phase 1 - All CORS currently `["*"]` — must update `CORSMiddleware` in `main.py` (Phase 2 D-09) - `SECRET_KEY` is already in `.env.example` and `config.py` with default `"CHANGEME"` — Phase 2 reads it for JWT signing - Rate limiting store: Redis is already wired as the Celery broker — also use it for rate limit counters (no second Redis needed per D-09) - `documents.user_id` stays nullable — do NOT add NOT NULL in Phase 2 (Phase 3 migration adds it) ## Specific Ideas - PyOTP `valid_window=1` recommended (from STATE.md Open Questions) — allows ±30s clock drift without expanding the replay window excessively - Audit existing codebase for any `passlib`/`bcrypt` usage before removing them (STATE.md note) - Reset token format: signed JWT (separate short-lived token, not a session token) with `sub=user_id`, `type=password-reset`, `exp=now+3600` - Backup codes: 8–10 codes, each 8–10 alphanumeric chars, stored as Argon2 hashes in a `backup_codes` table (or JSONB in `users` — researcher should check best practice) - TOTP replay prevention: store used `(user_id, code, validity_window_bucket)` tuples in Redis with TTL = TOTP validity window (60s for `valid_window=1`) ## Deferred Ideas None — discussion stayed within phase scope. --- *Phase: 2-Users & Authentication* *Context gathered: 2026-05-22*