8.9 KiB
Phase 2: Users & Authentication - Context
Gathered: 2026-05-22 Status: Ready for planning
## Phase BoundaryShip 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.
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.examplewith placeholder values and comments. - D-02: When
SMTP_HOSTis not set (dev/local), the password reset token/link is logged to backend stdout (visible indocker 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-resetendpoint 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_PASSWORDenv vars. On app startup (lifespan), if no users exist and both vars are set, create the admin account withrole = 'admin'. Idempotent — skipped if any users already exist. - D-05: If
ADMIN_EMAIL/ADMIN_PASSWORDare not set, log aWARNINGat 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
usersrow has aquotasrow.
API Auth Scope
- D-07: Phase 2 ships only the new auth and admin endpoints. Existing
/api/documents,/api/topics,/api/settingsstay public — they gainget_current_userguards in Phase 3 when per-user document isolation is enforced. - D-08: Admin endpoints live at
/api/admin/*. Every handler in this router requiresget_current_admin(not justget_current_user). This enforces SEC-07: admin role verified on every admin request. - D-09: CORS locked down via
CORS_ORIGINSenv var (comma-separated list). Pydantic Settings parses it aslist[str]. Default when not set:["http://localhost:5173"].allow_origins=["*"]removed in Phase 2.
Frontend Auth UX
- D-10: Full auth wall. Vue Router
beforeEachguard: if noaccessTokeninuseAuthStoreand route is not/loginor/register, redirect to/login. After login, redirect back to the originally requested route. - D-11:
useAuthStoreholds{ accessToken, user }in memory (never localStorage). A centralized fetch wrapper (updatingfrontend/src/api/client.js) addsAuthorization: Bearerheader and handles 401 by calling/api/auth/refresh(uses httpOnly cookie), retrying the original request, or redirecting to/loginon refresh failure. - D-12: Admin panel at
/adminroute with a dedicatedAdminViewcomponent. Sidebar link (AppSidebar.vue) visible only whenuseAuthStore().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_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: PyOTPvalid_window=1recommendation 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 withSECRET_KEY(already in .env.example),SMTP_HOST,SMTP_PORT,SMTP_USER,SMTP_PASSWORD,SMTP_FROM,ADMIN_EMAIL,ADMIN_PASSWORD,CORS_ORIGINSbackend/db/models.py—User,RefreshToken,QuotaORM models are fully defined and migrated; no schema changes needed for Phase 2.AuditLogmodel also ready.backend/celery_app.py— Celery app already configured; addtasks/email_tasks.pyfor reset email dispatch (mirrorstasks/document_tasks.pypattern)backend/deps/db.py— existingget_dbdependency; addget_current_userandget_current_adminFastAPI dependencies herebackend/main.py— lifespan already handles MinIO + engine disposal; extend to add admin bootstrap and CORS updatebackend/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 howget_current_usershould be structured - Service layer (
services/extractor.py,services/classifier.py) — pure Python modules, no FastAPI coupling; newservices/auth.pyandservices/email.pyfollow the same boundary - Pinia-as-Facade — existing stores (
documents,topics,settings) never call API directly; newuseAuthStorefollows same pattern api/router modules —api/documents.py,api/topics.py,api/settings.py; addapi/auth.pyandapi/admin.pyfollowing same structure
Integration Points
backend/main.py— includeapi/auth.pyandapi/admin.pyrouters; updateCORSMiddlewareto useCORS_ORIGINS; add admin bootstrap to lifespanfrontend/src/router/index.js— add navigation guard +/login,/register,/account,/adminroutesfrontend/src/api/client.js— updaterequest()to inject Bearer token and handle 401 auto-refreshfrontend/src/stores/— addauth.js(useAuthStore); existing stores don't need changes in Phase 2frontend/src/components/layout/AppSidebar.vue— conditionally show/adminlink based onuseAuthStore().user?.role
Key Constraints from Phase 1
- All CORS currently
["*"]— must updateCORSMiddlewareinmain.py(Phase 2 D-09) SECRET_KEYis already in.env.exampleandconfig.pywith 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_idstays nullable — do NOT add NOT NULL in Phase 2 (Phase 3 migration adds it)
</code_context>
## Specific Ideas- PyOTP
valid_window=1recommended (from STATE.md Open Questions) — allows ±30s clock drift without expanding the replay window excessively - Audit existing codebase for any
passlib/bcryptusage 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_codestable (or JSONB inusers— 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 forvalid_window=1)
None — discussion stayed within phase scope.
Phase: 2-Users & Authentication Context gathered: 2026-05-22