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:
curo1305
2026-06-12 15:34:31 +02:00
co-authored by Claude Sonnet 4.6
parent 373e009ed8
commit ba30ada87a
8 changed files with 1897 additions and 2 deletions
@@ -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>