# Phase 2: Users & Authentication - Context

**Gathered:** 2026-05-22
**Status:** Ready for planning

<domain>
## 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.

</domain>

<decisions>
## 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.

</decisions>

<canonical_refs>
## 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)

</canonical_refs>

<code_context>
## 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)

</code_context>

<specifics>
## 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`)

</specifics>

<deferred>
## Deferred Ideas

None — discussion stayed within phase scope.

</deferred>

---

*Phase: 2-Users & Authentication*
*Context gathered: 2026-05-22*
