docs(09): create phase 9 plan — Admin Panel Rearchitecture
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
373e009ed8
commit
ba30ada87a
+21
-2
@@ -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 | — |
|
||||
|
||||
|
||||
@@ -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\\)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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).
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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
|
||||
|
||||
<interfaces>
|
||||
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).
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Create Wave 0 test stubs for the overview endpoint</name>
|
||||
<files>backend/tests/test_admin_overview.py</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- 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`.
|
||||
</behavior>
|
||||
<action>Create `backend/tests/test_admin_overview.py` with the 8 tests listed in `<behavior>`. 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).</action>
|
||||
<verify>
|
||||
<automated>cd backend && pytest tests/test_admin_overview.py -v</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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: ...")`.
|
||||
</acceptance_criteria>
|
||||
<done>Wave 0 test scaffolds exist for ADMIN-11; all 8 tests are xfail and pytest exits 0.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Implement overview.py endpoint and register on admin router</name>
|
||||
<files>backend/api/admin/overview.py, backend/api/admin/__init__.py</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- 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.
|
||||
</behavior>
|
||||
<action>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.</action>
|
||||
<verify>
|
||||
<automated>cd backend && pytest tests/test_admin_overview.py -v -x</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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`.
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## 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 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `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.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
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.
|
||||
</output>
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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
|
||||
|
||||
<interfaces>
|
||||
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: `<aside class="w-64 bg-white border-r border-gray-200 flex flex-col h-full shrink-0">`
|
||||
- Logo block: `<div class="px-6 py-5 border-b border-gray-100"><h1 class="text-lg font-bold text-indigo-600 tracking-tight">DocuVault</h1><p class="text-xs text-gray-400 mt-0.5">Document Manager</p></div>`
|
||||
- Nav: `<nav class="flex-1 px-3 py-4 overflow-y-auto">…</nav>`
|
||||
- Scoped CSS: `.nav-link { @apply flex items-center px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-100 hover:text-gray-900 transition-colors text-sm font-medium; }` and `.nav-link-active { @apply bg-indigo-50 text-indigo-700; }`
|
||||
- SVG attrs: `class="w-4 h-4 mr-2 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"` with `stroke-linecap="round" stroke-linejoin="round" stroke-width="2"`
|
||||
- User footer block exists at the bottom with avatar initial + email + sign-out button.
|
||||
|
||||
From frontend/src/layouts/AuthLayout.vue (reference pattern):
|
||||
- Simple template-only layout shell hosting `<router-view />`. AdminLayout follows the same "layout-as-route-component" pattern; App.vue requires NO changes.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Add getAdminOverview() to api/admin.js</name>
|
||||
<files>frontend/src/api/admin.js</files>
|
||||
<read_first>
|
||||
- frontend/src/api/admin.js (current exports — Phase 8 CODE-04 file; find the existing `request` import and the pattern other GET helpers use, e.g. `listAdminUsers`)
|
||||
- frontend/src/api/utils.js (confirm `request` is exported here per Phase 8 CODE-04)
|
||||
- frontend/src/api/client.js (verify `getAdminOverview` re-export is automatic via the barrel)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- `getAdminOverview()` returns a Promise resolving to `{ user_count, total_storage_bytes, doc_status, recent_audit }`.
|
||||
- On 401 the underlying `request()` triggers a silent refresh; on non-401 errors the rejected Promise carries the server message.
|
||||
- Function is exported as a named export and re-exported automatically from `client.js` via the existing barrel.
|
||||
</behavior>
|
||||
<action>Open `frontend/src/api/admin.js`. Add a new named export `getAdminOverview` that calls `request('/api/admin/overview')` and returns its result (no extra wrapping). Follow the exact style of the existing GET helpers in the same file (likely `export async function getAdminOverview() { return request('/api/admin/overview') }`). Do NOT define a duplicate `request` import; reuse whatever import line already exists in `admin.js`. Do NOT touch `client.js` — Phase 8 CODE-04 made it a re-export barrel that picks up every named export from `admin.js` automatically.</action>
|
||||
<verify>
|
||||
<automated>cd frontend && grep -c "export async function getAdminOverview" src/api/admin.js && grep -c "/api/admin/overview" src/api/admin.js</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c "export async function getAdminOverview" frontend/src/api/admin.js` returns 1.
|
||||
- `grep -c "'/api/admin/overview'" frontend/src/api/admin.js` returns 1.
|
||||
- No new `import` line for `request` is added; existing import is reused.
|
||||
- `cd frontend && node -e "import('./src/api/admin.js').then(m => { if (typeof m.getAdminOverview !== 'function') process.exit(1) })"` exits 0 (function is exported).
|
||||
</acceptance_criteria>
|
||||
<done>`getAdminOverview()` exists in `api/admin.js` and resolves via the standard `request()` helper.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Create AdminLayout.vue + AdminSidebar.vue</name>
|
||||
<files>frontend/src/layouts/AdminLayout.vue, frontend/src/components/admin/AdminSidebar.vue</files>
|
||||
<read_first>
|
||||
- frontend/src/layouts/AuthLayout.vue (template-only layout pattern; confirms the "no App.vue branch" idiom)
|
||||
- frontend/src/components/layout/AppSidebar.vue (CSS classes, scoped style block, SVG family, user footer, sign-out function — copy these verbatim)
|
||||
- frontend/src/stores/auth.js (signature of `useAuthStore()` and `logout()`)
|
||||
- PATTERNS.md §AdminSidebar.vue (SVG path strings for the five nav icons)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- `AdminLayout.vue` template: `<div class="flex h-screen overflow-hidden">` with `<AdminSidebar />` then `<main class="flex-1 overflow-y-auto"><div class="p-8 max-w-5xl mx-auto"><router-view /></div></main>`. Uses `<script setup>` with `import AdminSidebar from '../components/admin/AdminSidebar.vue'`.
|
||||
- `AdminSidebar.vue` renders an `<aside class="w-64 bg-white border-r border-gray-200 flex flex-col h-full shrink-0">` matching `AppSidebar.vue` chrome.
|
||||
- Header subtitle reads "Admin" with classes `text-xs text-indigo-500 font-semibold mt-0.5` (D-05) instead of "Document Manager".
|
||||
- Nav contains exactly 5 `<router-link>` entries in D-07 order: Overview (`/admin`), Users (`/admin/users`), Quotas (`/admin/quotas`), AI Config (`/admin/ai`), Audit Log (`/admin/audit`). No "Back to app" link (D-06).
|
||||
- Overview active-state check uses `$route.path === '/admin'`; the other four use `$route.path.startsWith('/admin/users')` etc.
|
||||
- Footer: avatar initial (`authStore.user.email[0].toUpperCase()` with `?` fallback) + email + sign-out button that calls `authStore.logout()` then `router.push('/login')`.
|
||||
- Scoped `<style scoped>` block contains `.nav-link` and `.nav-link-active` with the exact `@apply` lines from `AppSidebar.vue`.
|
||||
</behavior>
|
||||
<action>Create `frontend/src/layouts/AdminLayout.vue` mirroring `AuthLayout.vue` structure but with the flex-row layout from PATTERNS.md §AdminLayout (sidebar + main + p-8 max-w-5xl mx-auto content wrapper). The `<router-view />` lives inside the content wrapper. `<script setup>` imports `AdminSidebar` from `../components/admin/AdminSidebar.vue` only — no other imports needed. Create `frontend/src/components/admin/AdminSidebar.vue`. Template: `<aside>` container with the exact classes from `<interfaces>`, header `<div>` with `<h1>DocuVault</h1>` (same classes as `AppSidebar.vue`) and `<p class="text-xs text-indigo-500 font-semibold mt-0.5">Admin</p>` (D-05). `<nav class="flex-1 px-3 py-4 overflow-y-auto">` containing 5 `<router-link>` elements in D-07 order. Each link uses `class="nav-link"` with `:class="{ 'nav-link-active': … }"` and contains an inline `<svg>` icon (use the path strings from PATTERNS.md §AdminSidebar.vue table for Overview/Users/Quotas/AI Config/Audit Log) followed by the link label. The Overview link active-state uses `$route.path === '/admin'`; the others use `$route.path.startsWith('/admin/<segment>')`. SVG attributes: `class="w-4 h-4 mr-2 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"` and each `<path>` has `stroke-linecap="round" stroke-linejoin="round" stroke-width="2"`. Footer block at the bottom replicates the avatar+email+sign-out pattern from `AppSidebar.vue` — copy that block verbatim and ensure it ends INSIDE the `<aside>`. `<script setup>` imports: `import { useRouter } from 'vue-router'`, `import { useAuthStore } from '../../stores/auth.js'`; sets `const authStore = useAuthStore()`, `const router = useRouter()`, and defines `async function signOut() { await authStore.logout(); router.push('/login') }`. Scoped `<style scoped>` block contains the two `@apply` rules from `<interfaces>`. DO NOT add a "Back to app" router-link anywhere (D-06).</action>
|
||||
<verify>
|
||||
<automated>cd frontend && grep -c "router-view" src/layouts/AdminLayout.vue && grep -c "to=\"/admin/audit\"" src/components/admin/AdminSidebar.vue && grep -c "Back to app" src/components/admin/AdminSidebar.vue</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `frontend/src/layouts/AdminLayout.vue` exists.
|
||||
- `grep -c "<router-view" frontend/src/layouts/AdminLayout.vue` returns 1.
|
||||
- `grep -c "import AdminSidebar from '../components/admin/AdminSidebar.vue'" frontend/src/layouts/AdminLayout.vue` returns 1.
|
||||
- `grep -c "p-8 max-w-5xl mx-auto" frontend/src/layouts/AdminLayout.vue` returns 1.
|
||||
- `frontend/src/components/admin/AdminSidebar.vue` exists.
|
||||
- `grep -c "to=\"/admin\"" frontend/src/components/admin/AdminSidebar.vue` returns at least 1 (Overview link).
|
||||
- `grep -c "to=\"/admin/users\"" frontend/src/components/admin/AdminSidebar.vue` returns 1.
|
||||
- `grep -c "to=\"/admin/quotas\"" frontend/src/components/admin/AdminSidebar.vue` returns 1.
|
||||
- `grep -c "to=\"/admin/ai\"" frontend/src/components/admin/AdminSidebar.vue` returns 1.
|
||||
- `grep -c "to=\"/admin/audit\"" frontend/src/components/admin/AdminSidebar.vue` returns 1.
|
||||
- `grep -ci "back to app" frontend/src/components/admin/AdminSidebar.vue` returns 0 (D-06).
|
||||
- `grep -c "text-indigo-500 font-semibold" frontend/src/components/admin/AdminSidebar.vue` returns at least 1 (Admin badge).
|
||||
- `grep -c "@apply" frontend/src/components/admin/AdminSidebar.vue` returns 2 (.nav-link + .nav-link-active).
|
||||
- `grep -c "authStore.logout" frontend/src/components/admin/AdminSidebar.vue` returns 1.
|
||||
- `cd frontend && npx vue-tsc --noEmit 2>&1 | grep -E "(AdminLayout|AdminSidebar)" | wc -l` returns 0 (no template/script errors in these two files); if `vue-tsc` is not configured, fall back to `npm run build` succeeding.
|
||||
</acceptance_criteria>
|
||||
<done>AdminLayout + AdminSidebar exist with the correct chrome; no Back-to-app link; nav links cover the five admin destinations in D-07 order.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Create AdminOverviewView.vue</name>
|
||||
<files>frontend/src/views/admin/AdminOverviewView.vue</files>
|
||||
<read_first>
|
||||
- frontend/src/components/admin/AuditLogTab.vue (table header + row pattern for the recent audit table; copy the table column layout for `event_type`, `owner_handle`, `target_user_handle`, `created_at`, `ip_address`)
|
||||
- frontend/src/components/admin/AdminUsersTab.vue (data-fetch-on-mount pattern with local `ref` + `onMounted` + try/catch; loading + error UI block)
|
||||
- frontend/src/utils/formatters.js (use existing `formatSize` for `total_storage_bytes` and `formatDate` for audit `created_at`)
|
||||
- frontend/src/api/admin.js (confirm `getAdminOverview` export from Task 1)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Template root is a single `<div>` with NO top-level padding (AdminLayout owns `p-8 max-w-5xl mx-auto`).
|
||||
- Loading block ("Loading overview…") shown when `loading.value === true`.
|
||||
- Error block (red text) shown when `error.value` is non-null.
|
||||
- Stat cards section: a 4-column grid (`grid grid-cols-1 md:grid-cols-4 gap-4`) of four `<div class="bg-white border border-gray-200 rounded-xl p-6">` cards: Users (`overview.user_count`), Storage (`formatSize(overview.total_storage_bytes)`), Processing (`overview.doc_status.processing ?? 0`), Ready (`overview.doc_status.ready ?? 0`). A fifth "Failed" card may be added beside or under (D-01 says 3-4 cards; 4 is the chosen number — choice noted in summary).
|
||||
- Recent audit table renders `overview.recent_audit` (max 10 rows) with columns: When (`formatDate(entry.created_at)`), Event (`entry.event_type`), Actor (`entry.owner_handle ?? '—'`), Target (`entry.target_user_handle ?? '—'`), IP (`entry.ip_address ?? '—'`). Uses the same table CSS structure as `AuditLogTab.vue`.
|
||||
- Empty audit array renders "No recent activity" placeholder text — NOT a generic "No items" (a proper EmptyState component lands in Phase 10; for Phase 9 a one-line placeholder is acceptable).
|
||||
</behavior>
|
||||
<action>Create `frontend/src/views/admin/AdminOverviewView.vue`. Template root: `<div>`. Inside, top section is `<h2 class="text-xl font-semibold text-gray-900 mb-6">Overview</h2>`. Below that: loading conditional `<div v-if="loading" class="text-gray-500">Loading overview…</div>`, error conditional `<div v-else-if="error" class="text-red-600">{{ error }}</div>`, then content `<template v-else-if="overview">` containing the stat-cards grid then the recent audit table. Use exactly four stat cards (Users, Storage, Processing, Ready) — choice rationale: ADMIN-11 lists 4 stats explicitly; D-01 says 3-4 cards (4 chosen). The Processing/Ready cards read `overview.doc_status?.processing ?? 0` and `overview.doc_status?.ready ?? 0` respectively. The audit table column layout copies AuditLogTab.vue's table head and row structure for the five fields listed in `<behavior>`. `<script setup>`: `import { ref, onMounted } from 'vue'`; `import { getAdminOverview } from '../../api/admin.js'`; `import { formatSize, formatDate } from '../../utils/formatters.js'`; `const loading = ref(false)`, `const error = ref(null)`, `const overview = ref(null)`; `onMounted(async () => { loading.value = true; try { overview.value = await getAdminOverview() } catch (e) { error.value = e?.message || 'Failed to load overview' } finally { loading.value = false } })`. No Pinia store — single-fetch view (RESEARCH.md Open Question 2 — chose "no store"). No inline color classes that need safelist — formatSize/formatDate return strings, not class names.</action>
|
||||
<verify>
|
||||
<automated>cd frontend && grep -c "getAdminOverview" src/views/admin/AdminOverviewView.vue && grep -c "formatSize" src/views/admin/AdminOverviewView.vue && grep -c "recent_audit" src/views/admin/AdminOverviewView.vue</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- File `frontend/src/views/admin/AdminOverviewView.vue` exists.
|
||||
- First non-comment template element is `<div>` with no `p-8`, `p-6`, or `max-w-` class (AdminLayout owns the padding).
|
||||
- `grep -c "getAdminOverview" frontend/src/views/admin/AdminOverviewView.vue` returns at least 1 (import) and one call.
|
||||
- `grep -c "from '../../utils/formatters.js'" frontend/src/views/admin/AdminOverviewView.vue` returns 1.
|
||||
- `grep -c "doc_status" frontend/src/views/admin/AdminOverviewView.vue` returns at least 2 (processing + ready cards).
|
||||
- `grep -c "total_storage_bytes" frontend/src/views/admin/AdminOverviewView.vue` returns at least 1.
|
||||
- `grep -c "user_count" frontend/src/views/admin/AdminOverviewView.vue` returns at least 1.
|
||||
- `grep -c "recent_audit" frontend/src/views/admin/AdminOverviewView.vue` returns at least 1.
|
||||
- `grep -cE "p-8|max-w-5xl" frontend/src/views/admin/AdminOverviewView.vue` returns 0 (no double padding).
|
||||
- `cd frontend && npm run build` succeeds (the view compiles even though router doesn't reference it yet — Vite reports it as an unused module but no error).
|
||||
</acceptance_criteria>
|
||||
<done>AdminOverviewView fetches `/api/admin/overview` on mount and renders four stat cards + a recent-audit table; no top-level padding; no Pinia store.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Vue runtime → backend admin API | new `getAdminOverview()` call crosses; relies on shared `request()` for auth + refresh |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-09-02-01 | Information Disclosure | AdminOverviewView render | accept | Component only renders fields returned by the backend; backend whitelist (T-09-01-02) is the authoritative gate. No `v-html` or innerHTML; Vue auto-escaping handles XSS. |
|
||||
| T-09-02-02 | Elevation of Privilege | AdminLayout shown to non-admin | mitigate | Router guard updated in 09-04 (`to.matched.some(r => r.meta.requiresAdmin)`) ensures the layout never mounts for non-admin users; this plan ships the chrome only. |
|
||||
| T-09-02-03 | Spoofing (Sidebar logout) | `authStore.logout()` call | accept | Reuses existing logout flow audited in Phase 7.1; no new code path. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `cd frontend && npm run build` → succeeds.
|
||||
- `grep -ci "back to app" frontend/src/components/admin/AdminSidebar.vue` → 0 (D-06).
|
||||
- Five admin destinations covered by `<router-link to="...">` in the sidebar.
|
||||
- `AdminOverviewView.vue` calls `getAdminOverview()` and uses `formatSize`/`formatDate` from shared formatters.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- ADMIN-08: `AdminLayout.vue` exists as a standalone layout with its own sidebar and a `<router-view />`.
|
||||
- ADMIN-09: AdminSidebar has the five D-07 nav items in order with SVG icons; no Back-to-app link (D-06 override applied).
|
||||
- ADMIN-11: AdminOverviewView single-fetches `/api/admin/overview` and displays the four stats + last-10 audit table.
|
||||
- `getAdminOverview()` lives in `api/admin.js`; the `client.js` barrel re-exports it without modification (Phase 8 CODE-04 invariant preserved).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-admin-panel-rearchitecture/09-02-SUMMARY.md` when done. Include: files created, the 4-card choice rationale (vs 3), and confirmation that no Back-to-app link is present and no top-level padding is duplicated in AdminOverviewView.
|
||||
</output>
|
||||
@@ -0,0 +1,174 @@
|
||||
---
|
||||
phase: 09-admin-panel-rearchitecture
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- 09-02
|
||||
files_modified:
|
||||
- frontend/src/views/admin/AdminUsersView.vue
|
||||
- frontend/src/views/admin/AdminQuotasView.vue
|
||||
- frontend/src/views/admin/AdminAiView.vue
|
||||
- frontend/src/views/admin/AdminAuditView.vue
|
||||
autonomous: true
|
||||
requirements:
|
||||
- ADMIN-08
|
||||
- ADMIN-10
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Each of the four extracted views (Users, Quotas, AI, Audit) is a self-contained .vue file under frontend/src/views/admin/"
|
||||
- "Each extracted view behaves identically to its Phase 8 tab component — same data fetches, same actions, same template DOM"
|
||||
- "Top-level template element of each view is a plain <div> with NO p-8, p-6, or max-w- class (AdminLayout owns the content padding)"
|
||||
artifacts:
|
||||
- path: "frontend/src/views/admin/AdminUsersView.vue"
|
||||
provides: "Standalone /admin/users view extracted from AdminUsersTab.vue"
|
||||
- path: "frontend/src/views/admin/AdminQuotasView.vue"
|
||||
provides: "Standalone /admin/quotas view extracted from AdminQuotasTab.vue"
|
||||
- path: "frontend/src/views/admin/AdminAiView.vue"
|
||||
provides: "Standalone /admin/ai view extracted from AdminAiConfigTab.vue"
|
||||
- path: "frontend/src/views/admin/AdminAuditView.vue"
|
||||
provides: "Standalone /admin/audit view extracted from AuditLogTab.vue"
|
||||
key_links:
|
||||
- from: "frontend/src/views/admin/*View.vue"
|
||||
to: "frontend/src/api/admin.js"
|
||||
via: "named imports preserved from source tab components"
|
||||
pattern: "from '../../api/"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Promote the four existing admin tab components (`AdminUsersTab.vue`, `AdminQuotasTab.vue`, `AdminAiConfigTab.vue`, `AuditLogTab.vue`) into standalone view files in `frontend/src/views/admin/` (D-11). Extraction is **structural and behavior-preserving** — same template, same script, same data fetches, same emitted actions, with import paths rewritten for the new location and the component renamed in the script setup.
|
||||
|
||||
Purpose: D-11 mandates the tab components become views. D-13 mandates AuditLog is promoted as-is (no new features). The Phase 9 architecture (D-09/D-10/D-11) requires each admin section be a standalone deep-linkable view component — `AdminLayout`'s `<router-view />` must render them.
|
||||
|
||||
Output: 4 new view files. Original tab components and `AdminView.vue` are deleted in 09-04 (after router rewiring). This plan does NOT delete anything and does NOT touch the router — those land in 09-04 to keep the codebase in a buildable state between waves.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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/components/admin/AdminUsersTab.vue
|
||||
@frontend/src/components/admin/AdminQuotasTab.vue
|
||||
@frontend/src/components/admin/AdminAiConfigTab.vue
|
||||
@frontend/src/components/admin/AuditLogTab.vue
|
||||
@frontend/src/views/AdminView.vue
|
||||
|
||||
<interfaces>
|
||||
From RESEARCH.md §Pattern 4 (codebase audit):
|
||||
- `AdminUsersTab.vue` — top element `<div>`, no padding.
|
||||
- `AdminQuotasTab.vue` — top element `<div>`, no padding.
|
||||
- `AdminAiConfigTab.vue` — top element `<div>`, no padding.
|
||||
- `AuditLogTab.vue` — top element `<div>`, no padding.
|
||||
|
||||
Tab components currently live at depth `frontend/src/components/admin/` (two levels deep from `src/`). Extracted views live at `frontend/src/views/admin/` (also two levels deep). Therefore relative imports of the form `../../api/...`, `../../stores/...`, `../../utils/...` resolve identically from the new location — **no relative import paths need adjustment**. Any imports that go through `../layout/...` (one level up to `components/`) would break, but these tab components do NOT import any sibling component under `components/`.
|
||||
|
||||
Verify before extracting each file: `grep -E "^import" frontend/src/components/admin/<TabFile>.vue` — every import path should already start with `../../` (relative to `components/admin/`). If any import uses a different depth, document the rewrite in the SUMMARY.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Extract AdminUsersTab.vue and AdminQuotasTab.vue to views/admin/</name>
|
||||
<files>frontend/src/views/admin/AdminUsersView.vue, frontend/src/views/admin/AdminQuotasView.vue</files>
|
||||
<read_first>
|
||||
- frontend/src/components/admin/AdminUsersTab.vue (entire file — this is the source being promoted)
|
||||
- frontend/src/components/admin/AdminQuotasTab.vue (entire file — this is the source being promoted)
|
||||
- frontend/src/views/AdminView.vue (current parent — confirm which props/events these tab components rely on so the standalone views can drop the prop-passing layer)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- `AdminUsersView.vue` template + script behave identically to `AdminUsersTab.vue` for the user CRUD flows (create user, deactivate user, set quota, set AI provider, etc.).
|
||||
- `AdminQuotasView.vue` template + script behave identically to `AdminQuotasTab.vue` for the quota management flows.
|
||||
- Both files compile under Vite with no warnings about unused imports or unresolved paths.
|
||||
- No prop is required to mount either view (AdminLayout passes nothing to its children).
|
||||
</behavior>
|
||||
<action>Copy the entire contents of `frontend/src/components/admin/AdminUsersTab.vue` into a new file `frontend/src/views/admin/AdminUsersView.vue`. Rename the component if the script defines a `name:` field (Options-API style) or sets `defineOptions({ name: 'AdminUsersTab' })` (Composition-API) — change `'AdminUsersTab'` to `'AdminUsersView'`. If neither convention is used, no rename is needed. Verify every import line resolves from the new path (use the rule in `<interfaces>`: `../../<segment>` resolves identically). If `AdminUsersTab.vue` declares props that were used to receive data from `AdminView.vue` and they are no longer relevant (e.g. a `currentUser` prop), remove the `defineProps` block AND audit where those props were referenced — if a prop was only used as a passthrough, replace its template references with direct store reads (`useAuthStore().user`). Do NOT change template DOM, do NOT change `onMounted` fetches, do NOT change emitted events (emitted events are dropped at the view boundary — they no longer have a parent listening; check whether any emit was the sole trigger of a fetch on the parent — if so, replace the emit with a direct re-fetch inside the view). Repeat the same extraction for `AdminQuotasTab.vue` → `frontend/src/views/admin/AdminQuotasView.vue`.</action>
|
||||
<verify>
|
||||
<automated>cd frontend && [ -f src/views/admin/AdminUsersView.vue ] && [ -f src/views/admin/AdminQuotasView.vue ] && diff <(sed -n '/<template>/,/<\/template>/p' src/components/admin/AdminUsersTab.vue | wc -l) <(sed -n '/<template>/,/<\/template>/p' src/views/admin/AdminUsersView.vue | wc -l) && npm run build 2>&1 | grep -E "AdminUsersView|AdminQuotasView" | grep -i error | wc -l</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `frontend/src/views/admin/AdminUsersView.vue` exists.
|
||||
- `frontend/src/views/admin/AdminQuotasView.vue` exists.
|
||||
- First non-comment template element of each is `<div>` (no `p-8`, `p-6`, `max-w-` class on the root).
|
||||
- Line count of each new view is within ±10% of its source tab (no large unintended changes).
|
||||
- `cd frontend && npm run build` succeeds with no Vite errors referencing the new view files.
|
||||
- `grep -E "defineProps|props:" frontend/src/views/admin/AdminUsersView.vue` either returns no match OR every declared prop is also USED inside the template (no dead prop declarations).
|
||||
- Same check for `AdminQuotasView.vue`.
|
||||
- All existing API client calls in the source files appear identically in the new files (`grep -E "from '../../api/" frontend/src/views/admin/AdminUsersView.vue` returns the same count as the source).
|
||||
</acceptance_criteria>
|
||||
<done>AdminUsersView and AdminQuotasView are standalone views that build cleanly; behavior preserved; no top-level padding.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Extract AdminAiConfigTab.vue and AuditLogTab.vue to views/admin/</name>
|
||||
<files>frontend/src/views/admin/AdminAiView.vue, frontend/src/views/admin/AdminAuditView.vue</files>
|
||||
<read_first>
|
||||
- frontend/src/components/admin/AdminAiConfigTab.vue (entire file)
|
||||
- frontend/src/components/admin/AuditLogTab.vue (entire file — D-13: promote as-is, no feature changes)
|
||||
- frontend/src/views/AdminView.vue (confirm prop-passing pattern)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- `AdminAiView.vue` template + script behave identically to `AdminAiConfigTab.vue` for the AI provider configuration flows (global system AI providers section + per-user AI assignment table, both preserved exactly per Phase 7 plan-05 pitfall 6).
|
||||
- `AdminAuditView.vue` template + script behave identically to `AuditLogTab.vue` for the audit log view: filter bar, paginated table, CSV download, daily-export list (all Phase 6.2 features preserved exactly).
|
||||
- Both files compile under Vite with no warnings.
|
||||
- Dynamic color classes used in `AuditLogTab.vue` (`bg-blue-50 text-blue-600`, `bg-purple-50 text-purple-600`, `bg-amber-50 text-amber-700`, `bg-gray-100 text-gray-600`) continue to be generated via the same `actionTypeClass()` helper in the new view — DO NOT inline these classes or hardcode them.
|
||||
</behavior>
|
||||
<action>Copy `frontend/src/components/admin/AdminAiConfigTab.vue` to `frontend/src/views/admin/AdminAiView.vue`. Apply the same rules as Task 1: rename component name if explicit, drop dead `defineProps`, replace orphaned `emit` calls with direct re-fetch where the emit was the only refresh trigger. Note: the cross-cutting constraint from Phase 7 (AdminAiConfigTab.vue per-user assignment table is preserved untouched; global system section is ABOVE it) must remain — do not reorder sections. Copy `frontend/src/components/admin/AuditLogTab.vue` to `frontend/src/views/admin/AdminAuditView.vue`. D-13 mandates **as-is** promotion — no new features, no template tweaks. The `actionTypeClass()` function MUST be preserved verbatim (its `bg-amber-50 text-amber-700` etc. drive the safelist requirement closed in 09-04). Verify the import paths still resolve from `views/admin/` (the depth is unchanged from `components/admin/`).</action>
|
||||
<verify>
|
||||
<automated>cd frontend && [ -f src/views/admin/AdminAiView.vue ] && [ -f src/views/admin/AdminAuditView.vue ] && grep -c "actionTypeClass" src/views/admin/AdminAuditView.vue && npm run build 2>&1 | grep -E "AdminAiView|AdminAuditView" | grep -i error | wc -l</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `frontend/src/views/admin/AdminAiView.vue` exists.
|
||||
- `frontend/src/views/admin/AdminAuditView.vue` exists.
|
||||
- First non-comment template element of each is `<div>` (no top-level padding).
|
||||
- `grep -c "actionTypeClass" frontend/src/views/admin/AdminAuditView.vue` returns at least 1 (helper preserved).
|
||||
- `grep -c "bg-amber-50" frontend/src/views/admin/AdminAuditView.vue` returns at least 1 (D-13 as-is preservation).
|
||||
- `grep -c "bg-purple-50" frontend/src/views/admin/AdminAuditView.vue` returns at least 1.
|
||||
- `grep -c "System AI" frontend/src/views/admin/AdminAiView.vue` returns at least 1 (global system section title preserved from Phase 7).
|
||||
- `cd frontend && npm run build` succeeds.
|
||||
- Line counts within ±10% of source tab components.
|
||||
</acceptance_criteria>
|
||||
<done>AdminAiView + AdminAuditView are standalone, build cleanly, preserve all Phase 6.2/7 behavior; dynamic color classes still produced through `actionTypeClass()`.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| (none new) | Behavior-preserving extraction; no new ingress, egress, or trust transitions |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-09-03-01 | Tampering | Tab-to-view extraction | mitigate | Verbatim copy of template + script preserves behavior; line-count check ±10% catches accidental edits; `npm run build` catches resolution errors |
|
||||
| T-09-03-02 | Information Disclosure | Orphaned emit handlers | mitigate | Action step audits every `emit(...)` for orphaned parent listeners and replaces them with direct re-fetches so no admin action silently no-ops (e.g., user creation modal closing without triggering list refresh) |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `cd frontend && npm run build` → succeeds.
|
||||
- All four new view files exist under `frontend/src/views/admin/`.
|
||||
- Each view's first template element is `<div>` with no top-level padding class.
|
||||
- Original tab components in `frontend/src/components/admin/` are untouched (deletion happens in 09-04).
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- ADMIN-08: Four standalone admin view files exist under `frontend/src/views/admin/`.
|
||||
- ADMIN-10: Each view is a self-contained component that the router can mount as a child of `/admin` (router wiring lands in 09-04).
|
||||
- Behavior preserved: every admin CRUD/filter/download interaction continues to work after wiring in 09-04.
|
||||
</verification>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-admin-panel-rearchitecture/09-03-SUMMARY.md` when done. List the four new files, any dead-prop removals, and any orphaned-emit-to-fetch replacements made during extraction.
|
||||
</output>
|
||||
@@ -0,0 +1,249 @@
|
||||
---
|
||||
phase: 09-admin-panel-rearchitecture
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on:
|
||||
- 09-01
|
||||
- 09-02
|
||||
- 09-03
|
||||
files_modified:
|
||||
- frontend/src/router/index.js
|
||||
- frontend/src/views/auth/LoginView.vue
|
||||
- frontend/tailwind.config.js
|
||||
- frontend/src/views/AdminView.vue
|
||||
- frontend/src/components/admin/AdminUsersTab.vue
|
||||
- frontend/src/components/admin/AdminQuotasTab.vue
|
||||
- frontend/src/components/admin/AdminAiConfigTab.vue
|
||||
- frontend/src/components/admin/AuditLogTab.vue
|
||||
autonomous: true
|
||||
requirements:
|
||||
- ADMIN-08
|
||||
- ADMIN-10
|
||||
- ADMIN-12
|
||||
- CODE-06
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Navigating to /admin/users, /admin/quotas, /admin/ai, /admin/audit in a fresh browser tab loads the correct admin view with the admin sidebar"
|
||||
- "A non-admin user navigating to /admin/users is redirected to /"
|
||||
- "An admin user navigating to any non-admin route is redirected to /admin"
|
||||
- "On login, admin users land at /admin; non-admin users land at /"
|
||||
- "Tailwind safelist covers all dynamic color families used by formatters.js and AuditLogTab.actionTypeClass()"
|
||||
- "AdminView.vue and the four AdminXxxTab.vue files are deleted; no file in the repo imports them"
|
||||
artifacts:
|
||||
- path: "frontend/src/router/index.js"
|
||||
provides: "nested /admin route subtree + corrected guard + admin role redirect"
|
||||
contains: "to.matched.some"
|
||||
- path: "frontend/src/views/auth/LoginView.vue"
|
||||
provides: "admin role redirect on login"
|
||||
contains: "user?.role === 'admin'"
|
||||
- path: "frontend/tailwind.config.js"
|
||||
provides: "safelist for dynamic color classes (sky + amber included)"
|
||||
contains: "safelist"
|
||||
key_links:
|
||||
- from: "frontend/src/router/index.js"
|
||||
to: "frontend/src/layouts/AdminLayout.vue"
|
||||
via: "lazy import as /admin route component"
|
||||
pattern: "import.*AdminLayout"
|
||||
- from: "frontend/src/router/index.js"
|
||||
to: "frontend/src/views/admin/AdminOverviewView.vue"
|
||||
via: "lazy import as /admin child path ''"
|
||||
pattern: "AdminOverviewView"
|
||||
- from: "frontend/src/router/index.js beforeEach"
|
||||
to: "Vue Router 4 matched array"
|
||||
via: "to.matched.some(r => r.meta.requiresAdmin)"
|
||||
pattern: "to\\.matched\\.some\\(r => r\\.meta\\.requiresAdmin\\)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Wire the router to use `AdminLayout.vue` as the `/admin` route component with five lazy-loaded children (one per admin view). Fix the `beforeEach` guard so it uses `to.matched.some(r => r.meta.requiresAdmin)` and apply D-09/D-10 strict admin role separation. Wire the login-success handler so admin users land at `/admin` and regular users at `/`. Add the Tailwind safelist (D-14 corrected to include `sky` and `amber`). Delete `AdminView.vue` and the four `AdminXxxTab.vue` files (D-12).
|
||||
|
||||
Purpose: ADMIN-08 (AdminLayout becomes the route component), ADMIN-10 (deep-linkable URLs), ADMIN-12 (`to.matched.some()` guard), CODE-06 (safelist), and the cleanup mandate from D-12 (delete dead `AdminView.vue` + four tab files).
|
||||
|
||||
Output: rewired router, fixed guard, login redirect, Tailwind safelist, and a clean repo with zero references to the deleted files.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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/router/index.js
|
||||
@frontend/src/views/auth/LoginView.vue
|
||||
@frontend/tailwind.config.js
|
||||
@frontend/src/stores/auth.js
|
||||
@frontend/src/utils/formatters.js
|
||||
|
||||
<interfaces>
|
||||
From CONTEXT.md and RESEARCH.md (locked):
|
||||
- D-08: admin login → `/admin`.
|
||||
- D-09: admin on non-admin route → redirect `/admin`.
|
||||
- D-10: non-admin on `/admin/*` → redirect `/`. Both checks use `to.matched.some(r => r.meta.requiresAdmin)`.
|
||||
- D-14 (corrected by RESEARCH.md Pattern 7): safelist must include `sky` (formatters.js OneDrive) and `amber` (AuditLogTab `actionTypeClass()`):
|
||||
```
|
||||
safelist: [
|
||||
{ pattern: /bg-(blue|sky|green|purple|orange|amber|gray|indigo|red)-(50|100|500|600)/ },
|
||||
{ pattern: /text-(blue|sky|green|purple|orange|amber|gray|indigo|red)-(400|500|600|700)/ },
|
||||
]
|
||||
```
|
||||
|
||||
From RESEARCH.md Pattern 3 (LoginView.vue current code):
|
||||
- `handleLoginResult(result)` — when `!result`, currently `const redirect = route.query.redirect || '/'; await router.push(redirect)`. Modify the `!result` branch only.
|
||||
|
||||
From frontend/src/stores/auth.js (verified RESEARCH.md):
|
||||
- `user.value = data.user` runs synchronously inside the `login()` action before it returns — so `authStore.user.role` is populated when `handleLoginResult(!result)` fires.
|
||||
|
||||
From PATTERNS.md (current router state):
|
||||
- `frontend/src/router/index.js` currently has `{ path: '/admin', component: () => import('../views/AdminView.vue'), meta: { requiresAdmin: true } }` as a flat route.
|
||||
- Existing guard is `if (to.meta.requiresAdmin && authStore.user?.role !== 'admin') { return { path: '/' } }` — the broken check that lets child routes through.
|
||||
- Auth-only routes (`/login`, `/register`, etc.) carry `meta: { public: true, layout: 'auth' }` — both flags MUST be preserved unchanged.
|
||||
- Other user routes (`/`, `/topics`, `/folders/:id`, `/shared`, `/account`, `/settings`, `/cloud/...`) have no `requiresAdmin` flag and no `public` flag → these are the routes that the D-09 admin-redirect MUST send admins away from.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Rewire router/index.js — nested /admin + corrected guard + Tailwind safelist</name>
|
||||
<files>frontend/src/router/index.js, frontend/tailwind.config.js</files>
|
||||
<read_first>
|
||||
- frontend/src/router/index.js (full current file — locate the existing flat `/admin` route and the existing `beforeEach` guard for in-place replacement)
|
||||
- frontend/tailwind.config.js (current 9-line config — add a `safelist:` array between `content:` and `theme:`)
|
||||
- frontend/src/utils/formatters.js (confirm the dynamic class set; the safelist must cover every family referenced here)
|
||||
- frontend/src/components/admin/AuditLogTab.vue (confirm `actionTypeClass()` uses `bg-amber-50 text-amber-700` plus blue/gray/purple families)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- The router replaces the existing flat `/admin` route with a nested route whose `component` is the lazy-loaded `AdminLayout.vue` and whose `children` are five lazy-loaded admin views in the order: `''` (Overview), `users`, `quotas`, `ai`, `audit`. Each lazy import uses `() => import('../views/admin/Admin<Name>View.vue')`.
|
||||
- The parent `/admin` route carries `meta: { requiresAdmin: true }`. Child routes do NOT individually re-declare `requiresAdmin` — the guard relies on `to.matched.some()` to inherit from the parent.
|
||||
- The `beforeEach` guard:
|
||||
1. If route is non-public and `authStore.accessToken` is null, attempt silent refresh; on failure redirect to `/login?redirect=<original>`.
|
||||
2. Compute `isAdminRoute = to.matched.some(r => r.meta.requiresAdmin)`.
|
||||
3. Compute `isAdmin = authStore.user?.role === 'admin'`.
|
||||
4. D-10a: if `isAdminRoute && !isAdmin` → redirect `{ path: '/' }`.
|
||||
5. D-09: if `!isAdminRoute && !to.meta.public && isAdmin` → redirect `{ path: '/admin' }`.
|
||||
- Auth-public routes (`/login`, `/register`, `/forgot-password`, `/reset-password`) remain reachable for both admin and non-admin users (the `!to.meta.public` clause exempts them).
|
||||
- The Tailwind config gains a `safelist` array with two regex patterns covering `bg-(blue|sky|green|purple|orange|amber|gray|indigo|red)-(50|100|500|600)` and `text-(blue|sky|green|purple|orange|amber|gray|indigo|red)-(400|500|600|700)`.
|
||||
</behavior>
|
||||
<action>Open `frontend/src/router/index.js`. Locate the existing flat `/admin` route and replace it with the nested structure: parent `path: '/admin'`, `component: () => import('../layouts/AdminLayout.vue')`, `meta: { requiresAdmin: true }`, and a `children: [...]` array of five entries — `{ path: '', component: () => import('../views/admin/AdminOverviewView.vue') }`, then `{ path: 'users', component: () => import('../views/admin/AdminUsersView.vue') }`, then `quotas` → `AdminQuotasView.vue`, then `ai` → `AdminAiView.vue`, then `audit` → `AdminAuditView.vue`. Remove the old `() => import('../views/AdminView.vue')` reference entirely (Task 3 deletes that file). Replace the current `beforeEach` guard body with the 5-step structure from `<behavior>`: refresh-then-guard, computing `isAdminRoute` via `to.matched.some(r => r.meta.requiresAdmin)`, then the two D-09 / D-10a redirect branches. Preserve the existing public-route bypass: every route currently bearing `meta: { public: true }` (login, register, password reset) must still resolve without the admin/non-admin checks firing — the `!to.meta.public` clauses in steps 4 and 5 ensure this. Do NOT remove `meta.layout: 'auth'` from any auth route — `App.vue` still reads it. Open `frontend/tailwind.config.js`. Add a `safelist:` property between the existing `content:` line and the `theme:` block. The array contains two objects with `pattern` regexes exactly as specified in `<interfaces>`. Do NOT alter the existing `content`, `theme`, or `plugins` lines.</action>
|
||||
<verify>
|
||||
<automated>cd frontend && grep -c "to.matched.some(r => r.meta.requiresAdmin)" src/router/index.js && grep -c "AdminLayout" src/router/index.js && grep -c "AdminOverviewView" src/router/index.js && grep -c "AdminUsersView" src/router/index.js && grep -c "AdminAuditView" src/router/index.js && grep -c "/admin" src/router/index.js && grep -c "safelist" tailwind.config.js && grep -c "amber" tailwind.config.js && grep -c "sky" tailwind.config.js && npm run build</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c "to.matched.some(r => r.meta.requiresAdmin)" frontend/src/router/index.js` returns at least 1.
|
||||
- `grep -c "to.meta.requiresAdmin" frontend/src/router/index.js` returns 0 (broken flat check removed).
|
||||
- `grep -c "AdminLayout" frontend/src/router/index.js` returns 1.
|
||||
- `grep -c "AdminOverviewView" frontend/src/router/index.js` returns 1.
|
||||
- `grep -c "AdminUsersView" frontend/src/router/index.js` returns 1.
|
||||
- `grep -c "AdminQuotasView" frontend/src/router/index.js` returns 1.
|
||||
- `grep -c "AdminAiView" frontend/src/router/index.js` returns 1.
|
||||
- `grep -c "AdminAuditView" frontend/src/router/index.js` returns 1.
|
||||
- `grep -c "AdminView" frontend/src/router/index.js` returns 0 (old flat ref gone).
|
||||
- `grep -c "children:" frontend/src/router/index.js` returns at least 1.
|
||||
- The two D-09 / D-10a redirect branches both exist: `grep -c "path: '/admin'" frontend/src/router/index.js` returns at least 2 (route + admin redirect) AND `grep -c "path: '/'" frontend/src/router/index.js` returns at least 2 (route + non-admin redirect).
|
||||
- `grep -c "safelist" frontend/tailwind.config.js` returns 1.
|
||||
- `grep -c "amber" frontend/tailwind.config.js` returns at least 1 (both patterns include `amber`).
|
||||
- `grep -c "sky" frontend/tailwind.config.js` returns at least 1.
|
||||
- `grep -E "blue\\|sky\\|green\\|purple\\|orange\\|amber\\|gray\\|indigo\\|red" frontend/tailwind.config.js | wc -l` returns at least 2 (one per pattern).
|
||||
- `cd frontend && npm run build` succeeds.
|
||||
</acceptance_criteria>
|
||||
<done>Nested `/admin` route subtree wired; guard fixed; Tailwind safelist installed; build green.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Wire admin login redirect in LoginView.vue</name>
|
||||
<files>frontend/src/views/auth/LoginView.vue</files>
|
||||
<read_first>
|
||||
- frontend/src/views/auth/LoginView.vue (full current file — locate `handleLoginResult`)
|
||||
- frontend/src/stores/auth.js (confirm `user.role` is populated synchronously inside `login()`)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- After a fully successful login (`!result` branch), the function computes `defaultRedirect = authStore.user?.role === 'admin' ? '/admin' : '/'` and pushes to `route.query.redirect || defaultRedirect`.
|
||||
- Other branches (`result.requires_totp`, `result.requires_password_change`) are unchanged.
|
||||
- The `?redirect=` query param is honored as before — but only as a no-op default for non-admin users. (D-08 puts the role check FIRST so admins always land in `/admin`; the existing guard from Task 1 then prevents admin from following any `?redirect=/` to user routes via the D-09 branch.)
|
||||
</behavior>
|
||||
<action>Open `frontend/src/views/auth/LoginView.vue`. Find the `handleLoginResult` function; in the `if (!result)` branch, replace the existing two lines (`const redirect = route.query.redirect || '/'` and `await router.push(redirect)`) with: compute `const defaultRedirect = authStore.user?.role === 'admin' ? '/admin' : '/'`, then `const redirect = route.query.redirect || defaultRedirect`, then `await router.push(redirect)`. The `authStore` variable name should already be in scope from the existing import — verify before adding any new imports.</action>
|
||||
<verify>
|
||||
<automated>cd frontend && grep -c "authStore.user?.role === 'admin'" src/views/auth/LoginView.vue && npm run build</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c "authStore.user?.role === 'admin'" frontend/src/views/auth/LoginView.vue` returns 1.
|
||||
- `grep -c "defaultRedirect" frontend/src/views/auth/LoginView.vue` returns at least 2 (definition + use).
|
||||
- The `if (!result)` branch still handles the `route.query.redirect` query param (the existing user-side redirect still works).
|
||||
- Other branches (`requires_totp`, `requires_password_change`) are byte-for-byte identical to the previous version (no accidental edits).
|
||||
- `cd frontend && npm run build` succeeds.
|
||||
</acceptance_criteria>
|
||||
<done>Admin login redirects to `/admin`; non-admin login retains existing behavior including optional `?redirect=` honored.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Delete AdminView.vue + four AdminXxxTab.vue files</name>
|
||||
<files>frontend/src/views/AdminView.vue, frontend/src/components/admin/AdminUsersTab.vue, frontend/src/components/admin/AdminQuotasTab.vue, frontend/src/components/admin/AdminAiConfigTab.vue, frontend/src/components/admin/AuditLogTab.vue</files>
|
||||
<read_first>
|
||||
- frontend/src/views/AdminView.vue (confirm it imports the four tab files and no other component imports it post-router-rewire)
|
||||
- frontend/src/components/admin/__tests__ (list directory — if any tests reference the tab components by path, they must be migrated or deleted alongside)
|
||||
- frontend/src/router/index.js (after Task 1, confirm no remaining `AdminView.vue` reference)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- All five files are removed via `git rm` (so the deletion is staged for commit).
|
||||
- No file in `frontend/src/` imports any of the deleted files after this task.
|
||||
- The build succeeds with the same five `/admin/*` routes resolving via the new view files from 09-02 and 09-03.
|
||||
</behavior>
|
||||
<action>Verify with `grep -r "AdminView" frontend/src/ --include='*.vue' --include='*.js'` returns no remaining references (only the legacy file itself before deletion). Verify with `grep -r "AdminUsersTab\\|AdminQuotasTab\\|AdminAiConfigTab\\|AuditLogTab" frontend/src/ --include='*.vue' --include='*.js'` returns only the source files themselves (no consumers). If any consumer reference survives (e.g., a stray test in `frontend/src/components/admin/__tests__/`), stop and report it in the SUMMARY — do not silently leave dead imports. Delete via `git rm frontend/src/views/AdminView.vue frontend/src/components/admin/AdminUsersTab.vue frontend/src/components/admin/AdminQuotasTab.vue frontend/src/components/admin/AdminAiConfigTab.vue frontend/src/components/admin/AuditLogTab.vue`. If `frontend/src/components/admin/__tests__/` contains test files that import any of the deleted tab components, update those tests to import the corresponding new view files (e.g., `AdminUsersTab.vue` → `views/admin/AdminUsersView.vue`); if the tests are obsolete because they test behavior that the routing rearchitecture changes, `git rm` them too and note in SUMMARY.</action>
|
||||
<verify>
|
||||
<automated>cd frontend && [ ! -f src/views/AdminView.vue ] && [ ! -f src/components/admin/AdminUsersTab.vue ] && [ ! -f src/components/admin/AdminQuotasTab.vue ] && [ ! -f src/components/admin/AdminAiConfigTab.vue ] && [ ! -f src/components/admin/AuditLogTab.vue ] && ! grep -r "AdminUsersTab\\|AdminQuotasTab\\|AdminAiConfigTab\\|AuditLogTab\\|views/AdminView" src/ --include='*.vue' --include='*.js' && npm run build</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- None of the five files exists in the working tree: `[ ! -f frontend/src/views/AdminView.vue ]` etc. all return 0.
|
||||
- `grep -rE "AdminUsersTab|AdminQuotasTab|AdminAiConfigTab|AuditLogTab|views/AdminView" frontend/src/ --include='*.vue' --include='*.js'` returns no match.
|
||||
- `cd frontend && npm run build` succeeds.
|
||||
- `git status -s` shows the five files as `D` (deleted, staged for commit).
|
||||
- Manual smoke check (recorded in SUMMARY): navigating `/admin`, `/admin/users`, `/admin/quotas`, `/admin/ai`, `/admin/audit` after `npm run dev` mounts the correct view with the admin sidebar.
|
||||
</acceptance_criteria>
|
||||
<done>Dead files deleted; build green; no orphan references; the five admin URLs resolve to the new views.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Browser address bar → router beforeEach guard | Untrusted URL crosses; guard decides whether to mount admin chrome |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-09-04-01 | Elevation of Privilege | `beforeEach` guard | mitigate | `to.matched.some(r => r.meta.requiresAdmin)` covers all child routes; backend `get_current_admin` deps on `/api/admin/*` are the authoritative second gate; manual smoke test in Task 3 verifies non-admin → `/admin/users` redirects to `/` |
|
||||
| T-09-04-02 | Privilege Escalation | LoginView `?redirect=` query param | mitigate | D-08 puts role check FIRST: even if a malicious link sends `?redirect=/`, admins are immediately redirected to `/` then to `/admin` by the D-09 guard branch; the guard is the final authority — frontend redirect manipulation cannot grant access to the wrong area |
|
||||
| T-09-04-03 | Information Disclosure | Vite production build purging dynamic classes | mitigate | Safelist (CODE-06) ensures every dynamic class family used by `formatters.js` + `actionTypeClass()` survives the build; manual visual check of OneDrive provider badge and audit action badges post-build is recorded in SUMMARY |
|
||||
| T-09-04-04 | Denial-of-Service (Redirect loop) | D-09 admin redirect | mitigate | Guard structure (refresh-await BEFORE both checks) ensures `authStore.user` is populated before the admin-redirect branch evaluates; `isAdminRoute` for `/admin/*` short-circuits the D-09 branch so admins on `/admin` are never redirected back to `/admin`; matches RESEARCH.md Pitfall 3 fix |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `cd frontend && npm run build` → succeeds with safelist applied.
|
||||
- `grep -rE "AdminUsersTab|AdminQuotasTab|AdminAiConfigTab|AuditLogTab|views/AdminView" frontend/src/ --include='*.vue' --include='*.js'` → no match.
|
||||
- `grep -c "to.matched.some" frontend/src/router/index.js` ≥ 1; `grep -c "to.meta.requiresAdmin" frontend/src/router/index.js` = 0.
|
||||
- Manual smoke (live dev server) — recorded in SUMMARY: `/admin/users` reachable as admin; redirects to `/` as non-admin; admin login lands at `/admin`; non-admin login lands at `/` (or `?redirect=` target).
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- ADMIN-08: `AdminLayout` is the `/admin` route component; `AdminView.vue` deleted.
|
||||
- ADMIN-10: Direct navigation to `/admin/users`, `/admin/quotas`, `/admin/ai`, `/admin/audit` mounts the correct view with the admin sidebar.
|
||||
- ADMIN-12: Guard uses `to.matched.some(r => r.meta.requiresAdmin)`; non-admin redirected from any `/admin/*` child.
|
||||
- CODE-06: Tailwind safelist includes every dynamic color family used by `formatters.js` and `AuditLogTab.actionTypeClass()`, including `sky` (OneDrive) and `amber` (audit badges).
|
||||
- D-08/D-09/D-10 strict admin separation enforced.
|
||||
- Five legacy files removed.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-admin-panel-rearchitecture/09-04-SUMMARY.md` when done. Include: confirmation of all five `/admin/*` URLs resolving correctly under `npm run dev`, the list of deleted files, screenshot or note that OneDrive (`sky`) and audit badges (`amber`) render in production color, and any test-file migrations performed in Task 3.
|
||||
</output>
|
||||
@@ -0,0 +1,258 @@
|
||||
---
|
||||
phase: 09-admin-panel-rearchitecture
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on:
|
||||
- 09-01
|
||||
- 09-02
|
||||
- 09-03
|
||||
- 09-04
|
||||
files_modified:
|
||||
- backend/api/admin/overview.py
|
||||
- backend/api/admin/__init__.py
|
||||
- backend/api/admin/users.py
|
||||
- backend/api/admin/quotas.py
|
||||
- backend/api/admin/ai.py
|
||||
- backend/api/admin/shared.py
|
||||
- backend/api/documents/__init__.py
|
||||
- backend/api/documents/upload.py
|
||||
- backend/api/documents/content.py
|
||||
- backend/api/documents/crud.py
|
||||
- backend/api/documents/shared.py
|
||||
- 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
|
||||
- frontend/src/layouts/AdminLayout.vue
|
||||
- frontend/src/components/admin/AdminSidebar.vue
|
||||
- frontend/src/views/admin/AdminOverviewView.vue
|
||||
- frontend/src/views/admin/AdminUsersView.vue
|
||||
- frontend/src/views/admin/AdminQuotasView.vue
|
||||
- frontend/src/views/admin/AdminAiView.vue
|
||||
- frontend/src/views/admin/AdminAuditView.vue
|
||||
- frontend/src/router/index.js
|
||||
- frontend/src/views/auth/LoginView.vue
|
||||
- frontend/tailwind.config.js
|
||||
- frontend/src/api/admin.js
|
||||
autonomous: false
|
||||
requirements:
|
||||
- CODE-09
|
||||
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Every file touched in Phase 9 contains no 'what' comments — only 'why' comments remain"
|
||||
- "Every Phase 8 backend sub-package file (api/admin/*.py, api/documents/*.py, api/auth/*.py) has been comment-purged using the D-16 criterion"
|
||||
- "The 'NO prefix' constraint comments in the three package __init__.py files are PRESERVED — they are non-obvious invariants per D-16"
|
||||
- "Full backend pytest suite is green and frontend build succeeds after the purge"
|
||||
artifacts:
|
||||
- path: "backend/api/admin/__init__.py"
|
||||
provides: "constraint comment about NO prefix preserved (D-16 'why' comment exemption)"
|
||||
contains: "NO prefix"
|
||||
key_links:
|
||||
- from: "purged file set"
|
||||
to: "test suite"
|
||||
via: "pytest -v"
|
||||
pattern: "0 failed"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Apply the CODE-09 comment purge across (a) every file touched in Phase 9 (both backend and frontend) and (b) retroactively across the Phase 8 backend sub-packages (`backend/api/admin/`, `backend/api/documents/`, `backend/api/auth/`). Remove every "what" comment (generic docstrings, "this does X" inline notes). Keep every "why" comment (constraints, pitfall notes, non-obvious invariants — D-16). The `prefix="/api/admin"` / "NO prefix" constraint comments are the canonical "why" exemplars and MUST be preserved.
|
||||
|
||||
Purpose: CODE-09 requires that no comment describes WHAT code does. D-15 mandates a single dedicated plan at the end of Phase 9 covering Phase 9 files plus Phase 8 backend sub-packages.
|
||||
|
||||
Output: cleaner code; same behavior; full test suite green; frontend build green.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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/auth/__init__.py
|
||||
@backend/api/documents/__init__.py
|
||||
|
||||
<interfaces>
|
||||
From D-16 (purge criterion):
|
||||
- **REMOVE** comments that describe WHAT the code does: generic function docstrings like `"""Return all users."""`, inline `# Loop over users` notes, `// Set state to loading` annotations, comment blocks restating the obvious from the next line.
|
||||
- **KEEP** comments that explain WHY: constraint notes (e.g., "NO prefix — parent carries /api/admin"), pitfall references (e.g., "T-08-04-04 prevention"), non-obvious invariants (e.g., "constant-time comparison required for token validation"), security boundary notes (e.g., "never include credentials_enc here").
|
||||
|
||||
From RESEARCH.md §Pitfall 6 (explicit exemption):
|
||||
- The constraint docstring at the top of `backend/api/admin/__init__.py` ("sub-routers carry NO prefix") reads like a docstring but is actually a non-obvious invariant comment. PRESERVE IT.
|
||||
- Equivalent constraint comments at the top of `backend/api/auth/__init__.py` and `backend/api/documents/__init__.py` follow the same rule — if they document the prefix invariant or a cross-module import constraint, PRESERVE.
|
||||
|
||||
From CLAUDE.md (Code Standards):
|
||||
- "Comments exist only where the *why* is non-obvious. Never explain what the code does."
|
||||
|
||||
Phase 8 backend file inventory (from RESEARCH.md Open Question 3 + repo audit):
|
||||
- backend/api/admin/: __init__.py, shared.py, users.py, quotas.py, ai.py
|
||||
- backend/api/documents/: __init__.py, shared.py, upload.py, content.py, crud.py
|
||||
- backend/api/auth/: __init__.py, shared.py, tokens.py, totp.py, password.py
|
||||
- (Note: roadmap mentions `sessions.py` for auth, but the repo only contains the four files above — purge what exists.)
|
||||
|
||||
Phase 9 backend files (created in 09-01):
|
||||
- backend/api/admin/overview.py
|
||||
- (backend/api/admin/__init__.py — already in Phase 8 set above; touched again in 09-01)
|
||||
|
||||
Phase 9 frontend files (created/modified in 09-02, 09-03, 09-04):
|
||||
- frontend/src/layouts/AdminLayout.vue
|
||||
- frontend/src/components/admin/AdminSidebar.vue
|
||||
- frontend/src/views/admin/AdminOverviewView.vue
|
||||
- frontend/src/views/admin/AdminUsersView.vue
|
||||
- frontend/src/views/admin/AdminQuotasView.vue
|
||||
- frontend/src/views/admin/AdminAiView.vue
|
||||
- frontend/src/views/admin/AdminAuditView.vue
|
||||
- frontend/src/router/index.js
|
||||
- frontend/src/views/auth/LoginView.vue
|
||||
- frontend/tailwind.config.js
|
||||
- frontend/src/api/admin.js
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Backend purge — Phase 8 sub-packages + Phase 9 overview.py</name>
|
||||
<files>backend/api/admin/__init__.py, backend/api/admin/shared.py, backend/api/admin/users.py, backend/api/admin/quotas.py, backend/api/admin/ai.py, backend/api/admin/overview.py, backend/api/documents/__init__.py, backend/api/documents/shared.py, backend/api/documents/upload.py, backend/api/documents/content.py, backend/api/documents/crud.py, 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/admin/__init__.py (anchor file for the "NO prefix" invariant comment — confirm before touching anything else)
|
||||
- backend/api/auth/__init__.py (same — confirm equivalent constraint comment exists and keep)
|
||||
- backend/api/documents/__init__.py (same)
|
||||
- One representative file per package to calibrate the purge: backend/api/admin/users.py, backend/api/documents/upload.py, backend/api/auth/tokens.py
|
||||
- backend/api/admin/overview.py (new from 09-01 — verify the NO-prefix WHY comment exists and is preserved)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- For each file in the list: read top-to-bottom; for each comment / docstring, decide "WHAT" (remove) vs "WHY" (keep) per D-16.
|
||||
- Module-level docstrings that only restate the filename (e.g., `"""Admin users API."""`) are WHAT — remove.
|
||||
- Module-level docstrings that document a constraint or invariant (e.g., the admin `__init__.py` "NO prefix" block) are WHY — KEEP.
|
||||
- Function docstrings of the form `"""Return all users."""` or `"""Get the user by ID."""` — WHAT, remove.
|
||||
- Function docstrings that describe a non-obvious contract (e.g., `"""Decrement quota atomically; raises QuotaExceeded if new total > limit."""`) — WHY (invariant), KEEP.
|
||||
- Inline `#` comments restating the next line (e.g., `# Build the query` above `query = select(User)`) — WHAT, remove.
|
||||
- Inline `#` comments documenting why a step is needed (e.g., `# atomic UPDATE; never read-then-write (CLAUDE.md)`) — WHY, KEEP.
|
||||
- TODOs are WHY-adjacent — KEEP unless the underlying TODO is already resolved.
|
||||
- Bandit suppression annotations (`# noqa: S105`, `# nosec`) with rationale — KEEP as written.
|
||||
- Behavior of every endpoint and helper is UNCHANGED. No code paths added or removed.
|
||||
</behavior>
|
||||
<action>For each file in `<files>`, apply the WHAT-vs-WHY judgment per `<behavior>`. Edit in place via the Edit tool — never rewrite entire files. Specific exemptions to PRESERVE verbatim: (1) the constraint docstring at the top of `backend/api/admin/__init__.py` documenting the `prefix="/api/admin"` invariant and sub-router NO-prefix rule; (2) any equivalent constraint comment at the top of `backend/api/auth/__init__.py` and `backend/api/documents/__init__.py`; (3) the "T-08-04-04 prevention" note (or whichever threat-ID note exists) tying the prefix rule to the Phase 8 threat model; (4) any HKDF domain-separation `info=...` rationale comments in `auth/*.py`; (5) any "constant-time comparison" notes around `hmac.compare_digest`; (6) any atomic-UPDATE-RETURNING quota notes; (7) the "NO prefix" single-line comment in `backend/api/admin/overview.py` from 09-01. After purging each file, run `cd backend && python -c "from api.admin import router; from api.documents import router; from api.auth import router"` to confirm the imports still resolve (catches accidentally-removed `from x import y` lines). After all files are purged, run `cd backend && pytest -v` and fail the task if any new test failure appears compared to the green baseline at the end of 09-04. Do NOT touch any `# type: ignore` comments (these are tool directives, not human comments).</action>
|
||||
<verify>
|
||||
<automated>cd backend && python -c "from api.admin import router as a; from api.documents import router as d; from api.auth import router as u; print(len(a.routes), len(d.routes), len(u.routes))" && grep -c "NO prefix" api/admin/__init__.py && grep -c "NO prefix" api/admin/overview.py && pytest -v --tb=no -q | tail -20</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Module imports still succeed for all three admin packages (`from api.admin import router` etc.).
|
||||
- `grep -c "NO prefix" backend/api/admin/__init__.py` returns at least 1 (constraint comment preserved).
|
||||
- `grep -c "NO prefix" backend/api/admin/overview.py` returns at least 1 (Phase 9 WHY comment preserved).
|
||||
- `cd backend && pytest -v --tb=no` reports the same passed count as the green baseline at the end of 09-04 (record both counts in SUMMARY).
|
||||
- `cd backend && bandit -r api/admin/ api/documents/ api/auth/ -q` produces zero new HIGH findings vs. baseline.
|
||||
- For each file purged, line count delta is non-positive (purging cannot add lines).
|
||||
- No `from <module> import <name>` line was removed by the purge (verify with diff that no `import` lines are gone).
|
||||
- Spot check 3 random `# `-prefixed comments still present in the files — each one passes the "could a reader figure this out from the next line of code?" test → if yes, it must be removed; if no (non-obvious WHY), it stays.
|
||||
</acceptance_criteria>
|
||||
<done>All Phase 8 backend sub-packages + Phase 9 `overview.py` purged of WHAT comments; invariant comments preserved; full backend suite green.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Frontend purge — Phase 9 Vue + JS files</name>
|
||||
<files>frontend/src/layouts/AdminLayout.vue, frontend/src/components/admin/AdminSidebar.vue, frontend/src/views/admin/AdminOverviewView.vue, frontend/src/views/admin/AdminUsersView.vue, frontend/src/views/admin/AdminQuotasView.vue, frontend/src/views/admin/AdminAiView.vue, frontend/src/views/admin/AdminAuditView.vue, frontend/src/router/index.js, frontend/src/views/auth/LoginView.vue, frontend/tailwind.config.js, frontend/src/api/admin.js</files>
|
||||
<read_first>
|
||||
- The 11 files in `<files>` (skim each; identify WHAT vs WHY comments)
|
||||
- CLAUDE.md §Code Standards (the comment rule)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Same WHAT-vs-WHY rule as Task 1.
|
||||
- Vue `<!-- HTML comments -->` follow the same rule.
|
||||
- JS `// inline` comments and `/* block */` comments follow the same rule.
|
||||
- JSDoc / TSDoc `/** … */` blocks describing return value or generic param meaning are WHAT — remove unless they document a non-obvious invariant.
|
||||
- Tailwind config has no comments to purge by default but check.
|
||||
- Tag-level comments in `.vue` files like `<!-- Logo -->`, `<!-- Nav section -->`, `<!-- Footer -->` are WHAT (the section is obvious from the markup) — remove unless they document a non-obvious wiring constraint (e.g., `<!-- inside aside, NOT inside main — admin scope only -->`).
|
||||
- The four extracted views (`AdminUsersView`, `AdminQuotasView`, `AdminAiView`, `AdminAuditView`) inherit comments from their Phase 8 source tab components — apply the purge to those inherited comments as part of this task.
|
||||
- Behavior unchanged. `npm run build` continues to succeed.
|
||||
</behavior>
|
||||
<action>For each file in `<files>`, apply WHAT-vs-WHY judgment. Specific preservations: any constraint note about Vue Router 4 meta inheritance (`to.matched.some(...)`) — KEEP; any note documenting why `request()` is reused via `utils.js` (Phase 8 CODE-04 invariant) — KEEP if present; any safelist rationale documenting why `sky` + `amber` were added — KEEP; any D-XX decision references inline (e.g., `// D-08: admin login redirect`) — KEEP (these are anchor notes linking back to CONTEXT.md). Tag-section comments in the Vue templates that just label a layout section (`<!-- Stat cards -->`) — remove. After purging, run `cd frontend && npm run build` and the existing frontend test command (Vitest if configured) to confirm no regression.</action>
|
||||
<verify>
|
||||
<automated>cd frontend && npm run build && grep -c "to.matched.some" src/router/index.js</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `cd frontend && npm run build` succeeds.
|
||||
- `grep -c "to.matched.some" frontend/src/router/index.js` still returns at least 1 (guard not accidentally erased).
|
||||
- The "Admin" subtitle text in `AdminSidebar.vue` is still present (verify with `grep -c "Admin</p>" frontend/src/components/admin/AdminSidebar.vue` returns 1 — the visible badge text is not a comment).
|
||||
- The safelist regex in `tailwind.config.js` is still present (`grep -c "amber" frontend/tailwind.config.js` ≥ 1; `grep -c "sky" frontend/tailwind.config.js` ≥ 1).
|
||||
- For each purged file, line count delta is non-positive.
|
||||
- No `import` line was removed.
|
||||
- Spot check 3 surviving `<!-- … -->` comments — each one passes the "could a reader figure this out from the next markup line?" test.
|
||||
</acceptance_criteria>
|
||||
<done>All 11 Phase 9 frontend files purged of WHAT comments; build green; behavior unchanged.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Human verification — full Phase 9 UX + comment purge spot check</name>
|
||||
<files>(no files — human verification only)</files>
|
||||
<action>Follow the steps in `<how-to-verify>` below; record any failures in feedback; respond per `<resume-signal>`.</action>
|
||||
<verify><human-check>see how-to-verify below</human-check></verify>
|
||||
<done>Human approves Phase 9 end-to-end: admin URLs route correctly, login redirect honors role, guard blocks non-admin from /admin/*, redirects admin from user routes, OneDrive (sky) + audit (amber) badges render in production build, and surviving comments are genuine WHY notes.</done>
|
||||
<what-built>
|
||||
Phase 9 is complete after this checkpoint. The user verifies that:
|
||||
- The five admin URLs render the correct view with the admin sidebar.
|
||||
- Login as admin lands at `/admin`; login as regular user lands at `/`.
|
||||
- A non-admin navigating to `/admin/users` is bounced to `/`.
|
||||
- An admin navigating to `/` is bounced to `/admin`.
|
||||
- OneDrive provider chip renders in sky color in production build; audit action badges render in amber.
|
||||
- Spot-checking three random purged files confirms that the surviving comments are all genuine WHY notes.
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
1. Backend suite: `cd backend && pytest -v` — record passed count; must equal the 09-04 baseline.
|
||||
2. Frontend build: `cd frontend && npm run build` — must succeed.
|
||||
3. Start the stack: `docker compose up -d` (or `cd backend && uvicorn main:app --reload` + `cd frontend && npm run dev`).
|
||||
4. As an **admin** user: log in. Confirm you land at `/admin`. Click each of the 5 nav links and confirm: Overview shows 4 stat cards + a 10-row recent-activity table; Users/Quotas/AI/Audit show their full pre-Phase-9 functionality.
|
||||
5. Open browser dev tools, paste `/topics` into the URL bar. Confirm the guard redirects you back to `/admin`.
|
||||
6. Log out. Log in as a **non-admin** user. Confirm you land at `/` (or `?redirect=` target).
|
||||
7. Paste `/admin/users` into the URL bar. Confirm the guard redirects you to `/`.
|
||||
8. Production build visual check: with `npm run build && npm run preview`, look at an account with OneDrive connected (or open the Cloud sidebar) — the OneDrive chip should render in sky color, NOT default gray. Open the audit log — the action-type badges should show their amber/blue/purple colors.
|
||||
9. Open three random files from the Task 1 and Task 2 file lists (your pick). For each, scan the remaining comments: each comment must be a WHY note. If any comment merely restates the next line of code, the purge missed it — record the file + line in your feedback.
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" if all steps pass. Otherwise describe what failed (e.g., "OneDrive chip is gray in production build" or "/admin/audit 404s after admin login").</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Comment purge → invariant erasure | Code that depends on a constraint may silently break if the constraint comment is removed and a future change violates the rule |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-09-05-01 | Tampering | Constraint comment erasure | mitigate | Explicit preservation list in Task 1 action prose (NO-prefix invariants, HKDF domain separation, constant-time comparison, atomic UPDATE-RETURNING); acceptance criteria assert specific grep results on the preserved anchor comments |
|
||||
| T-09-05-02 | Denial-of-Service | Accidental import removal | mitigate | After purge, Task 1 runs `python -c "from api.admin import router; …"` to confirm package imports resolve; full pytest -v is the second gate |
|
||||
| T-09-05-03 | Information Disclosure | Comment leaking implementation details | accept | Purge removes more than it adds; no new comments introduced; existing WHY comments already reviewed in Phase 8 security agent runs |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `cd backend && pytest -v` — same passed count as 09-04 baseline; zero new failures.
|
||||
- `cd frontend && npm run build` — succeeds.
|
||||
- Anchor comments still present (NO prefix, etc.) — verified by grep in acceptance criteria.
|
||||
- Human checkpoint (Task 3) records visual confirmation of admin UX + dynamic color rendering + spot-check of preserved WHY comments.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- CODE-09: No file in the Phase 9 / Phase 8 backend purge set contains a comment that describes WHAT the code does.
|
||||
- D-15: One dedicated plan covered Phase 9 files + retroactive Phase 8 backend sub-packages — this is that plan.
|
||||
- D-16: WHY comments preserved; the "NO prefix" anchors are the canonical exemplars.
|
||||
- Backend suite green; frontend build green; human UAT approved.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-admin-panel-rearchitecture/09-05-SUMMARY.md` when done. Include: backend pytest passed count (before vs after), list of files purged, list of anchor comments explicitly preserved, and the human-checkpoint approval line.
|
||||
</output>
|
||||
@@ -0,0 +1,700 @@
|
||||
# Phase 9: Admin Panel Rearchitecture - Pattern Map
|
||||
|
||||
**Mapped:** 2026-06-12
|
||||
**Files analyzed:** 14
|
||||
**Analogs found:** 14 / 14
|
||||
|
||||
---
|
||||
|
||||
## File Classification
|
||||
|
||||
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|
||||
|---|---|---|---|---|
|
||||
| `frontend/src/layouts/AdminLayout.vue` | layout | request-response | `frontend/src/layouts/AuthLayout.vue` | exact |
|
||||
| `frontend/src/components/admin/AdminSidebar.vue` | component | request-response | `frontend/src/components/layout/AppSidebar.vue` | exact |
|
||||
| `frontend/src/views/admin/AdminOverviewView.vue` | view | request-response | `frontend/src/components/admin/AdminUsersTab.vue` | role-match |
|
||||
| `frontend/src/views/admin/AdminUsersView.vue` | view | CRUD | `frontend/src/components/admin/AdminUsersTab.vue` | exact |
|
||||
| `frontend/src/views/admin/AdminQuotasView.vue` | view | CRUD | `frontend/src/components/admin/AdminQuotasTab.vue` | exact |
|
||||
| `frontend/src/views/admin/AdminAiView.vue` | view | CRUD | `frontend/src/components/admin/AdminAiConfigTab.vue` | exact |
|
||||
| `frontend/src/views/admin/AdminAuditView.vue` | view | request-response | `frontend/src/components/admin/AuditLogTab.vue` | exact |
|
||||
| `frontend/src/router/index.js` | config | request-response | `frontend/src/router/index.js` (self) | exact |
|
||||
| `frontend/src/views/auth/LoginView.vue` | view | request-response | `frontend/src/views/auth/LoginView.vue` (self) | exact |
|
||||
| `frontend/tailwind.config.js` | config | — | `frontend/tailwind.config.js` (self) | exact |
|
||||
| `backend/api/admin/overview.py` | controller | request-response | `backend/api/admin/users.py` | exact |
|
||||
| `backend/api/admin/__init__.py` | config | — | `backend/api/admin/__init__.py` (self) | exact |
|
||||
| `backend/tests/test_admin_overview.py` | test | request-response | `backend/tests/test_admin_api.py` | exact |
|
||||
| Phase 8 backend sub-packages (comment purge) | — | — | All files in `backend/api/admin/`, `backend/api/auth/`, `backend/api/documents/` | — |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Assignments
|
||||
|
||||
### `frontend/src/layouts/AdminLayout.vue` (layout, request-response)
|
||||
|
||||
**Analog:** `frontend/src/layouts/AuthLayout.vue`
|
||||
|
||||
**Complete analog** (lines 1–12):
|
||||
```vue
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div class="w-full max-w-sm">
|
||||
<!-- Brand logo -->
|
||||
<div class="text-center mb-6">
|
||||
<h1 class="text-xl font-semibold text-indigo-600 tracking-tight">DocuVault</h1>
|
||||
</div>
|
||||
<!-- Auth card content -->
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**AdminLayout pattern to implement** (mirrors above structure, adapted for sidebar layout):
|
||||
```vue
|
||||
<template>
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
<AdminSidebar />
|
||||
<main class="flex-1 overflow-y-auto">
|
||||
<div class="p-8 max-w-5xl mx-auto">
|
||||
<router-view />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AdminSidebar from '../components/admin/AdminSidebar.vue'
|
||||
</script>
|
||||
```
|
||||
|
||||
**Critical constraint:** `App.vue` already has `<router-view />` in its `v-else` branch. When `/admin` resolves, that router-view renders `AdminLayout`, and `AdminLayout`'s own `<router-view />` renders child views. Do NOT add a third `v-else-if` branch to `App.vue` — this causes double rendering (RESEARCH.md Pitfall 2).
|
||||
|
||||
The padding `p-8 max-w-5xl mx-auto` moves from `AdminView.vue` line 2 to `AdminLayout.vue`'s content wrapper. The four tab components have no top-level padding, so double-padding risk does not apply here.
|
||||
|
||||
---
|
||||
|
||||
### `frontend/src/components/admin/AdminSidebar.vue` (component, request-response)
|
||||
|
||||
**Analog:** `frontend/src/components/layout/AppSidebar.vue`
|
||||
|
||||
**Container pattern** (lines 2–7):
|
||||
```vue
|
||||
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col h-full shrink-0">
|
||||
<!-- Logo -->
|
||||
<div class="px-6 py-5 border-b border-gray-100">
|
||||
<h1 class="text-lg font-bold text-indigo-600 tracking-tight">DocuVault</h1>
|
||||
<p class="text-xs text-gray-400 mt-0.5">Document Manager</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Admin badge adaptation** — replace the `text-gray-400` subtitle with:
|
||||
```vue
|
||||
<p class="text-xs text-indigo-500 font-semibold mt-0.5">Admin</p>
|
||||
```
|
||||
|
||||
**Nav section pattern** (lines 10–14):
|
||||
```vue
|
||||
<nav class="flex-1 px-3 py-4 overflow-y-auto">
|
||||
<router-link
|
||||
to="/topics"
|
||||
class="nav-link"
|
||||
:class="{ 'nav-link-active': $route.path.startsWith('/topics') }"
|
||||
>
|
||||
```
|
||||
|
||||
**Admin nav links pattern** (adapt active-state check per route):
|
||||
```vue
|
||||
<router-link to="/admin" class="nav-link"
|
||||
:class="{ 'nav-link-active': $route.path === '/admin' }">
|
||||
<svg class="w-4 h-4 mr-2 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="..." />
|
||||
</svg>
|
||||
Overview
|
||||
</router-link>
|
||||
```
|
||||
|
||||
**SVG icon attributes** (lines 16–19 of AppSidebar.vue — exact attributes to copy):
|
||||
```
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
class="w-4 h-4 mr-2 shrink-0"
|
||||
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
```
|
||||
|
||||
**User identity footer** (lines 215–230):
|
||||
```vue
|
||||
<div v-if="authStore.user" class="flex items-center gap-3 px-4 py-3 border-t border-gray-100 mt-2 -mx-3">
|
||||
<div class="bg-indigo-100 text-indigo-700 text-xs font-semibold rounded-full w-8 h-8 flex items-center justify-center shrink-0">
|
||||
{{ authStore.user.email ? authStore.user.email[0].toUpperCase() : '?' }}
|
||||
</div>
|
||||
<span class="text-xs text-gray-600 truncate flex-1">{{ authStore.user.email }}</span>
|
||||
<button @click="signOut" aria-label="Sign out" class="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Sign-out function** (lines 284–287):
|
||||
```javascript
|
||||
async function signOut() {
|
||||
await authStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
```
|
||||
|
||||
**Scoped CSS** (lines 317–322):
|
||||
```vue
|
||||
<style scoped>
|
||||
.nav-link {
|
||||
@apply flex items-center px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-100 hover:text-gray-900 transition-colors text-sm font-medium;
|
||||
}
|
||||
.nav-link-active {
|
||||
@apply bg-indigo-50 text-indigo-700;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
**Script imports pattern** (lines 236–251):
|
||||
```javascript
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../../stores/auth.js'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
async function signOut() {
|
||||
await authStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
```
|
||||
|
||||
**D-06 constraint:** No "Back to app" link. Do not add a `/` router-link anywhere in AdminSidebar.
|
||||
|
||||
---
|
||||
|
||||
### `frontend/src/views/admin/AdminOverviewView.vue` (view, request-response)
|
||||
|
||||
**Analog:** `frontend/src/components/admin/AdminUsersTab.vue` (data-fetch-on-mount pattern)
|
||||
|
||||
**Top-level template structure** (AdminUsersTab.vue line 2):
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<!-- content — no padding here; AdminLayout owns p-8 max-w-5xl mx-auto -->
|
||||
```
|
||||
|
||||
**Data fetch pattern** (AdminUsersTab.vue script — onMounted with local ref state, no store):
|
||||
```javascript
|
||||
import { ref, onMounted } from 'vue'
|
||||
import * as api from '../../api/client.js'
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
const overview = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
overview.value = await api.getAdminOverview()
|
||||
} catch (e) {
|
||||
error.value = e.message || 'Failed to load overview'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**No Pinia store** — single-fetch component consistent with all existing admin tab components (confirmed by RESEARCH.md open question 2 answer).
|
||||
|
||||
**Response shape to expect** (from RESEARCH.md Pattern 6):
|
||||
```
|
||||
{ user_count, total_storage_bytes, doc_status: { processing, ready, failed }, recent_audit: [...] }
|
||||
```
|
||||
|
||||
**Stat card pattern** (use same border/rounded/bg pattern as AdminUsersTab.vue's create-user panel, line 4):
|
||||
```vue
|
||||
<div class="bg-white border border-gray-200 rounded-xl p-6">
|
||||
<!-- stat card content -->
|
||||
</div>
|
||||
```
|
||||
|
||||
**Audit table headers pattern** (copy from AuditLogTab.vue table header pattern for `recent_audit` rows).
|
||||
|
||||
---
|
||||
|
||||
### `frontend/src/views/admin/AdminUsersView.vue` (view, CRUD)
|
||||
|
||||
**Analog:** `frontend/src/components/admin/AdminUsersTab.vue` — this IS the source file, promoted to a view.
|
||||
|
||||
**Extraction rule** (RESEARCH.md Pattern 4): The tab component's top element is `<div>` with no padding. Copy entire file content. Rename component name in `<script setup>` if present. Wire as router child — no structural changes needed.
|
||||
|
||||
**Top element** (AdminUsersTab.vue line 2):
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
```
|
||||
|
||||
No `p-8` or `max-w` at top level — confirmed safe to promote as-is.
|
||||
|
||||
---
|
||||
|
||||
### `frontend/src/views/admin/AdminQuotasView.vue` (view, CRUD)
|
||||
|
||||
**Analog:** `frontend/src/components/admin/AdminQuotasTab.vue` — same extraction rule as AdminUsersView.
|
||||
|
||||
Top element is `<div>` with no padding — confirmed safe to promote as-is.
|
||||
|
||||
---
|
||||
|
||||
### `frontend/src/views/admin/AdminAiView.vue` (view, CRUD)
|
||||
|
||||
**Analog:** `frontend/src/components/admin/AdminAiConfigTab.vue` — same extraction rule.
|
||||
|
||||
Top element is `<div>` with no padding — confirmed safe to promote as-is.
|
||||
|
||||
---
|
||||
|
||||
### `frontend/src/views/admin/AdminAuditView.vue` (view, request-response)
|
||||
|
||||
**Analog:** `frontend/src/components/admin/AuditLogTab.vue` — same extraction rule.
|
||||
|
||||
**Top element** (AuditLogTab.vue line 2):
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<!-- Filter bar -->
|
||||
<div class="flex flex-wrap gap-3 mb-4 items-end">
|
||||
```
|
||||
|
||||
No padding at top level — confirmed safe to promote as-is.
|
||||
|
||||
**Dynamic color classes that need safelist** (AuditLogTab.vue `actionTypeClass()` function):
|
||||
```
|
||||
bg-blue-50 text-blue-600
|
||||
bg-gray-100 text-gray-600
|
||||
bg-purple-50 text-purple-600
|
||||
bg-amber-50 text-amber-700
|
||||
```
|
||||
|
||||
These are constructed dynamically and MUST be in `tailwind.config.js` safelist.
|
||||
|
||||
---
|
||||
|
||||
### `frontend/src/router/index.js` (config, request-response)
|
||||
|
||||
**Analog:** `frontend/src/router/index.js` (self — modify in place)
|
||||
|
||||
**Current flat admin route** (line 42 — to be replaced):
|
||||
```javascript
|
||||
{ path: '/admin', component: () => import('../views/AdminView.vue'), meta: { requiresAdmin: true } },
|
||||
```
|
||||
|
||||
**Replacement nested route structure** (RESEARCH.md Pattern 2):
|
||||
```javascript
|
||||
{
|
||||
path: '/admin',
|
||||
component: () => import('../layouts/AdminLayout.vue'),
|
||||
meta: { requiresAdmin: true },
|
||||
children: [
|
||||
{ path: '', component: () => import('../views/admin/AdminOverviewView.vue') },
|
||||
{ path: 'users', component: () => import('../views/admin/AdminUsersView.vue') },
|
||||
{ path: 'quotas', component: () => import('../views/admin/AdminQuotasView.vue') },
|
||||
{ path: 'ai', component: () => import('../views/admin/AdminAiView.vue') },
|
||||
{ path: 'audit', component: () => import('../views/admin/AdminAuditView.vue') },
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
All use lazy `() => import(...)` — consistent with existing auth view imports on lines 20–33.
|
||||
|
||||
**Current broken guard** (lines 91–93 — to be replaced):
|
||||
```javascript
|
||||
if (to.meta.requiresAdmin && authStore.user?.role !== 'admin') {
|
||||
return { path: '/' }
|
||||
}
|
||||
```
|
||||
|
||||
**Replacement guard** (RESEARCH.md Pattern 2, implements D-09/D-10):
|
||||
```javascript
|
||||
router.beforeEach(async (to) => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
if (!to.meta.public && !authStore.accessToken) {
|
||||
try {
|
||||
await authStore.refresh()
|
||||
} catch {
|
||||
return { path: '/login', query: { redirect: to.fullPath } }
|
||||
}
|
||||
}
|
||||
|
||||
const isAdminRoute = to.matched.some(r => r.meta.requiresAdmin)
|
||||
const isAdmin = authStore.user?.role === 'admin'
|
||||
|
||||
// D-10a: non-admin attempting admin route
|
||||
if (isAdminRoute && !isAdmin) {
|
||||
return { path: '/' }
|
||||
}
|
||||
|
||||
// D-09: admin attempting non-admin, non-public route
|
||||
if (!isAdminRoute && !to.meta.public && isAdmin) {
|
||||
return { path: '/admin' }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Guard order is critical:** refresh is awaited FIRST, then both checks run with populated `authStore.user`. This prevents the redirect loop on page reload (RESEARCH.md Pitfall 3).
|
||||
|
||||
---
|
||||
|
||||
### `frontend/src/views/auth/LoginView.vue` (view, request-response)
|
||||
|
||||
**Analog:** `frontend/src/views/auth/LoginView.vue` (self — modify `handleLoginResult` only)
|
||||
|
||||
**Current `handleLoginResult`** (lines 217–234):
|
||||
```javascript
|
||||
async function handleLoginResult(result) {
|
||||
if (!result) {
|
||||
// Full success — tokens set in store
|
||||
const redirect = route.query.redirect || '/'
|
||||
await router.push(redirect)
|
||||
return
|
||||
}
|
||||
if (result.requires_totp) {
|
||||
error.value = null
|
||||
step.value = 'totp'
|
||||
return
|
||||
}
|
||||
if (result.requires_password_change) {
|
||||
await router.push('/account')
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Modified `handleLoginResult`** for D-08 (change only the `!result` branch):
|
||||
```javascript
|
||||
async function handleLoginResult(result) {
|
||||
if (!result) {
|
||||
// Admin users land in /admin; regular users land at redirect or /
|
||||
const defaultRedirect = authStore.user?.role === 'admin' ? '/admin' : '/'
|
||||
const redirect = route.query.redirect || defaultRedirect
|
||||
await router.push(redirect)
|
||||
return
|
||||
}
|
||||
// ... rest unchanged
|
||||
}
|
||||
```
|
||||
|
||||
**Why this is safe:** `authStore.user` is set synchronously at `stores/auth.js` line 82 (`user.value = data.user`) before `login()` returns — `authStore.user` is populated when `handleLoginResult(!result)` runs. Source: `frontend/src/stores/auth.js` lines 80–84.
|
||||
|
||||
---
|
||||
|
||||
### `frontend/tailwind.config.js` (config)
|
||||
|
||||
**Analog:** `frontend/tailwind.config.js` (self — add safelist only)
|
||||
|
||||
**Current config** (lines 1–9):
|
||||
```javascript
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
import forms from '@tailwindcss/forms'
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{vue,js}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [forms],
|
||||
}
|
||||
```
|
||||
|
||||
**Modified config** (add `safelist` between `content` and `theme`):
|
||||
```javascript
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
import forms from '@tailwindcss/forms'
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{vue,js}'],
|
||||
safelist: [
|
||||
{ pattern: /bg-(blue|sky|green|purple|orange|amber|gray|indigo|red)-(50|100|500|600)/ },
|
||||
{ pattern: /text-(blue|sky|green|purple|orange|amber|gray|indigo|red)-(400|500|600|700)/ },
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [forms],
|
||||
}
|
||||
```
|
||||
|
||||
**Color family rationale** (from RESEARCH.md Pattern 7, verified against `formatters.js` and `AuditLogTab.vue`):
|
||||
- `sky`: OneDrive provider — `text-sky-500`, `bg-sky-50`
|
||||
- `amber`: Audit log action type badges — `bg-amber-50 text-amber-700`
|
||||
- D-14's proposed pattern omitted both `sky` and `amber` — this is the corrected version.
|
||||
|
||||
---
|
||||
|
||||
### `backend/api/admin/overview.py` (controller, request-response)
|
||||
|
||||
**Analog:** `backend/api/admin/users.py`
|
||||
|
||||
**File header pattern** (users.py lines 1–30):
|
||||
```python
|
||||
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
|
||||
|
||||
router = APIRouter() # NO prefix — parent __init__.py carries /api/admin (D-04)
|
||||
```
|
||||
|
||||
**Admin dependency pattern** (users.py lines 77–81):
|
||||
```python
|
||||
@router.get("/users")
|
||||
async def list_users(
|
||||
session: AsyncSession = Depends(get_db),
|
||||
_admin: User = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
```
|
||||
|
||||
**Aggregate query pattern** (users.py lines 87–91 — scalar count):
|
||||
```python
|
||||
result = await session.execute(
|
||||
select(User).order_by(User.created_at.desc())
|
||||
)
|
||||
users = result.scalars().all()
|
||||
```
|
||||
|
||||
**`func.count` / `func.sum` pattern** (users.py lines 185–192 — scalar aggregate):
|
||||
```python
|
||||
count_result = await session.execute(
|
||||
select(func.count(User.id)).where(
|
||||
User.role == "admin",
|
||||
User.is_active.is_(True),
|
||||
)
|
||||
)
|
||||
active_admin_count = count_result.scalar_one()
|
||||
```
|
||||
|
||||
**Cross-module import for audit query helpers** (RESEARCH.md Pattern 6 — anti-pattern warning):
|
||||
```python
|
||||
# CORRECT: import from api.audit (top-level module at backend/api/audit.py)
|
||||
from api.audit import _build_filtered_query_with_handles, _audit_to_dict_with_handles
|
||||
|
||||
# WRONG: these do not exist and would cause ImportError:
|
||||
# from api.admin.audit import ...
|
||||
# from api.admin import _build_filtered_query_with_handles
|
||||
```
|
||||
|
||||
**`_build_filtered_query_with_handles` signature** (audit.py lines 132–142):
|
||||
```python
|
||||
def _build_filtered_query_with_handles(
|
||||
start: Optional[datetime],
|
||||
end: Optional[datetime],
|
||||
user_uuid: Optional[uuid.UUID],
|
||||
event_type: Optional[str],
|
||||
):
|
||||
```
|
||||
Call with all `None` for unfiltered last-10: `_build_filtered_query_with_handles(None, None, None, None).limit(10)`
|
||||
|
||||
**Security invariant** (from CLAUDE.md + RESEARCH.md §Security Domain): The response dict must never include `password_hash`, `credentials_enc`, `extracted_text`, or document-level content. Use `_audit_to_dict_with_handles` as the serializer for audit entries — it is already security-audited.
|
||||
|
||||
---
|
||||
|
||||
### `backend/api/admin/__init__.py` (config)
|
||||
|
||||
**Analog:** `backend/api/admin/__init__.py` (self — one import + one include_router call)
|
||||
|
||||
**Current content** (lines 1–23):
|
||||
```python
|
||||
"""Admin API package — router aggregator.
|
||||
...
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
from api.admin.users import router as users_router
|
||||
from api.admin.quotas import router as quotas_router
|
||||
from api.admin.ai import router as ai_router
|
||||
|
||||
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
||||
router.include_router(users_router)
|
||||
router.include_router(quotas_router)
|
||||
router.include_router(ai_router)
|
||||
```
|
||||
|
||||
**Modification** — add one import and one `include_router` call following the existing pattern:
|
||||
```python
|
||||
from api.admin.overview import router as overview_router
|
||||
# ...
|
||||
router.include_router(overview_router)
|
||||
```
|
||||
|
||||
**WHY comment to keep** (lines 3–13 of current file): The constraint comment explaining `prefix="/api/admin"` on parent and NO prefix on sub-routers is a non-obvious invariant. Keep it per D-16. This is the comment that prevents the "circular import" and "wrong prefix" pitfalls.
|
||||
|
||||
---
|
||||
|
||||
### `backend/tests/test_admin_overview.py` (test, request-response)
|
||||
|
||||
**Analog:** `backend/tests/test_admin_api.py`
|
||||
|
||||
**Test file header + imports pattern** (test_admin_api.py lines 1–30):
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from db.models import AuditLog, Quota, User
|
||||
from sqlalchemy import select
|
||||
from deps.auth import get_current_admin
|
||||
from deps.db import get_db
|
||||
from services.auth import hash_password
|
||||
from tests.test_auth_api import FakeRedis
|
||||
```
|
||||
|
||||
**`make_admin_user` fixture** (test_admin_api.py lines 34–50 — copy verbatim):
|
||||
```python
|
||||
async def make_admin_user(session: AsyncSession) -> User:
|
||||
user = User(
|
||||
id=uuid.uuid4(),
|
||||
handle=f"admin_{uuid.uuid4().hex[:6]}",
|
||||
email=f"admin_{uuid.uuid4().hex[:6]}@example.com",
|
||||
password_hash=hash_password("AdminPass1!Secret"),
|
||||
role="admin",
|
||||
is_active=True,
|
||||
totp_enabled=False,
|
||||
password_must_change=False,
|
||||
)
|
||||
session.add(user)
|
||||
quota = Quota(user_id=user.id, limit_bytes=104857600, used_bytes=0)
|
||||
session.add(quota)
|
||||
await session.flush()
|
||||
return user
|
||||
```
|
||||
|
||||
**`admin_client` fixture** (test_admin_api.py lines 72–94):
|
||||
```python
|
||||
@pytest_asyncio.fixture
|
||||
async def admin_client(db_session: AsyncSession):
|
||||
from main import app
|
||||
admin = await make_admin_user(db_session)
|
||||
app.dependency_overrides[get_db] = lambda: db_session
|
||||
app.dependency_overrides[get_current_admin] = lambda: admin
|
||||
app.state.redis = FakeRedis()
|
||||
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
|
||||
yield c, admin, db_session
|
||||
app.dependency_overrides.clear()
|
||||
app.state.redis = None
|
||||
```
|
||||
|
||||
**Test pattern** (test_admin_api.py lines 99–133):
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_overview_requires_admin(async_client: AsyncClient):
|
||||
resp = await async_client.get("/api/admin/overview")
|
||||
assert resp.status_code in {401, 403}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_overview_returns_expected_keys(admin_client):
|
||||
client, _admin, _session = admin_client
|
||||
resp = await client.get("/api/admin/overview")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "user_count" in data
|
||||
assert "total_storage_bytes" in data
|
||||
assert "doc_status" in data
|
||||
assert "recent_audit" in data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_overview_no_sensitive_fields(admin_client):
|
||||
client, _admin, _session = admin_client
|
||||
resp = await client.get("/api/admin/overview")
|
||||
assert resp.status_code == 200
|
||||
body = resp.text
|
||||
assert "password_hash" not in body
|
||||
assert "credentials_enc" not in body
|
||||
assert "extracted_text" not in body
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shared Patterns
|
||||
|
||||
### Admin endpoint auth dependency
|
||||
**Source:** `backend/api/admin/users.py` lines 77–81
|
||||
**Apply to:** `backend/api/admin/overview.py`
|
||||
```python
|
||||
async def endpoint_name(
|
||||
session: AsyncSession = Depends(get_db),
|
||||
_admin: User = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
```
|
||||
`get_current_admin` (from `deps.auth`) raises 403 for non-admin tokens and 401 for missing tokens.
|
||||
|
||||
### No-prefix sub-router declaration
|
||||
**Source:** `backend/api/admin/users.py` line 30
|
||||
**Apply to:** `backend/api/admin/overview.py`
|
||||
```python
|
||||
router = APIRouter() # NO prefix — parent __init__.py carries /api/admin (D-04)
|
||||
```
|
||||
|
||||
### Admin response whitelist pattern
|
||||
**Source:** `backend/api/admin/shared.py` lines 13–28
|
||||
**Apply to:** `backend/api/admin/overview.py`
|
||||
```python
|
||||
def _user_to_dict(user: User) -> dict:
|
||||
return {
|
||||
"id": str(user.id),
|
||||
"handle": user.handle,
|
||||
"email": user.email,
|
||||
# ... explicit fields only
|
||||
# NEVER: password_hash, credentials_enc, totp_secret, extracted_text
|
||||
}
|
||||
```
|
||||
The overview endpoint uses `_audit_to_dict_with_handles` from `api.audit` for the `recent_audit` field — the same whitelist principle applies.
|
||||
|
||||
### Vue Router 4 lazy import pattern
|
||||
**Source:** `frontend/src/router/index.js` lines 20–33
|
||||
**Apply to:** All five admin child routes in `router/index.js`
|
||||
```javascript
|
||||
component: () => import('../views/admin/AdminOverviewView.vue')
|
||||
```
|
||||
|
||||
### Vue admin component no-top-padding rule
|
||||
**Source:** `frontend/src/components/admin/AdminUsersTab.vue` line 2
|
||||
**Apply to:** All four extracted views (`AdminUsersView`, `AdminQuotasView`, `AdminAiView`, `AdminAuditView`)
|
||||
```vue
|
||||
<template>
|
||||
<div> <!-- no p-8, no max-w — AdminLayout owns the content padding -->
|
||||
```
|
||||
|
||||
### `to.matched.some()` guard idiom
|
||||
**Source:** `frontend/src/router/index.js` (replacement for current line 91)
|
||||
**Apply to:** `frontend/src/router/index.js` `beforeEach` guard
|
||||
```javascript
|
||||
const isAdminRoute = to.matched.some(r => r.meta.requiresAdmin)
|
||||
```
|
||||
Never use `to.meta.requiresAdmin` for child routes — it is only set on the parent and is `undefined` on children in Vue Router 4.
|
||||
|
||||
---
|
||||
|
||||
## No Analog Found
|
||||
|
||||
All 14 files have analogs. No entries in this section.
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
**Analog search scope:** `frontend/src/layouts/`, `frontend/src/components/`, `frontend/src/views/`, `frontend/src/router/`, `frontend/src/stores/`, `frontend/tailwind.config.js`, `backend/api/admin/`, `backend/api/audit.py`, `backend/tests/`
|
||||
**Files scanned:** 14 analog files read directly
|
||||
**Pattern extraction date:** 2026-06-12
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
phase: 9
|
||||
slug: admin-panel-rearchitecture
|
||||
status: draft
|
||||
nyquist_compliant: false
|
||||
wave_0_complete: false
|
||||
created: 2026-06-12
|
||||
---
|
||||
|
||||
# Phase 9 — Validation Strategy
|
||||
|
||||
> Per-phase validation contract for feedback sampling during execution.
|
||||
|
||||
---
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Framework** | pytest (backend) + manual browser/vitest (frontend) |
|
||||
| **Config file** | `backend/pytest.ini` |
|
||||
| **Quick run command** | `cd backend && pytest tests/test_admin_overview.py -x` |
|
||||
| **Full suite command** | `cd backend && pytest -v` |
|
||||
| **Estimated runtime** | ~60 seconds |
|
||||
|
||||
---
|
||||
|
||||
## Sampling Rate
|
||||
|
||||
- **After every task commit:** Run `cd backend && pytest tests/test_admin_overview.py -x` (after overview endpoint tasks)
|
||||
- **After every plan wave:** Run `cd backend && pytest -v`
|
||||
- **Before `/gsd:verify-work`:** Full suite must be green
|
||||
- **Max feedback latency:** 60 seconds
|
||||
|
||||
---
|
||||
|
||||
## Per-Task Verification Map
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||
| overview-endpoint | 09-0N | Wave 1 | ADMIN-11 | Admin data leak | Response never contains `credentials_enc`, doc content | Integration | `pytest tests/test_admin_overview.py::test_overview_aggregate` | No — Wave 0 gap | ❌ |
|
||||
| overview-no-sensitive | 09-0N | Wave 1 | ADMIN-11 | Sensitive field exposure | `credentials_enc` and document content absent from response | Security | `pytest tests/test_admin_overview.py::test_overview_no_sensitive_fields` | No — Wave 0 gap | ❌ |
|
||||
| admin-guard | 09-0N | Wave 2 | ADMIN-12 | Privilege escalation | Non-admin navigating to `/admin/users` redirected to `/` | Integration (browser) | Manual browser test or Playwright | N/A | ❌ |
|
||||
| adminview-deleted | 09-0N | Wave 3 | ADMIN-08 | Dead code | `AdminView.vue` absent from repo | Static | `grep -r "AdminView" frontend/src/ \| grep -v AdminLayout \| grep -v AdminOverview` | N/A | ❌ |
|
||||
| tailwind-safelist | 09-0N | Wave 3 | CODE-06 | Visual regression | Dynamic provider color classes render in prod build | Build | `npm run build` + visual check | N/A | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 Gaps
|
||||
|
||||
Test files that must be created before feature code:
|
||||
|
||||
- `backend/tests/test_admin_overview.py` — covers ADMIN-11 aggregate query + security invariants (no sensitive fields)
|
||||
|
||||
---
|
||||
|
||||
## Security Invariants (Must All Pass)
|
||||
|
||||
- [ ] `GET /api/admin/overview` never returns `credentials_enc`, `password_hash`, or document content
|
||||
- [ ] `GET /api/admin/overview` is admin-only (`get_current_admin` dep enforced)
|
||||
- [ ] Non-admin user accessing `/admin/*` routes is redirected to `/` by `beforeEach` guard
|
||||
- [ ] Admin user accessing non-admin routes (`/`, `/settings`, etc.) is redirected to `/admin`
|
||||
Reference in New Issue
Block a user