From ba30ada87a0c3fbea8d12cfecb2fe554b421899a Mon Sep 17 00:00:00 2001 From: curo1305 Date: Fri, 12 Jun 2026 15:34:31 +0200 Subject: [PATCH] =?UTF-8?q?docs(09):=20create=20phase=209=20plan=20?= =?UTF-8?q?=E2=80=94=20Admin=20Panel=20Rearchitecture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5 plans across 4 waves covering ADMIN-08..12, CODE-06, CODE-09: - 09-01 (W1): backend GET /api/admin/overview + 8 ADMIN-11 tests - 09-02 (W1): AdminLayout + AdminSidebar + AdminOverviewView + getAdminOverview API - 09-03 (W2): extract 4 admin tab components to standalone views - 09-04 (W3): nested /admin route + to.matched.some guard + D-08/D-09/D-10 redirects + Tailwind safelist (sky+amber) + login redirect + delete AdminView/tabs - 09-05 (W4): CODE-09 comment purge for Phase 9 + retroactive Phase 8 backend sub-packages + human UAT checkpoint All locked decisions D-01..D-16 mapped to tasks; safelist corrected to include sky (OneDrive) and amber (audit badges) per RESEARCH.md Pattern 7. Co-Authored-By: Claude Sonnet 4.6 --- .planning/ROADMAP.md | 23 +- .../09-01-PLAN.md | 191 +++++ .../09-02-PLAN.md | 242 ++++++ .../09-03-PLAN.md | 174 +++++ .../09-04-PLAN.md | 249 +++++++ .../09-05-PLAN.md | 258 +++++++ .../09-PATTERNS.md | 700 ++++++++++++++++++ .../09-VALIDATION.md | 62 ++ 8 files changed, 1897 insertions(+), 2 deletions(-) create mode 100644 .planning/phases/09-admin-panel-rearchitecture/09-01-PLAN.md create mode 100644 .planning/phases/09-admin-panel-rearchitecture/09-02-PLAN.md create mode 100644 .planning/phases/09-admin-panel-rearchitecture/09-03-PLAN.md create mode 100644 .planning/phases/09-admin-panel-rearchitecture/09-04-PLAN.md create mode 100644 .planning/phases/09-admin-panel-rearchitecture/09-05-PLAN.md create mode 100644 .planning/phases/09-admin-panel-rearchitecture/09-PATTERNS.md create mode 100644 .planning/phases/09-admin-panel-rearchitecture/09-VALIDATION.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index afa0704..8d3d0d9 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -601,7 +601,26 @@ _Started: 2026-06-07_ 3. The browser back button moves from `/admin/audit` to `/admin/users` (or whichever path was previous) without a full page reload 4. `AdminView.vue` is deleted from the repository; no file imports it 5. A production build (`npm run build`) renders topic badge and provider chip colors correctly — no gray/invisible badges caused by purged dynamic Tailwind classes -**Plans**: TBD + +**Plans**: 5 plans (4 waves) + +**Wave 1** — Foundation (parallel) + +- [ ] 09-01-PLAN.md — Backend overview.py endpoint + 8 ADMIN-11 tests (overview aggregate query + security invariants) +- [ ] 09-02-PLAN.md — Frontend AdminLayout + AdminSidebar + AdminOverviewView + getAdminOverview API client + +**Wave 2** *(blocked on 09-02)* + +- [ ] 09-03-PLAN.md — Extract 4 admin tab components to standalone views (AdminUsersView, AdminQuotasView, AdminAiView, AdminAuditView) + +**Wave 3** *(blocked on 09-01, 09-02, 09-03)* + +- [ ] 09-04-PLAN.md — Router rearchitecture (nested /admin + to.matched.some guard + D-08/D-09/D-10 redirects) + Tailwind safelist + LoginView admin redirect + delete AdminView.vue + 4 tab files + +**Wave 4** *(blocked on 09-04)* + +- [ ] 09-05-PLAN.md — CODE-09 comment purge (Phase 9 files + retroactive Phase 8 backend sub-packages) + human checkpoint UAT + **UI hint**: yes --- @@ -666,7 +685,7 @@ _Started: 2026-06-07_ | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 8. Stack Upgrade & Backend Decomposition | 4/8 | In Progress| | -| 9. Admin Panel Rearchitecture | 0/TBD | Not started | — | +| 9. Admin Panel Rearchitecture | 0/5 | Planned | — | | 10. UX & Interaction | 0/TBD | Not started | — | | 11. Visual Design, Responsive Layout & Cleanup | 0/TBD | Not started | — | diff --git a/.planning/phases/09-admin-panel-rearchitecture/09-01-PLAN.md b/.planning/phases/09-admin-panel-rearchitecture/09-01-PLAN.md new file mode 100644 index 0000000..e9cc4d1 --- /dev/null +++ b/.planning/phases/09-admin-panel-rearchitecture/09-01-PLAN.md @@ -0,0 +1,191 @@ +--- +phase: 09-admin-panel-rearchitecture +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - backend/api/admin/overview.py + - backend/api/admin/__init__.py + - backend/tests/test_admin_overview.py +autonomous: true +requirements: + - ADMIN-11 +user_setup: [] + +must_haves: + truths: + - "GET /api/admin/overview returns 200 for admin caller with keys user_count, total_storage_bytes, doc_status, recent_audit" + - "GET /api/admin/overview returns 401 or 403 for non-admin / unauthenticated callers" + - "Overview response body contains no occurrence of password_hash, credentials_enc, or extracted_text" + - "recent_audit list has at most 10 entries" + artifacts: + - path: "backend/api/admin/overview.py" + provides: "GET /api/admin/overview aggregate endpoint" + contains: "router = APIRouter()" + - path: "backend/api/admin/__init__.py" + provides: "overview_router registration on admin parent router" + contains: "from api.admin.overview import router as overview_router" + - path: "backend/tests/test_admin_overview.py" + provides: "ADMIN-11 endpoint + security invariant tests" + contains: "def test_overview_no_sensitive_fields" + key_links: + - from: "backend/api/admin/overview.py" + to: "backend/api/audit.py" + via: "import _build_filtered_query_with_handles, _audit_to_dict_with_handles" + pattern: "from api.audit import" + - from: "backend/api/admin/__init__.py" + to: "backend/api/admin/overview.py" + via: "include_router(overview_router)" + pattern: "include_router\\(overview_router\\)" +--- + + +Add a new admin overview backend endpoint at `GET /api/admin/overview` that returns aggregated platform stats (user count, total storage, document status breakdown) plus the 10 most recent audit-log entries in a single response payload, and register it on the admin parent router. Backed by `backend/tests/test_admin_overview.py` covering ADMIN-11 + security invariants. + +Purpose: ADMIN-11 requires an admin overview that combines stats + recent audit entries; D-02 mandates a new `overview.py` sub-module inside `backend/api/admin/`; D-03 requires the audit rows inline (single HTTP call, no `/api/audit` round-trip from the overview view). + +Output: `backend/api/admin/overview.py` (new), `backend/api/admin/__init__.py` (modified — one import + one include_router call), `backend/tests/test_admin_overview.py` (new — 3 promoted tests). + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/phases/09-admin-panel-rearchitecture/09-CONTEXT.md +@.planning/phases/09-admin-panel-rearchitecture/09-RESEARCH.md +@.planning/phases/09-admin-panel-rearchitecture/09-PATTERNS.md +@CLAUDE.md +@backend/api/admin/__init__.py +@backend/api/admin/users.py +@backend/api/audit.py +@backend/tests/test_admin_api.py + + +From backend/api/audit.py: + - `_build_filtered_query_with_handles(start, end, user_uuid, event_type)` — returns a SQLAlchemy `Select` statement joining `AuditLog` with `users` for owner_handle + target_user_handle. Pass `None` for all four args to get the unfiltered base query; chain `.limit(10)` for the most recent 10. + - `_audit_to_dict_with_handles(audit_log_row, owner_handle, target_user_handle, ip)` — returns a safe dict for one audit entry; the security-audited whitelist serializer reused for ADMIN-06. + +From backend/db/models.py: + - `User` (fields used: `id`, `role` literal `'user'` vs `'admin'`) + - `Quota` (fields used: `used_bytes`) + - `Document` (fields used: `status`) + - `AuditLog` + +From backend/deps/auth.py: + - `get_current_admin` — FastAPI dependency that raises 401 (no token) or 403 (non-admin token). + +From backend/api/admin/__init__.py (current state): + - `router = APIRouter(prefix="/api/admin", tags=["admin"])` — parent aggregator; sub-routers MUST carry NO prefix per the "NO prefix" WHY comment at the top of the file (do not remove that comment in this plan). + + + + + + + Task 1: Create Wave 0 test stubs for the overview endpoint + backend/tests/test_admin_overview.py + + - backend/tests/test_admin_api.py (copy `make_admin_user` fixture verbatim + `admin_client` fixture pattern; reuse `FakeRedis` import from `tests.test_auth_api`) + - backend/tests/test_audit.py (audit fixture seeding pattern for `recent_audit` assertion) + - backend/tests/conftest.py (existing `async_client` and `db_session` fixtures) + - backend/api/audit.py (signature of `_build_filtered_query_with_handles` so test data covers a query that returns at least one row) + + + - Test 1 `test_overview_requires_admin`: unauthenticated GET /api/admin/overview returns 401 or 403. + - Test 2 `test_overview_non_admin_forbidden`: regular-user JWT returns 403. + - Test 3 `test_overview_returns_expected_keys`: admin GET returns 200 with all four top-level keys present (`user_count`, `total_storage_bytes`, `doc_status`, `recent_audit`). + - Test 4 `test_overview_user_count_excludes_admins`: seed one admin + two `role='user'` users; `user_count == 2`. + - Test 5 `test_overview_total_storage_sums_quotas`: seed quotas (1024 + 2048); `total_storage_bytes == 3072`. + - Test 6 `test_overview_doc_status_groups_by_status`: seed documents with statuses `ready`, `ready`, `processing`, `failed`; `doc_status == {"ready": 2, "processing": 1, "failed": 1}`. + - Test 7 `test_overview_recent_audit_limit_10`: seed 15 audit entries; `len(recent_audit) == 10`; entries ordered newest first. + - Test 8 `test_overview_no_sensitive_fields`: response body string contains none of `password_hash`, `credentials_enc`, `extracted_text`, `totp_secret`, `api_key_enc`. + + Create `backend/tests/test_admin_overview.py` with the 8 tests listed in ``. Reuse `make_admin_user` and `admin_client` patterns from `test_admin_api.py` verbatim (copy fixtures into this file or import from `tests.test_admin_api` if existing test files do that — match the prevailing pattern in `test_admin_ai_config.py`). All 8 tests SHOULD be marked `@pytest.mark.xfail(strict=True, reason="ADMIN-11: overview endpoint not yet implemented")` initially so Task 2 can flip them to passing. Each test uses `pytest.mark.asyncio`. Use the `FakeRedis` import from `tests.test_auth_api` (matches existing admin tests). The total_storage test seeds Quota rows directly; the doc_status test seeds Document rows directly; the recent_audit test seeds AuditLog rows directly via the existing `AuditLog` ORM model — do not call audit-write helpers (that introduces coupling). Body-string scan in `test_overview_no_sensitive_fields` uses `resp.text` (full raw JSON string). + + cd backend && pytest tests/test_admin_overview.py -v + + + - File `backend/tests/test_admin_overview.py` exists. + - `grep -c "^async def test_" backend/tests/test_admin_overview.py` returns 8. + - All 8 tests show XFAIL (expected fail) in pytest output since the endpoint does not exist yet — `pytest tests/test_admin_overview.py -v` exits 0. + - File imports `pytest`, `pytest_asyncio`, `uuid`, `httpx.AsyncClient`, and `from db.models import User, Quota, Document, AuditLog`. + - Every test function carries both `@pytest.mark.asyncio` and `@pytest.mark.xfail(strict=True, reason="ADMIN-11: ...")`. + + Wave 0 test scaffolds exist for ADMIN-11; all 8 tests are xfail and pytest exits 0. + + + + Task 2: Implement overview.py endpoint and register on admin router + backend/api/admin/overview.py, backend/api/admin/__init__.py + + - backend/api/admin/users.py (full file — exact `router = APIRouter()` pattern, dependency wiring `_admin: User = Depends(get_current_admin)`, scalar/aggregate query idioms with `func.count`) + - backend/api/admin/__init__.py (current 24 lines — preserve the WHY comment about prefix per D-16; pattern for `include_router` ordering) + - backend/api/audit.py (signature and body of `_build_filtered_query_with_handles` and `_audit_to_dict_with_handles` — see lines around the audit-log listing endpoint; confirm the helper returns aliased handle columns the dict serializer expects) + - backend/db/models.py (`User.role`, `Quota.used_bytes`, `Document.status`) + - backend/tests/test_admin_overview.py (the 8 xfail tests this implementation must satisfy) + + + - Endpoint path `/overview` declared on a sub-router with NO prefix; aggregator carries `/api/admin` so full URL is `/api/admin/overview`. + - Handler is `async def get_overview` returning `dict` with exact keys `user_count`, `total_storage_bytes`, `doc_status`, `recent_audit`. + - `user_count` is `SELECT count(id) FROM users WHERE role = 'user'`. + - `total_storage_bytes` is `SELECT sum(used_bytes) FROM quotas` coerced to `0` when NULL. + - `doc_status` is `{status: count}` from `SELECT status, count(id) FROM documents GROUP BY status`. + - `recent_audit` is the result of `_build_filtered_query_with_handles(None, None, None, None).limit(10)` mapped via `_audit_to_dict_with_handles`. + - Response never includes raw user records, raw audit rows, password_hash, credentials_enc, totp_secret, api_key_enc, or extracted_text. + + Create `backend/api/admin/overview.py`. Imports: `from __future__ import annotations`; `from fastapi import APIRouter, Depends`; `from sqlalchemy import func, select`; `from sqlalchemy.ext.asyncio import AsyncSession`; `from db.models import Document, Quota, User`; `from deps.auth import get_current_admin`; `from deps.db import get_db`; `from api.audit import _build_filtered_query_with_handles, _audit_to_dict_with_handles`. Declare `router = APIRouter()` with NO prefix (single WHY comment: "NO prefix — parent `__init__.py` carries `/api/admin` (D-02)"). Declare `@router.get("/overview")` async function `get_overview(session: AsyncSession = Depends(get_db), _admin: User = Depends(get_current_admin)) -> dict`. Compute user_count via `await session.scalar(select(func.count(User.id)).where(User.role == "user"))` (default to 0 if None). Compute total_storage_bytes via `await session.scalar(select(func.sum(Quota.used_bytes)))` (default 0). Compute doc_status via `(await session.execute(select(Document.status, func.count(Document.id)).group_by(Document.status))).all()` and convert to dict. Compute recent_audit by calling `_build_filtered_query_with_handles(None, None, None, None).limit(10)`, executing, then mapping each row tuple via `_audit_to_dict_with_handles(*row)` — confirm the row unpacking shape against the audit.py source (the existing list endpoint already does this — copy that exact iteration). Return the dict with the four keys. Modify `backend/api/admin/__init__.py`: add `from api.admin.overview import router as overview_router` alongside existing imports (preserve the docstring WHY comment); append `router.include_router(overview_router)` after the existing three `include_router` calls. Then remove the `@pytest.mark.xfail` decorators from all 8 tests in `backend/tests/test_admin_overview.py` so they execute as real tests. + + cd backend && pytest tests/test_admin_overview.py -v -x + + + - File `backend/api/admin/overview.py` exists. + - `grep -c "router = APIRouter()" backend/api/admin/overview.py` returns 1. + - `grep -E "APIRouter\\(prefix=" backend/api/admin/overview.py` returns no match (NO prefix on sub-router). + - `grep -c "from api.audit import _build_filtered_query_with_handles" backend/api/admin/overview.py` returns 1. + - `grep -c "_admin: User = Depends(get_current_admin)" backend/api/admin/overview.py` returns 1. + - `grep -c "overview_router" backend/api/admin/__init__.py` returns 2 (one import, one include_router). + - `grep -c "xfail" backend/tests/test_admin_overview.py` returns 0 (all xfails removed). + - `pytest backend/tests/test_admin_overview.py -v` reports 8 passed, 0 failed, 0 xfailed. + - `pytest backend/tests/test_admin_api.py backend/tests/test_audit.py -v` continues to pass (no regression in existing admin/audit tests). + - Endpoint module is discoverable: `python -c "from api.admin.overview import router; print(router.routes[0].path)"` prints `/overview`. + + GET /api/admin/overview returns 200 for admins with the four-key payload; all 8 test_admin_overview tests pass; existing admin and audit test suites stay green. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| browser → /api/admin/overview | admin JWT crosses; aggregate stats + audit rows returned | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-09-01-01 | Elevation of Privilege | GET /api/admin/overview | mitigate | `_admin: User = Depends(get_current_admin)` raises 401/403 for non-admin tokens; covered by `test_overview_requires_admin` + `test_overview_non_admin_forbidden` | +| T-09-01-02 | Information Disclosure | GET /api/admin/overview response | mitigate | Response is hand-rolled dict with only aggregate counts + whitelisted `_audit_to_dict_with_handles` rows; no `password_hash`/`credentials_enc`/`extracted_text`/`totp_secret`/`api_key_enc` ever serialized; covered by `test_overview_no_sensitive_fields` (raw body grep) | +| T-09-01-03 | Tampering (Reusable code) | `_build_filtered_query_with_handles` import | accept | Cross-module import from sibling `api/audit.py` is a stable existing helper (already security-audited and tested); risk: future audit.py refactor breaks the import — accepted because the import path is documented in the WHY comment and an ImportError fails loud on startup | + + + +- `cd backend && pytest tests/test_admin_overview.py tests/test_admin_api.py tests/test_audit.py -v` → all pass, zero failures, zero xfails. +- `cd backend && pytest -v` → no regressions across the full suite. +- `cd backend && python -c "from api.admin import router; paths=[r.path for r in router.routes]; assert '/api/admin/overview' in paths or any('/overview' in p for p in paths), paths"` → endpoint registered under the `/api/admin` prefix. +- `cd backend && bandit -r api/admin/overview.py` → zero HIGH findings. + + + +- ADMIN-11 backend slice deliverable: `GET /api/admin/overview` returns the four-key aggregate payload to admin callers; non-admin/unauthenticated callers get 401/403; response body never contains sensitive fields. +- Sub-router carries NO prefix (constraint preserved per existing Phase 8 invariant). +- All 8 dedicated tests pass; broader admin + audit suites stay green. + + + +Create `.planning/phases/09-admin-panel-rearchitecture/09-01-SUMMARY.md` when done. Include: files created, files modified, tests passing count, and confirmation that no sensitive fields are present in the overview response. + diff --git a/.planning/phases/09-admin-panel-rearchitecture/09-02-PLAN.md b/.planning/phases/09-admin-panel-rearchitecture/09-02-PLAN.md new file mode 100644 index 0000000..d6290e6 --- /dev/null +++ b/.planning/phases/09-admin-panel-rearchitecture/09-02-PLAN.md @@ -0,0 +1,242 @@ +--- +phase: 09-admin-panel-rearchitecture +plan: 02 +type: execute +wave: 1 +depends_on: [] +files_modified: + - frontend/src/layouts/AdminLayout.vue + - frontend/src/components/admin/AdminSidebar.vue + - frontend/src/views/admin/AdminOverviewView.vue + - frontend/src/api/admin.js +autonomous: true +requirements: + - ADMIN-08 + - ADMIN-09 + - ADMIN-11 + +user_setup: [] + +must_haves: + truths: + - "AdminLayout.vue renders an aside (AdminSidebar) and a main content area containing a router-view" + - "AdminSidebar shows the DocuVault logo with an 'Admin' subtitle and five nav links: Overview, Users, Quotas, AI Config, Audit Log — no Back to app link" + - "AdminOverviewView fetches GET /api/admin/overview on mount and renders four stat cards + a 10-row audit table" + - "frontend/src/api/admin.js exports getAdminOverview()" + artifacts: + - path: "frontend/src/layouts/AdminLayout.vue" + provides: "Admin route shell (sidebar + router-view)" + contains: "router-view" + - path: "frontend/src/components/admin/AdminSidebar.vue" + provides: "Admin-only sidebar nav with 5 router-links, no Back to app" + contains: "to=\"/admin/audit\"" + - path: "frontend/src/views/admin/AdminOverviewView.vue" + provides: "Stats cards + recent audit table at /admin" + contains: "getAdminOverview" + - path: "frontend/src/api/admin.js" + provides: "getAdminOverview() API client function" + contains: "export async function getAdminOverview" + key_links: + - from: "frontend/src/layouts/AdminLayout.vue" + to: "frontend/src/components/admin/AdminSidebar.vue" + via: "import AdminSidebar" + pattern: "import AdminSidebar from '\\.\\./components/admin/AdminSidebar.vue'" + - from: "frontend/src/views/admin/AdminOverviewView.vue" + to: "frontend/src/api/admin.js" + via: "getAdminOverview()" + pattern: "getAdminOverview\\(" + - from: "frontend/src/api/admin.js" + to: "/api/admin/overview" + via: "request()" + pattern: "/api/admin/overview" +--- + + +Create the admin route shell — `AdminLayout.vue` (the route component at `/admin`), `AdminSidebar.vue` (the admin-only sidebar with five nav links per D-07 and no Back-to-app per D-06), and `AdminOverviewView.vue` (the new overview view at `/admin` showing stats cards + last-10 audit table per D-01, D-03). Extend the frontend `api/admin.js` module with `getAdminOverview()` so the view can fetch the new backend endpoint. + +Purpose: ADMIN-08 requires `AdminLayout.vue` as the `/admin` route component with its own sidebar; ADMIN-09 defines the nav set (D-06 overrides the Back-to-app link); ADMIN-11 requires the overview UI. This plan builds the shell + new view; routing is wired in 09-04. + +Output: 4 new/modified frontend files. The shell renders correctly when manually mounted; the overview view single-fetches the backend endpoint built in 09-01. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/phases/09-admin-panel-rearchitecture/09-CONTEXT.md +@.planning/phases/09-admin-panel-rearchitecture/09-RESEARCH.md +@.planning/phases/09-admin-panel-rearchitecture/09-PATTERNS.md +@CLAUDE.md +@frontend/src/layouts/AuthLayout.vue +@frontend/src/components/layout/AppSidebar.vue +@frontend/src/components/admin/AuditLogTab.vue +@frontend/src/api/admin.js +@frontend/src/stores/auth.js +@frontend/src/utils/formatters.js + + +From frontend/src/api/client.js (re-export barrel from Phase 8): + - `request(path, options)` — the canonical fetch helper with 401-refresh handling. New API helpers MUST call `request()` and not roll their own `fetch`. Import via `./utils.js` in the domain module: `import { request } from './utils.js'`. + +From frontend/src/api/admin.js (existing domain module — Phase 8 CODE-04 output): + - Already exports `listAdminUsers`, `createAdminUser`, `setAdminUserQuota`, `getAiConfig`, etc. via `request()`. Add `getAdminOverview` alongside these. + +From frontend/src/stores/auth.js: + - `useAuthStore()` exposes `user` (ref with `{ email, role, ... }`) and `logout()` action. AdminSidebar imports the store for the user footer + sign-out. + +From frontend/src/components/layout/AppSidebar.vue (reference pattern): + - Container: `