docs(02): capture phase context
This commit is contained in:
@@ -0,0 +1,114 @@
|
|||||||
|
# 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*
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# Phase 2: Users & Authentication - Discussion Log
|
||||||
|
|
||||||
|
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||||
|
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||||
|
|
||||||
|
**Date:** 2026-05-22
|
||||||
|
**Phase:** 2-Users & Authentication
|
||||||
|
**Areas discussed:** Email transport, Admin bootstrap, API auth scope, Frontend auth UX
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Email Transport
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| SMTP via env vars | SMTP_HOST/PORT/USER/PASSWORD/FROM env vars; dev fallback to stdout | ✓ |
|
||||||
|
| Resend / Mailgun API | Third-party transactional email API; one env var (API key) | |
|
||||||
|
| Console-only for now | Print reset link to stdout only; not production-ready | |
|
||||||
|
|
||||||
|
**User's choice:** SMTP via env vars
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| Log reset link to stdout | Developer sees link in docker compose logs; zero extra config | ✓ |
|
||||||
|
| Return token in API response | Token in JSON response when SMTP not set; easier for testing | |
|
||||||
|
| Return 503 | Hard failure if SMTP not configured; breaks local dev | |
|
||||||
|
|
||||||
|
**User's choice:** Log the reset link to backend stdout when SMTP not configured
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| Celery task (async) | Email enqueued; API returns 202 immediately; Redis already wired | ✓ |
|
||||||
|
| Synchronous inline | Email sent during request; SMTP timeout blocks user | |
|
||||||
|
|
||||||
|
**User's choice:** Celery task — async dispatch, 202 returned immediately
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Admin Bootstrap
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| ENV-var bootstrap on startup | ADMIN_EMAIL + ADMIN_PASSWORD; seeded on startup if no users exist; idempotent | ✓ |
|
||||||
|
| Alembic seed migration | Admin row inserted in migration; credentials from env at migration time | |
|
||||||
|
| CLI management command | Manual python manage.py create-admin step after deploy | |
|
||||||
|
| First-user-is-admin | First registered user becomes admin; race-condition risk | |
|
||||||
|
|
||||||
|
**User's choice:** ENV-var bootstrap on startup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| Skip silently if not set | App starts normally; admin can be created later | |
|
||||||
|
| Warn in startup logs if not set | WARNING logged; app starts; helpful reminder | ✓ |
|
||||||
|
| Refuse to start if not set | Startup fails; blocks headless/CI environments | |
|
||||||
|
|
||||||
|
**User's choice:** Warn in startup logs (app still starts)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| Same 100 MB default quota | Consistent; every users row gets a quotas row | ✓ |
|
||||||
|
| Unlimited quota for admin | Signals admins are operators, not subject to storage limits | |
|
||||||
|
| No quota row for admin | Admin doesn't upload; but requires special-casing in enforcement | |
|
||||||
|
|
||||||
|
**User's choice:** Same 100 MB default quota as regular users
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Auth Scope
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| Only new auth endpoints; existing stay public | Phase 2 ships /api/auth/* only; D-03 nullable user_id preserved | ✓ |
|
||||||
|
| Lock down all endpoints in Phase 2 | All endpoints get get_current_user + NOT NULL migration | |
|
||||||
|
|
||||||
|
**User's choice:** Only new auth endpoints; existing document/topics/settings stay public
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| /api/admin/* prefix | All admin under /api/admin/; get_current_admin on every handler | ✓ |
|
||||||
|
| /api/users/* with role check | Shared prefix, role-based access control | |
|
||||||
|
| /admin/* separate mount | Separate path, possible separate FastAPI sub-application | |
|
||||||
|
|
||||||
|
**User's choice:** /api/admin/* prefix with get_current_admin dependency
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| CORS_ORIGINS env var (comma-separated) | Pydantic parses as list; default localhost:5173 | ✓ |
|
||||||
|
| Hardcode origins per environment | Config file per environment or build-time injection | |
|
||||||
|
| Keep allow_origins=['*'] | Not recommended with auth cookies now in play | |
|
||||||
|
|
||||||
|
**User's choice:** CORS_ORIGINS env var, comma-separated
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Auth UX
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| Full auth wall | beforeEach guard redirects to /login if no access token | ✓ |
|
||||||
|
| Partial — auth only gates account/admin | Documents/topics still accessible without login | |
|
||||||
|
|
||||||
|
**User's choice:** Full auth wall — entire app requires login
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| useAuthStore + fetch interceptor | In-memory token; auto-refresh on 401; redirect on refresh failure | ✓ |
|
||||||
|
| Token in composable with manual headers | More explicit but repetitive across API calls | |
|
||||||
|
| You decide | Defer to downstream agents | |
|
||||||
|
|
||||||
|
**User's choice:** useAuthStore with in-memory token + fetch interceptor
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| /admin route, separate AdminView | Route guard (role=admin); sub-navigation; sidebar link conditionally shown | ✓ |
|
||||||
|
| Integrated into /settings as Admin tab | Mixes admin and personal settings | |
|
||||||
|
| You decide | Defer to UI researcher | |
|
||||||
|
|
||||||
|
**User's choice:** /admin route with dedicated AdminView; sidebar link visible for admin role only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| Full TOTP enrollment UI in Phase 2 | QR code + backup codes + acknowledgment; aligned with success criteria | ✓ |
|
||||||
|
| Backend-only; UI deferred to Phase 3 | Misaligns with Phase 2 success criterion #2 | |
|
||||||
|
|
||||||
|
**User's choice:** Full TOTP enrollment UI in Phase 2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Claude's Discretion
|
||||||
|
|
||||||
|
None — all areas had a clear user preference.
|
||||||
|
|
||||||
|
## Deferred Ideas
|
||||||
|
|
||||||
|
None — discussion stayed within phase scope.
|
||||||
Reference in New Issue
Block a user