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

8.9 KiB
Raw Permalink Blame History

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 810 codes with copy-all button, explicit acknowledgment checkbox before enabling. Not deferred to Phase 3.

<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.pyUser, 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 modulesapi/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>

## 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)
## Deferred Ideas

None — discussion stayed within phase scope.


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