Files
2026-05-22 14:33:20 +02:00

115 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 810 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: 810 codes, each 810 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*