docs(08): create phase 8 plan — 8 plans, 3 waves (stack upgrade + backend decomposition)

Wave 0: CR-01/02/03 test stubs + api/schemas.py (CloudConnectionOut migration)
Wave 1: useToastStore stub + Phase 7.1 frontend completion
Wave 2 (parallel): api/admin/, api/documents/, api/auth/ package splits + client.js decomposition + PERF-01 deps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-06-08 14:59:27 +02:00
co-authored by Claude Sonnet 4.6
parent c20a5d8913
commit 25e568973f
12 changed files with 3109 additions and 16 deletions
@@ -0,0 +1,273 @@
---
phase: 08-stack-upgrade-backend-decomposition
plan: 06
type: execute
wave: 2
depends_on: [08-03]
files_modified:
- backend/api/auth/__init__.py
- backend/api/auth/tokens.py
- backend/api/auth/totp.py
- backend/api/auth/password.py
- backend/api/auth/shared.py
- backend/api/auth.py
autonomous: true
requirements: [CODE-03, CODE-08, CR-01, CR-02, CR-03]
tags: [backend-decomposition, auth, sub-router, session-revocation]
must_haves:
truths:
- "backend/api/auth/ is a Python package with sub-modules tokens.py, totp.py, password.py, shared.py and __init__.py"
- "All auth endpoints respond on the same URL paths as before, including rate-limit decorators"
- "limiter is defined in api/auth/shared.py and re-exported from api/auth/__init__.py so `from api.auth import limiter` continues to work in main.py and tests/conftest.py"
- "change_password (in password.py), enable_totp (in totp.py), and disable_totp (in totp.py) preserve the existing revoke_all_refresh_tokens(skip_token_hash=...) call exactly — CR-01/CR-02/CR-03 tests promoted in plan 08-03 must still pass"
- "No sub-router declares a prefix; only api/auth/__init__.py carries prefix=/api/auth"
- "Old backend/api/auth.py monolith is deleted"
- "Test file imports `from api.auth import limiter as auth_limiter` (conftest.py, test_auth_api.py, test_auth_totp.py, test_totp_replay.py, test_security_headers.py) continue to work without modification"
artifacts:
- path: "backend/api/auth/__init__.py"
provides: "Router aggregator with prefix=/api/auth; includes tokens_router, totp_router, password_router; re-exports limiter"
contains: "router = APIRouter(prefix"
- path: "backend/api/auth/tokens.py"
provides: "register, login, refresh, logout, logout-all, me, me/quota, me/preferences handlers"
contains: "async def login"
- path: "backend/api/auth/totp.py"
provides: "totp/setup, totp/enable, totp DELETE (disable) handlers"
contains: "async def enable_totp"
- path: "backend/api/auth/password.py"
provides: "change-password, password-reset, password-reset/confirm handlers"
contains: "async def change_password"
- path: "backend/api/auth/shared.py"
provides: "Limiter instance, request/response Pydantic models, _set_refresh_cookie, _user_dict helpers"
contains: "limiter = Limiter"
key_links:
- from: "backend/main.py"
to: "backend/api/auth/__init__.py"
via: "from api.auth import limiter as auth_limiter AND from api.auth import router as auth_router"
pattern: "from api.auth import (limiter as auth_limiter|router as auth_router)"
- from: "backend/tests/conftest.py and 4 other test files"
to: "backend/api/auth/__init__.py"
via: "from api.auth import limiter as auth_limiter"
pattern: "from api.auth import limiter as auth_limiter"
- from: "password.py change_password handler"
to: "services.auth.revoke_all_refresh_tokens"
via: "skip_token_hash=skip_hash argument"
pattern: "revoke_all_refresh_tokens\\(.*skip_token_hash"
---
<objective>
Decompose the 825-line `backend/api/auth.py` monolith into the focused package `backend/api/auth/` per locked D-07 (4 sub-modules: tokens, totp, password, plus shared.py for cross-module helpers; module names mirror `services/auth.py` logical groupings). The Limiter instance must remain importable as `from api.auth import limiter` to avoid touching 5 test files plus `main.py`. The CR-01/CR-02/CR-03 session-revocation behavior (already implemented and now covered by passing tests from plan 08-03) MUST continue to work unchanged.
Purpose: CODE-03 (decomposition) + CODE-08 (single definition of auth Pydantic request models in `shared.py`) while preserving CR-01/CR-02/CR-03 production behavior and its passing test coverage.
Output: `backend/api/auth/` package; `backend/api/auth.py` monolith deleted; all auth + CR + rate-limit + security-header tests still pass.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/08-stack-upgrade-backend-decomposition/08-CONTEXT.md
@.planning/phases/08-stack-upgrade-backend-decomposition/08-RESEARCH.md
@.planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md
@backend/api/auth.py
@backend/services/auth.py
@backend/main.py
@backend/tests/conftest.py
@CLAUDE.md
<interfaces>
Endpoint to sub-module map (locked per RESEARCH.md §"api/auth.py → api/auth/ package"):
tokens.py: register (POST /register, line 110-188), login (POST /login, line 190-323),
refresh_token (POST /refresh, line 325-387), logout (POST /logout, line 389-422),
logout_all (POST /logout-all, line 424-449),
get_me (GET /me, line 451-457), get_my_quota (GET /me/quota, line 459-475),
get_my_preferences (GET /me/preferences, line 790-806),
update_my_preferences (PATCH /me/preferences, line 808 onwards)
totp.py: totp_setup (GET /totp/setup, line 557-577), enable_totp (POST /totp/enable, line 579-639),
disable_totp (DELETE /totp, line 641-685)
password.py: change_password (POST /change-password, line 477-540),
password_reset_request (POST /password-reset, line 687-720),
password_reset_confirm (POST /password-reset/confirm, line 722-778)
Pydantic model + helper map (shared.py):
shared.py: limiter (the slowapi Limiter instance, line 46),
RegisterRequest (line 51-55), LoginRequest (line 57-63), ChangePasswordRequest (line 65-69),
TotpEnableRequest (line 542-544), PasswordResetRequest (line 546-548),
PasswordResetConfirmRequest (line 550-554), PreferencesUpdate (line 780-787),
_set_refresh_cookie (line 72-94), _user_dict (line 96-105)
Package aggregator pattern for backend/api/auth/__init__.py — note the limiter re-export:
from fastapi import APIRouter
from api.auth.tokens import router as tokens_router
from api.auth.totp import router as totp_router
from api.auth.password import router as password_router
from api.auth.shared import limiter # re-exported so `from api.auth import limiter` still works
router = APIRouter(prefix="/api/auth", tags=["auth"])
router.include_router(tokens_router)
router.include_router(totp_router)
router.include_router(password_router)
Critical: 5 production/test files import `limiter`:
- backend/main.py line 21
- backend/tests/conftest.py line 222
- backend/tests/test_auth_api.py line 99
- backend/tests/test_auth_totp.py line 98
- backend/tests/test_totp_replay.py line 76
- backend/tests/test_security_headers.py line 72
None of these files may be modified by this plan — the `__init__.py` re-export keeps the import path stable.
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create backend/api/auth/ package — shared.py, tokens.py, totp.py, password.py, __init__.py</name>
<files>backend/api/auth/__init__.py, backend/api/auth/shared.py, backend/api/auth/tokens.py, backend/api/auth/totp.py, backend/api/auth/password.py</files>
<read_first>
- backend/api/auth.py lines 1-825 (full source — every endpoint, every Pydantic model, every helper, every import including hashlib for skip_hash computation, time for Redis nbf updates)
- backend/services/auth.py (confirm `revoke_all_refresh_tokens(session, user_id, skip_token_hash=None)` signature; copy the calling pattern verbatim)
- backend/main.py lines 20-30 and 246-260 (confirm `from api.auth import limiter as auth_limiter` and `app.state.limiter = auth_limiter` wiring)
- backend/tests/conftest.py line 220-225 (auth_limiter reset hook)
- .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md sections for `backend/api/auth/*` (exact import lists, rate-limit decorator pattern, ValueError-to-HTTPException bridge, session revocation pattern)
- CLAUDE.md §"Login token hardening" (must preserve: ES256, 15-min access TTL, JTI claim handling, fgp validation, refresh token rotation behavior in tokens.py)
</read_first>
<action>
This task creates the package and ALL five files. Execute in this order so the package is in a valid state on disk after each sub-step:
(A) Create `backend/api/auth/shared.py` first because every other sub-module imports from it. Add `from __future__ import annotations`. Imports: `from typing import Optional`, `from fastapi import Response`, `from pydantic import BaseModel, EmailStr`, `from slowapi import Limiter`, `from config import settings`, `from deps.utils import get_client_ip`. Then declare `limiter = Limiter(key_func=get_client_ip)` (line 46 of monolith). Move these Pydantic models verbatim from monolith: `RegisterRequest` (51-55), `LoginRequest` (57-63 — keep `remember_me: bool = False` field), `ChangePasswordRequest` (65-69), `TotpEnableRequest` (542-544), `PasswordResetRequest` (546-548), `PasswordResetConfirmRequest` (550-554), `PreferencesUpdate` (780-787). Move helpers verbatim: `_set_refresh_cookie(response, raw_token, remember_me=False)` (72-94 — note the remember_me-aware max_age branching from Phase 7.3) and `_user_dict(user)` (96-105).
(B) Create `backend/api/auth/tokens.py`. Header: `from __future__ import annotations`. Imports per PATTERNS.md §"backend/api/auth/tokens.py" plus `from api.auth.shared import limiter, RegisterRequest, LoginRequest, PreferencesUpdate, _set_refresh_cookie, _user_dict`. `router = APIRouter()` — NO prefix (D-04). Move handlers verbatim INCLUDING the `@limiter.limit("10/minute")` decorators: `register` (110-188), `login` (190-323), `refresh_token` (325-387), `logout` (389-422), `logout_all` (424-449), `get_me` (451-457), `get_my_quota` (459-475), `get_my_preferences` (790-806), `update_my_preferences` (808 onwards). The ES256 + JTI + fgp + Redis user_nbf logic in `login`/`refresh_token` is preserved verbatim (CLAUDE.md non-negotiable).
(C) Create `backend/api/auth/totp.py`. Header + imports per PATTERNS.md §"backend/api/auth/totp.py" plus `from api.auth.shared import limiter, TotpEnableRequest, _set_refresh_cookie`. `router = APIRouter()` — NO prefix. Move handlers verbatim: `totp_setup` (557-577), `enable_totp` (579-639), `disable_totp` (641-685). The CR-02 (enable_totp) and CR-03 (disable_totp) skip-hash + revoke_all_refresh_tokens + sessions_revoked logic is preserved verbatim — these handlers are covered by passing tests from plan 08-03, which this plan MUST not break. Copy `hashlib` and `time` imports as needed at the top.
(D) Create `backend/api/auth/password.py`. Header + imports per PATTERNS.md §"backend/api/auth/password.py" plus `from api.auth.shared import limiter, ChangePasswordRequest, PasswordResetRequest, PasswordResetConfirmRequest`. `router = APIRouter()` — NO prefix. Move handlers verbatim: `change_password` (477-540), `password_reset_request` (687-720), `password_reset_confirm` (722-778). The CR-01 (change_password) skip-hash + revoke_all_refresh_tokens + sessions_revoked + Redis user_nbf logic is preserved verbatim from monolith lines 514-540.
(E) Create `backend/api/auth/__init__.py`. Contents:
from fastapi import APIRouter
from api.auth.tokens import router as tokens_router
from api.auth.totp import router as totp_router
from api.auth.password import router as password_router
from api.auth.shared import limiter # re-export
router = APIRouter(prefix="/api/auth", tags=["auth"])
router.include_router(tokens_router)
router.include_router(totp_router)
router.include_router(password_router)
The `from api.auth.shared import limiter` line makes `from api.auth import limiter` work for `main.py` and the 5 test files (because module attribute access resolves through `__init__.py`).
To avoid the package-vs-module name collision, immediately after creating the package directory, rename `backend/api/auth.py` to `backend/api/auth_OLD_REMOVE_IN_TASK_2.py`. Task 2 verifies tests pass and deletes the renamed file.
</action>
<verify>
<automated>cd backend && python -c "from api.auth import router, limiter; print('auth routes:', len(router.routes)); print('limiter type:', type(limiter).__name__)"</automated>
</verify>
<acceptance_criteria>
- Files exist: `backend/api/auth/__init__.py`, `backend/api/auth/shared.py`, `backend/api/auth/tokens.py`, `backend/api/auth/totp.py`, `backend/api/auth/password.py`
- `grep -v '^#' backend/api/auth/__init__.py | grep -c 'router = APIRouter(prefix="/api/auth"'` returns 1
- `grep -v '^#' backend/api/auth/tokens.py | grep -c 'router = APIRouter()'` returns 1 (NO prefix)
- `grep -v '^#' backend/api/auth/totp.py | grep -c 'router = APIRouter()'` returns 1 (NO prefix)
- `grep -v '^#' backend/api/auth/password.py | grep -c 'router = APIRouter()'` returns 1 (NO prefix)
- `grep -c "limiter = Limiter" backend/api/auth/shared.py` returns 1
- `grep -c "from api.auth.shared import limiter" backend/api/auth/__init__.py` returns 1 (re-export)
- `grep -c "skip_token_hash=skip_hash" backend/api/auth/password.py` returns 1 (CR-01 preserved)
- `grep -c "skip_token_hash=skip_hash" backend/api/auth/totp.py` returns 2 (CR-02 enable_totp + CR-03 disable_totp preserved)
- `cd backend && python -c "from api.auth import limiter, router; assert callable(getattr(limiter, 'limit', None)); assert len(router.routes) >= 14"` exits 0 (9 tokens + 3 totp + 3 password = at minimum 14, likely 15)
</acceptance_criteria>
<done>Package exists with all 5 files; limiter re-exported from __init__.py; CR-01/02/03 skip_token_hash calls preserved; monolith renamed (not yet deleted).</done>
</task>
<task type="auto">
<name>Task 2: Run URL + CR regression suite, then delete the old monolith</name>
<files>backend/api/auth.py</files>
<read_first>
- backend/tests/test_auth.py (the three CR tests from plan 08-03)
- backend/tests/test_auth_api.py (rate-limit tests)
- backend/tests/test_auth_totp.py (TOTP integration tests)
- backend/tests/test_totp_replay.py (replay-prevention tests)
- backend/tests/test_security_headers.py (CSP/security headers tests)
- backend/api/auth_OLD_REMOVE_IN_TASK_2.py (renamed monolith)
</read_first>
<action>
Run the affected test files in sequence to confirm decomposition preserves: (1) URL paths, (2) rate-limit behavior (limiter import path), (3) CR-01/CR-02/CR-03 session revocation, (4) TOTP replay prevention, (5) CSP headers.
Command: `cd backend && pytest tests/test_auth.py tests/test_auth_api.py tests/test_auth_totp.py tests/test_totp_replay.py tests/test_security_headers.py -x -v`.
If ANY test fails, diagnose, fix the sub-module, and re-run. Special attention: if a test fails with `ImportError: cannot import name 'limiter' from 'api.auth'`, the `__init__.py` re-export line is missing or misspelled. If a CR test fails, compare the exact `skip_hash` computation block in `password.py` / `totp.py` against the original monolith — every byte must match.
Only after all five test files pass: delete `backend/api/auth_OLD_REMOVE_IN_TASK_2.py`. Re-run `cd backend && pytest -v` to confirm no other tests regressed.
</action>
<verify>
<automated>cd backend && pytest tests/test_auth.py tests/test_auth_api.py tests/test_auth_totp.py tests/test_totp_replay.py tests/test_security_headers.py -x -v 2>&1 | tail -20 && test ! -f backend/api/auth.py && test ! -f backend/api/auth_OLD_REMOVE_IN_TASK_2.py && echo "old monolith deleted"</automated>
</verify>
<acceptance_criteria>
- `test ! -f backend/api/auth.py` exits 0
- `test ! -f backend/api/auth_OLD_REMOVE_IN_TASK_2.py` exits 0
- `test -d backend/api/auth` exits 0
- `cd backend && pytest tests/test_auth.py -x` exits 0 (includes the three promoted CR tests)
- `cd backend && pytest tests/test_auth_api.py -x` exits 0
- `cd backend && pytest tests/test_auth_totp.py -x` exits 0
- `cd backend && pytest tests/test_totp_replay.py -x` exits 0
- `cd backend && pytest tests/test_security_headers.py -x` exits 0
- `cd backend && pytest tests/test_auth.py::test_change_password_revokes_other_sessions tests/test_auth.py::test_enable_totp_revokes_other_sessions tests/test_auth.py::test_disable_totp_revokes_other_sessions -v` shows all three as PASSED
- `cd backend && pytest -v` exits 0 with no new failures vs. baseline
- `grep -rn "^class RegisterRequest" backend/api/auth/` returns exactly one match (shared.py)
- `grep -rn "^class LoginRequest" backend/api/auth/` returns exactly one match (shared.py)
- No file in `backend/tests/` was modified by this plan (test imports still use `from api.auth import limiter`)
</acceptance_criteria>
<done>Old auth.py deleted; full backend suite green; CR-01/02/03 tests still PASSED; rate-limit / TOTP / security headers tests all green; no test file touched.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Unauthenticated client → /api/auth/login, /api/auth/register, /api/auth/refresh | Rate-limited via `@limiter.limit("10/minute")`; limiter must be the SAME instance app.state.limiter references |
| Authenticated user → /api/auth/change-password, /api/auth/totp/enable, /api/auth/totp (DELETE) | CR-01/CR-02/CR-03 require `revoke_all_refresh_tokens` with `skip_token_hash` for the current session — preserved verbatim |
| Authenticated user → /api/auth/refresh | Refresh-token rotation + family-revocation on reuse — preserved verbatim from monolith |
| Authenticated user → /api/auth/logout-all | Revoke all sessions including current; sets Redis user_nbf — preserved verbatim |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-08-06-01 | Spoofing | limiter re-export | mitigate | `__init__.py` re-exports `limiter` from `shared.py` so the SAME Limiter instance is accessible via the legacy import path. Acceptance criterion greps for the re-export line. |
| T-08-06-02 | Repudiation | CR-01/CR-02/CR-03 audit log entries | mitigate | `write_audit_log(..., metadata_={"sessions_revoked": revoked}, ...)` calls in change_password / enable_totp / disable_totp copied verbatim; passing tests from plan 08-03 verify behavior end-to-end. |
| T-08-06-03 | Tampering | Refresh-token rotation logic | mitigate | `refresh_token` handler in tokens.py copied verbatim including JTI, fgp, family-revocation, Redis nbf. |
| T-08-06-04 | Information Disclosure | Sub-router prefix doubling | mitigate | All three sub-routers declare `router = APIRouter()` with NO prefix; only `__init__.py` carries `prefix="/api/auth"`. |
| T-08-06-05 | Denial of Service | Circular import via `__init__.py` | mitigate | `__init__.py` does ONLY router aggregation + limiter re-export; shared models in `shared.py`. |
| T-08-06-06 | Elevation of Privilege | Session revocation skip | mitigate | `skip_token_hash=skip_hash` argument preserved in all three CR handlers; grep acceptance criterion counts occurrences. |
| T-08-06-SC | Supply Chain | No new packages | accept | This plan installs zero new packages. |
</threat_model>
<verification>
- `cd backend && pytest tests/test_auth.py tests/test_auth_api.py tests/test_auth_totp.py tests/test_totp_replay.py tests/test_security_headers.py -x -v` — zero failures
- `cd backend && pytest -v` — full suite zero failures
- `cd backend && python -c "from main import app; auth_paths = sorted({r.path for r in app.routes if r.path.startswith('/api/auth')}); print('\\n'.join(auth_paths))"` lists at minimum: `/api/auth/register`, `/api/auth/login`, `/api/auth/refresh`, `/api/auth/logout`, `/api/auth/logout-all`, `/api/auth/me`, `/api/auth/me/quota`, `/api/auth/me/preferences`, `/api/auth/totp/setup`, `/api/auth/totp/enable`, `/api/auth/totp`, `/api/auth/change-password`, `/api/auth/password-reset`, `/api/auth/password-reset/confirm`
- `cd backend && python -c "from main import app; from api.auth import limiter; assert app.state.limiter is limiter"` — confirms identity (same instance, not a copy)
</verification>
<success_criteria>
- `backend/api/auth/` package with 5 files
- `backend/api/auth.py` deleted
- All auth URL paths unchanged
- limiter still importable via `from api.auth import limiter` (5 test files + main.py unchanged)
- CR-01/CR-02/CR-03 tests still PASSED (not regressed by decomposition)
- All tests pass
</success_criteria>
<output>
Create `.planning/phases/08-stack-upgrade-backend-decomposition/08-06-SUMMARY.md` when done. Include: (a) full list of auth paths emitted by `app.routes` before vs. after (must be identical), (b) test counts for each of the 5 covered files, (c) confirmation that `app.state.limiter is api.auth.limiter` (identity check), (d) confirmation that no file under `backend/tests/` was modified.
</output>