diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index a40150a..70740ab 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -553,7 +553,31 @@ _Started: 2026-06-07_ 2. `import * as api from '../api/client.js'` in any existing consumer file resolves all previously-exported names without modification to that consumer file 3. No Pydantic model definition appears more than once across `api/admin/`, `api/documents/`, and `api/auth/` sub-packages — shared types live in a dedicated module 4. `PERF-01` packages are installed and `npm list` confirms `vue@^3.5.x`, `vite@^6.x`, `@vueuse/core@^14.x`, `sortablejs`, `@tailwindcss/forms`, `rollup-plugin-visualizer` are present -**Plans**: TBD +**Plans**: 8 plans (3 waves) + +**Wave 0** — Foundation (parallel) + +- [ ] 08-01-PLAN.md — CR-01/02/03 test stubs (3 xfail stubs in test_auth.py) + Wave 0 scaffolds for regression detection +- [ ] 08-02-PLAN.md — `api/schemas.py` creation + `CloudConnectionOut` migration from admin.py (MUST precede admin split) + +**Wave 1** *(blocked on Wave 0)* — Phase 7.1 completion (frontend only — backend already implemented) + +- [ ] 08-03-PLAN.md — `useToastStore` stub (`frontend/src/stores/toast.js`) + CR test promotion + SettingsAccountTab.vue + TotpEnrollment.vue inline toast replacement + +**Wave 2** *(blocked on Wave 1)* — Backend decomposition + frontend (parallel) + +- [ ] 08-04-PLAN.md — Split `api/admin.py` → `api/admin/` package: users.py, quotas.py, ai.py, shared.py, __init__.py (CODE-01) +- [ ] 08-05-PLAN.md — Split `api/documents.py` → `api/documents/` package: upload.py, crud.py, content.py, shared.py, __init__.py (CODE-02) +- [ ] 08-06-PLAN.md — Split `api/auth.py` → `api/auth/` package: tokens.py, totp.py, password.py, sessions.py, shared.py, __init__.py (CODE-03) +- [ ] 08-07-PLAN.md — Frontend `client.js` decomposition: utils.js + 7 domain modules + barrel rewrite (CODE-04) +- [ ] 08-08-PLAN.md — PERF-01 dependency bump + tailwind/vite config wiring + requirements.txt exact pinning (PERF-01, D-17) + +**Cross-cutting constraints:** + +- Sub-routers MUST have NO prefix on `APIRouter()` — only package `__init__.py` carries the prefix (Plans 04, 05, 06) +- `api/schemas.py` must exist before admin split — `api/cloud.py` imports `CloudConnectionOut` from admin (Plan 02 before Plan 04) +- `request()` moves to `utils.js` for circular-import avoidance; `client.js` re-exports it — zero consumer changes (Plan 07) +- Every admin sub-module handler retains `Depends(get_current_admin)` explicitly — never omit (Plans 04) --- diff --git a/.planning/STATE.md b/.planning/STATE.md index dbfa283..f0748bd 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,14 +2,14 @@ gsd_state_version: 1.0 milestone: v0.2 milestone_name: Phases -current_phase: Phase 8 (context gathered) -status: planning -last_updated: "2026-06-07" -last_activity: 2026-06-07 — Phase 8 context gathered +current_phase: "Phase 8: Stack Upgrade & Backend Decomposition" +status: planned +last_updated: "2026-06-08T00:00:00.000Z" +last_activity: 2026-06-08 — Phase 8 planned (8 plans, 3 waves) progress: total_phases: 17 completed_phases: 13 - total_plans: 65 + total_plans: 73 completed_plans: 65 percent: 76 --- @@ -17,22 +17,22 @@ progress: # Project State **Project:** DocuVault -**Status:** Phase 8 context gathered — ready to plan +**Status:** Phase 8 planned — ready to execute **Current Phase:** Phase 8: Stack Upgrade & Backend Decomposition -**Last Updated:** 2026-06-07 +**Last Updated:** 2026-06-08 ## Current Position -Phase: Phase 8 (context gathered, not started) +Phase: Phase 8 (planned, 8 plans ready) Plan: — -Status: Context captured — 8 areas discussed, 17 decisions locked -Last activity: 2026-06-07 — Phase 8 context gathered +Status: Ready to execute — 8 plans across 3 waves +Last activity: 2026-06-08 — Phase 8 planned ## Phase Status | Phase | Requirements | Status | |-------|-------------|--------| -| 8. Stack Upgrade & Backend Decomposition | PERF-01, CODE-01, CODE-02, CODE-03, CODE-04, CODE-08 | Not started | +| 8. Stack Upgrade & Backend Decomposition | PERF-01, CODE-01, CODE-02, CODE-03, CODE-04, CODE-08 | Planned (8 plans) | | 9. Admin Panel Rearchitecture | ADMIN-08..12, CODE-06, CODE-09 | Not started | | 10. UX & Interaction | UX-01..14, CODE-05 | Not started | | 11. Visual Design, Responsive Layout & Cleanup | VISUAL-01..04, RESP-01..05, CODE-07, PERF-02, PERF-03 | Not started | @@ -43,7 +43,7 @@ Last activity: 2026-06-07 — Phase 8 context gathered |---|---| | Phases complete | 0 / 4 | | Requirements mapped | 40 / 40 | -| Plans written | 0 | +| Plans written | 8 | | Plans complete | 0 | ## Accumulated Context @@ -82,7 +82,7 @@ _Updated at each phase transition._ | Field | Value | |---|---| -| Last session | 2026-06-07 — v0.2 roadmap created; 4 phases defined | -| Next action | /gsd:plan-phase 8 | +| Last session | 2026-06-08 — Phase 8 planned; 8 plans created | +| Next action | /gsd:execute-phase 8 | | Pending decisions | None | -| Resume file | .planning/ROADMAP.md — Phase 8 detail section | +| Resume file | .planning/phases/08-stack-upgrade-backend-decomposition/ | diff --git a/.planning/phases/08-stack-upgrade-backend-decomposition/08-01-PLAN.md b/.planning/phases/08-stack-upgrade-backend-decomposition/08-01-PLAN.md new file mode 100644 index 0000000..7add89f --- /dev/null +++ b/.planning/phases/08-stack-upgrade-backend-decomposition/08-01-PLAN.md @@ -0,0 +1,129 @@ +--- +phase: 08-stack-upgrade-backend-decomposition +plan: 01 +type: execute +wave: 0 +depends_on: [] +files_modified: + - backend/tests/test_auth.py +autonomous: true +requirements: [CR-01, CR-02, CR-03] +tags: [tests, session-revocation, wave-0] +must_haves: + truths: + - "Three new pytest tests exist for CR-01, CR-02, CR-03 as xfail stubs" + - "Stubs name the exact behavior they cover so executor of plan 08-03 can promote them" + - "Running pytest -v shows three new tests with status xfail (not error, not pass)" + artifacts: + - path: "backend/tests/test_auth.py" + provides: "Three new xfail test stubs for session revocation on privilege change" + contains: "test_change_password_revokes_other_sessions" + key_links: + - from: "backend/tests/test_auth.py" + to: "backend/api/auth.py change_password / enable_totp / disable_totp" + via: "test exercises real handler via httpx.AsyncClient" + pattern: "client.post\\(\"/api/auth/(change-password|totp/enable|totp)\"" +--- + + +Create Wave 0 test stubs (xfail) for CR-01, CR-02, CR-03. These tests will be promoted to passing in plan 08-03 after the `useToastStore` stub and frontend wiring are complete. The backend implementation for all three is ALREADY in place (verified in RESEARCH.md §"Wave 1: Session Revocation"); these stubs lock the expected behavior contract before any refactoring touches `api/auth.py`. + +Purpose: Anti-regression Nyquist scaffold — when plan 08-06 splits `api/auth.py` into `api/auth/` package, these promoted tests guarantee the session revocation logic still works through the new module structure. + +Output: Three xfail-marked tests added to `backend/tests/test_auth.py`. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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-VALIDATION.md +@backend/api/auth.py +@backend/services/auth.py +@backend/tests/test_auth.py +@backend/tests/conftest.py + + + + + + Task 1: Add three xfail stubs for CR-01/CR-02/CR-03 to test_auth.py + backend/tests/test_auth.py + + - backend/tests/test_auth.py (read the full file to discover existing fixtures, login helper pattern, and how authenticated requests are issued) + - backend/tests/conftest.py (locate `auth_user` fixture, `client` fixture, and `auth_limiter` reset hook) + - backend/api/auth.py lines 478-540 (change_password — confirm response shape includes `sessions_revoked`) + - backend/api/auth.py lines 579-636 (enable_totp — confirm response shape) + - backend/api/auth.py lines 641-682 (disable_totp — confirm response shape) + - backend/services/auth.py lines 250-273 (revoke_all_refresh_tokens signature with skip_token_hash) + + + - Test `test_change_password_revokes_other_sessions`: register user, log in twice to obtain TWO refresh-token rows in DB (call them session A and session B). Using session A's access token, POST `/api/auth/change-password` with the correct current password and a new valid password. Assert: response 200, body contains `"sessions_revoked": 1` (session B revoked, session A preserved via `skip_token_hash`); session A's refresh token is still usable on POST `/api/auth/refresh`; session B's refresh token fails on POST `/api/auth/refresh` with 401. + - Test `test_enable_totp_revokes_other_sessions`: register user, log in twice (sessions A and B). Using session A, POST `/api/auth/totp/setup` to obtain a `provisioning_uri`, derive a valid TOTP code via `pyotp.TOTP(secret).now()`, POST `/api/auth/totp/enable` with that code. Assert: response 200, body contains `"sessions_revoked": 1`, session B refresh fails 401, session A refresh succeeds. + - Test `test_disable_totp_revokes_other_sessions`: register user, enable TOTP, then log in twice with TOTP (sessions A and B). Using session A, DELETE `/api/auth/totp` with the current TOTP code. Assert: response 200, body contains `"sessions_revoked": 1`, session B refresh fails 401, session A refresh succeeds. + + +Append three test functions to `backend/tests/test_auth.py`, each decorated with `@pytest.mark.xfail(reason="Wave 0 stub — promoted to passing in 08-03", strict=False)`. Use the existing test patterns in the file (httpx.AsyncClient async fixtures, `auth_user` fixture from conftest, `await client.post(...)`). The three function names MUST be exactly: + - `async def test_change_password_revokes_other_sessions(client, db_session)` + - `async def test_enable_totp_revokes_other_sessions(client, db_session)` + - `async def test_disable_totp_revokes_other_sessions(client, db_session)` +Inside each test body, write the full assertion logic per the `` block — do NOT leave them as bare `pass` stubs. The tests SHOULD pass right now (backend already implemented per RESEARCH.md), but `strict=False` xfail allows either xpassed or xfailed without erroring the suite. Plan 08-03 will remove the `@pytest.mark.xfail` decorator and confirm they pass cleanly. Use `pyotp.TOTP(secret).now()` for TOTP code generation; import pyotp at the top of the test file if not already imported. For the login-twice pattern, use distinct `User-Agent` headers on each login to ensure separate refresh-token rows; capture `response.cookies['refresh_token']` for each session. + + + 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 --tb=no -q + + + - `grep -c "def test_change_password_revokes_other_sessions" backend/tests/test_auth.py` returns 1 + - `grep -c "def test_enable_totp_revokes_other_sessions" backend/tests/test_auth.py` returns 1 + - `grep -c "def test_disable_totp_revokes_other_sessions" backend/tests/test_auth.py` returns 1 + - `grep -c "pytest.mark.xfail" backend/tests/test_auth.py` increased by at least 3 vs. pre-change baseline + - Pytest output for these three test IDs shows status `XPASS` or `XFAIL` — never `ERROR` and never `FAILED` + - Body of each test asserts `data["sessions_revoked"] == 1` (not `>= 1`, not `is not None`) + - Body of each test verifies the OTHER session's refresh token returns 401 on `/api/auth/refresh` + - Body of each test verifies the CURRENT session's refresh token returns 200 on `/api/auth/refresh` + + Three xfail-marked tests appended to test_auth.py with full assertion logic exercising the documented CR-01/02/03 contracts; pytest collects and runs them without error. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| client → POST /api/auth/change-password | Authenticated user requests password change; backend must revoke all OTHER refresh tokens | +| client → POST /api/auth/totp/enable | Authenticated user enables TOTP; backend must revoke all OTHER refresh tokens | +| client → DELETE /api/auth/totp | Authenticated user disables TOTP; backend must revoke all OTHER refresh tokens | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-08-01-01 | Tampering | Test fixture isolation | mitigate | Each test creates its own user via `auth_user` fixture; tests do not share session state | +| T-08-01-02 | Repudiation | xfail strict mode | mitigate | `strict=False` permits XPASS without erroring; plan 08-03 removes the marker and runs strict | +| T-08-01-SC | Supply Chain | pytest, pyotp | accept | Already pinned in requirements.txt; this plan adds no new packages | + + + +- `cd backend && pytest tests/test_auth.py --tb=no -q` shows three new tests with xpassed/xfailed status (no errors) +- Full backend suite still green: `cd backend && pytest -v` — zero new failures vs. baseline +- File diff shows only additions to `backend/tests/test_auth.py` (no other files touched) + + + +- Three new xfail tests exist with the exact names listed +- Each test body contains real assertion logic (not `pass` or `pytest.skip`) +- Pytest collects all three without error + + + +Create `.planning/phases/08-stack-upgrade-backend-decomposition/08-01-SUMMARY.md` when done. Include: which fixtures were used, any helper functions added, the exact xfail decorator reason string, and the pre-/post-stub `pytest --co` count. + diff --git a/.planning/phases/08-stack-upgrade-backend-decomposition/08-02-PLAN.md b/.planning/phases/08-stack-upgrade-backend-decomposition/08-02-PLAN.md new file mode 100644 index 0000000..e273581 --- /dev/null +++ b/.planning/phases/08-stack-upgrade-backend-decomposition/08-02-PLAN.md @@ -0,0 +1,172 @@ +--- +phase: 08-stack-upgrade-backend-decomposition +plan: 02 +type: execute +wave: 0 +depends_on: [] +files_modified: + - backend/api/schemas.py + - backend/api/cloud.py +autonomous: true +requirements: [CODE-08] +tags: [shared-schemas, cross-package, prerequisite, wave-0] +must_haves: + truths: + - "backend/api/schemas.py exists and defines CloudConnectionOut with field_validator coerce_id_to_str" + - "backend/api/cloud.py imports CloudConnectionOut from api.schemas (not api.admin)" + - "All cloud endpoints continue to return the same JSON shape as before" + - "CloudConnectionOut definition appears exactly once across the entire backend tree" + artifacts: + - path: "backend/api/schemas.py" + provides: "Cross-package Pydantic response models (D-10 destination for shared schemas)" + contains: "class CloudConnectionOut" + - path: "backend/api/cloud.py" + provides: "Cloud endpoint router updated to import from api.schemas" + contains: "from api.schemas import CloudConnectionOut" + key_links: + - from: "backend/api/cloud.py" + to: "backend/api/schemas.py" + via: "import statement at module top" + pattern: "from api.schemas import CloudConnectionOut" + - from: "backend/api/admin.py" + to: "backend/api/schemas.py" + via: "import statement to be added in plan 08-04 admin split" + pattern: "from api.schemas import CloudConnectionOut" +--- + + +Create the new `backend/api/schemas.py` module and move `CloudConnectionOut` into it. Update `backend/api/cloud.py` to import from the new location. This MUST happen BEFORE plan 08-04 splits `api/admin.py` — otherwise the admin-split plan would break `cloud.py`'s `from api.admin import CloudConnectionOut` import. + +Purpose: Eliminate the cross-package coupling between `api/cloud.py` and `api/admin.py` (RESEARCH.md §"Pitfall 3"). Establishes the `api/schemas.py` module that plan 08-04 will continue populating with any models discovered to be shared. + +Output: New `backend/api/schemas.py` with `CloudConnectionOut`; one updated import in `backend/api/cloud.py`. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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/admin.py +@backend/api/cloud.py + + + + + +class CloudConnectionOut(BaseModel): + id: str + provider: str + display_name: str + status: str + connected_at: datetime + server_url: Optional[str] = None + connection_username: Optional[str] = None + model_config = {"from_attributes": True} + + @field_validator("id", mode="before") + @classmethod + def coerce_id_to_str(cls, v) -> str: + return str(v) + + +from api.admin import CloudConnectionOut + + +from api.schemas import CloudConnectionOut + + + + + + + Task 1: Create backend/api/schemas.py with CloudConnectionOut + backend/api/schemas.py + + - backend/api/admin.py lines 198-223 (current CloudConnectionOut definition — must be copied verbatim including the field_validator) + - backend/api/__init__.py (confirm it exists as a package marker; do not modify) + - .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md section "backend/api/schemas.py" (exact pattern template) + + +Create `backend/api/schemas.py` as a new file. Add a `from __future__ import annotations` header. Add a module docstring: `"""Cross-package Pydantic response models. Models here are used by 2+ API packages and cannot live in a single package without creating circular imports (D-10, RESEARCH.md Pitfall 3)."""`. Add imports: `from datetime import datetime`, `from typing import Optional`, `from pydantic import BaseModel, field_validator`. Then define `class CloudConnectionOut(BaseModel)` with the exact field set from `backend/api/admin.py` lines ~198-223 (per D-10): `id: str`, `provider: str`, `display_name: str`, `status: str`, `connected_at: datetime`, `server_url: Optional[str] = None`, `connection_username: Optional[str] = None`, `model_config = {"from_attributes": True}`, and the `@field_validator("id", mode="before") coerce_id_to_str` classmethod that returns `str(v)`. Add a class-level docstring noting SEC-08: `credentials_enc` deliberately excluded, moved from `api/admin.py`, used by `api/cloud.py` and `api/admin/`. Do NOT remove the class from `api/admin.py` in this task — task 2 handles the import switch in cloud.py, and plan 08-04 will delete the original definition during the admin split. + + + cd backend && python -c "from api.schemas import CloudConnectionOut; obj = CloudConnectionOut.model_validate({'id': 'abc-123', 'provider': 'google_drive', 'display_name': 'Test', 'status': 'ACTIVE', 'connected_at': '2026-06-07T00:00:00'}); assert obj.id == 'abc-123' and obj.provider == 'google_drive'" + + + - File `backend/api/schemas.py` exists + - `grep -c "^class CloudConnectionOut" backend/api/schemas.py` returns 1 + - `grep -c "coerce_id_to_str" backend/api/schemas.py` returns 1 + - `grep -c "from_attributes" backend/api/schemas.py` returns 1 + - `python -c "from api.schemas import CloudConnectionOut"` from inside `backend/` exits 0 with no output + - All seven fields (id, provider, display_name, status, connected_at, server_url, connection_username) are present per `grep -c " [a-z_]*: " backend/api/schemas.py` ≥ 7 + + schemas.py exists, importable, CloudConnectionOut validates a sample dict, original admin.py class is still in place (deletion deferred to plan 08-04). + + + + Task 2: Switch backend/api/cloud.py import to api.schemas + backend/api/cloud.py + + - backend/api/cloud.py line 35 (current `from api.admin import CloudConnectionOut`) + - backend/api/cloud.py top imports block (confirm no other re-exports from `api.admin` exist) + + +In `backend/api/cloud.py`, replace the single line `from api.admin import CloudConnectionOut` with `from api.schemas import CloudConnectionOut`. Do not change any other imports or any code below. After this edit, run the full test suite to confirm cloud endpoints still respond with the identical JSON shape (existing `tests/test_cloud.py` covers admin response surface; if not, the broader `pytest -v` smoke covers the admin list-cloud-connections endpoint too). + + + cd backend && grep -c "from api.admin import CloudConnectionOut" api/cloud.py; cd backend && grep -c "from api.schemas import CloudConnectionOut" api/cloud.py; cd backend && pytest tests/test_cloud.py -x -v 2>&1 | tail -20 + + + - `grep -c "from api.admin import CloudConnectionOut" backend/api/cloud.py` returns 0 + - `grep -c "from api.schemas import CloudConnectionOut" backend/api/cloud.py` returns 1 + - `cd backend && pytest tests/test_cloud.py -x` exits 0 + - `cd backend && pytest tests/test_admin.py -x -k "cloud"` exits 0 (admin endpoints that serialize CloudConnectionOut continue to work because the original definition in admin.py is still untouched at this point) + + cloud.py imports CloudConnectionOut from api.schemas; tests for cloud endpoints and any admin endpoints that surface cloud connections continue to pass. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| api/cloud.py → api/schemas.py | Module-load-time import; no runtime data crosses this boundary other than a class reference | +| Admin list-cloud-connections endpoint → CloudConnectionOut serialization | SEC-08 requires `credentials_enc` to be deliberately excluded from this model | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-08-02-01 | Information Disclosure | CloudConnectionOut schema | mitigate | Field list must NOT include `credentials_enc`; field set is restricted to the 7 fields documented in PATTERNS.md | +| T-08-02-02 | Tampering | id coercion | mitigate | Preserve `@field_validator("id", mode="before") coerce_id_to_str` so UUID→str conversion behavior is byte-identical to the old definition | +| T-08-02-03 | Denial of Service | Duplicate class definitions | mitigate | Plan 08-04 removes the old definition from api/admin.py during the admin split; until then both definitions coexist but only the schemas.py version is imported by cloud.py | +| T-08-02-SC | Supply Chain | No new packages | accept | This plan installs zero new packages | + + + +- `cd backend && pytest tests/test_cloud.py tests/test_admin.py -x` — zero failures +- `grep -rn "from api.admin import CloudConnectionOut" backend/` returns no matches (cloud.py was the only consumer) +- `grep -rn "from api.schemas import CloudConnectionOut" backend/` returns exactly one match (cloud.py) +- `backend/api/admin.py` still contains the original `class CloudConnectionOut` definition (deletion deferred to plan 08-04) + + + +- `backend/api/schemas.py` exists with `CloudConnectionOut` defined correctly +- `backend/api/cloud.py` imports from `api.schemas` +- Cloud and admin tests continue to pass +- No other file is modified + + + +Create `.planning/phases/08-stack-upgrade-backend-decomposition/08-02-SUMMARY.md` when done. Include: confirmation that the original class in admin.py is left untouched, the exact `pytest tests/test_cloud.py tests/test_admin.py -v` summary line, and a note that plan 08-04 owns the deletion of the old admin.py definition. + diff --git a/.planning/phases/08-stack-upgrade-backend-decomposition/08-03-PLAN.md b/.planning/phases/08-stack-upgrade-backend-decomposition/08-03-PLAN.md new file mode 100644 index 0000000..697d4ff --- /dev/null +++ b/.planning/phases/08-stack-upgrade-backend-decomposition/08-03-PLAN.md @@ -0,0 +1,228 @@ +--- +phase: 08-stack-upgrade-backend-decomposition +plan: 03 +type: execute +wave: 1 +depends_on: [08-01] +files_modified: + - frontend/src/stores/toast.js + - frontend/src/components/settings/SettingsAccountTab.vue + - frontend/src/components/auth/TotpEnrollment.vue + - backend/tests/test_auth.py +autonomous: true +requirements: [CR-01, CR-02, CR-03] +tags: [phase-7.1, toast-stub, session-revocation, frontend] +must_haves: + truths: + - "frontend/src/stores/toast.js exists, exports useToastStore, exposes show(message, type, duration)" + - "show() is a silent no-op in this phase (Phase 10 will render); calling it never throws" + - "SettingsAccountTab.vue calls toastStore.show('Other sessions have been terminated.', 'success') when data.sessions_revoked > 0 in both changePassword and disableTotp handlers" + - "TotpEnrollment.vue calls toastStore.show('Other sessions have been terminated.', 'success') when data.sessions_revoked > 0 in confirmEnrollment handler" + - "Inline sessionRevokedToast ref + setTimeout + inline toast template blocks are removed from both components" + - "CR-01, CR-02, CR-03 tests run as plain passing tests (xfail decorator removed)" + artifacts: + - path: "frontend/src/stores/toast.js" + provides: "Pinia toast store stub with the show() contract Phase 10 must honor" + contains: "export const useToastStore" + - path: "frontend/src/components/settings/SettingsAccountTab.vue" + provides: "Refactored to use toastStore.show() in changePassword + disableTotp handlers" + contains: "toastStore.show" + - path: "frontend/src/components/auth/TotpEnrollment.vue" + provides: "Refactored to use toastStore.show() in confirmEnrollment handler" + contains: "toastStore.show" + - path: "backend/tests/test_auth.py" + provides: "Three session-revocation tests promoted from xfail to passing" + contains: "test_change_password_revokes_other_sessions" + key_links: + - from: "SettingsAccountTab.vue changePassword handler" + to: "useToastStore.show" + via: "function call when data.sessions_revoked > 0" + pattern: "toastStore\\.show\\(['\"]Other sessions have been terminated" + - from: "TotpEnrollment.vue confirmEnrollment handler" + to: "useToastStore.show" + via: "function call when data.sessions_revoked > 0" + pattern: "toastStore\\.show\\(['\"]Other sessions have been terminated" +--- + + +Complete the absorbed Phase 7.1 work. The backend code for CR-01/CR-02/CR-03 is already implemented in `backend/api/auth.py` (RESEARCH.md confirmed lines 518, 615, 662). This plan: (1) creates the `useToastStore` Pinia stub per the UI-SPEC.md contract, (2) replaces the inline `sessionRevokedToast` ref + `setTimeout` pattern in `SettingsAccountTab.vue` and `TotpEnrollment.vue` with `toastStore.show(...)` calls, (3) removes the inline toast HTML blocks from both components, and (4) promotes the three CR xfail stubs from plan 08-01 to passing tests. + +Purpose: Ship CR-01/CR-02/CR-03 to the user-visible behavior contract that the UI-SPEC.md locks (toast on `sessions_revoked > 0`), with the call signature Phase 10 must honor without modifying any Phase 8 call site. + +Output: New toast store, refactored two Vue components, promoted three backend tests. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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 +@.planning/phases/08-stack-upgrade-backend-decomposition/08-UI-SPEC.md +@frontend/src/stores/topics.js +@frontend/src/components/settings/SettingsAccountTab.vue +@frontend/src/components/auth/TotpEnrollment.vue +@backend/tests/test_auth.py + + + +toastStore.show(message: string, type: 'success'|'error'|'info' = 'success', duration: number = 4000): void + + +SettingsAccountTab.vue / changePassword() → on data.sessions_revoked > 0 → toastStore.show('Other sessions have been terminated.', 'success') +SettingsAccountTab.vue / disableTotp() → on data.sessions_revoked > 0 → toastStore.show('Other sessions have been terminated.', 'success') +TotpEnrollment.vue / confirmEnrollment() → on data.sessions_revoked > 0 → toastStore.show('Other sessions have been terminated.', 'success') + + +SettingsAccountTab.vue: lines 6, 17, 211, 226-227, 263-264 (sessionRevokedToast ref + setTimeout + inline HTML block) +TotpEnrollment.vue: lines 6, 15, 149, 175-176 (sessionRevokedToast ref + setTimeout + inline HTML block) + + + + + + + Task 1: Create useToastStore Pinia stub at frontend/src/stores/toast.js + frontend/src/stores/toast.js + + - frontend/src/stores/topics.js (simplest existing Pinia store — copy the setup-store style with defineStore + named function exports) + - .planning/phases/08-stack-upgrade-backend-decomposition/08-UI-SPEC.md §"useToastStore API Contract" (exact signature, default values, "MUST NOT" rules) + - .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md §"frontend/src/stores/toast.js" (full stub template) + + + - Importing `useToastStore` from `'../stores/toast.js'` resolves successfully + - Calling `useToastStore().show('hello')` returns `undefined` and does not throw + - Calling `useToastStore().show('hello', 'error')` returns `undefined` and does not throw + - Calling `useToastStore().show('hello', 'success', 8000)` returns `undefined` and does not throw + - The store does not render any DOM element (verified visually — Phase 10 implements rendering) + + +Create `frontend/src/stores/toast.js`. Use the setup-store style (`defineStore('toast', () => { ... })`). Define a single function `show(message, type = 'success', duration = 4000)` with positional parameters only (the UI-SPEC explicitly forbids object-argument shape `show({message, type})`). The function body is empty (no-op stub). Return `{ show }` so the store exposes the method. Add a top-of-file docstring (JS comment block) stating: Phase 7.1 STUB — Phase 10 (UX-10) fills in the full implementation; the signature `show(message, type, duration)` is locked and Phase 10 must honor it without modifying Phase 8 call sites. Default values match UI-SPEC.md: `type = 'success'` (NOT `'info'`), `duration = 4000`. Export as named export: `export const useToastStore = defineStore(...)`. Do NOT import or depend on any component. + + + cd frontend && node -e "import('./src/stores/toast.js').then(m => { const s = m.useToastStore; if (typeof s !== 'function') throw new Error('useToastStore not a function'); console.log('ok'); })" 2>&1 | tail -5 + + + - File `frontend/src/stores/toast.js` exists + - `grep -c "export const useToastStore" frontend/src/stores/toast.js` returns 1 + - `grep -c "defineStore('toast'" frontend/src/stores/toast.js` returns 1 + - `grep -c "function show(message, type = 'success', duration = 4000)" frontend/src/stores/toast.js` returns 1 + - `cd frontend && npm test -- --run stores/toast 2>/dev/null || true` does not throw an import error + - The file contains NO `import` of any Vue component or DOM API + + toast.js exists with the exact UI-SPEC signature; the module imports cleanly; show() is a silent no-op. + + + + Task 2: Refactor SettingsAccountTab.vue and TotpEnrollment.vue to use toastStore + frontend/src/components/settings/SettingsAccountTab.vue, frontend/src/components/auth/TotpEnrollment.vue + + - frontend/src/components/settings/SettingsAccountTab.vue (full file — current inline toast HTML at lines 5-25, `sessionRevokedToast` ref at line 211, setTimeouts in changePassword at 225-228 and disableTotp at 261-264) + - frontend/src/components/auth/TotpEnrollment.vue (full file — current inline toast HTML at lines 5-23, `sessionRevokedToast` ref at line 149, setTimeout in confirmEnrollment at 174-177) + - frontend/src/stores/toast.js (the stub created in task 1) + - .planning/phases/08-stack-upgrade-backend-decomposition/08-UI-SPEC.md §"Sessions-Revoked Notification — Interaction Contract" (message copy locked exactly: 'Other sessions have been terminated.') + + +For `frontend/src/components/settings/SettingsAccountTab.vue`: + 1. Delete the inline `
...
` template block (lines ~5-25 — the fixed-position toast). + 2. Delete the line `const sessionRevokedToast = ref(false)` (~line 211). + 3. In the `changePassword` handler (~lines 220-230), replace `sessionRevokedToast.value = true; setTimeout(() => { sessionRevokedToast.value = false }, 5000)` with `toastStore.show('Other sessions have been terminated.', 'success')`. Keep the surrounding `if (data.sessions_revoked > 0) { ... }` guard. + 4. In the `disableTotp` handler (~lines 258-267), apply the same replacement. + 5. Add `import { useToastStore } from '../../stores/toast.js'` at the top with the other imports. + 6. Add `const toastStore = useToastStore()` near the other store/ref declarations in `