Moves phases 08–11 execution artifacts from .planning/phases/ to .planning/milestones/v0.2-phases/ to keep .planning/phases/ clean for the next milestone. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
14 KiB
phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 09 | 04 | frontend-router-auth |
|
|
|
|
|
|
Phase 09 Plan 04: Router Rewire, Login Redirect, Cleanup Summary
One-liner: Nested /admin route subtree wired with AdminLayout as component, to.matched.some() guard fixed, D-08/D-09/D-10 strict admin separation enforced, Tailwind safelist installed for sky+amber color families, and five legacy files deleted with tests migrated.
Tasks Completed
| Task | Name | Commit | Files |
|---|---|---|---|
| 1 | Rewire router/index.js + Tailwind safelist | 5dbfb6c |
frontend/src/router/index.js, frontend/tailwind.config.js |
| 2 | Wire admin login redirect in LoginView.vue | bdd68b2 |
frontend/src/views/auth/LoginView.vue |
| 3 | Delete AdminView.vue + 4 tab files; migrate tests | e6467d1 |
5 deletions, 6 modified (tests + api comment + settings comment) |
What Was Built
Task 1 — Nested /admin Route + Guard + Tailwind Safelist
Router rewire: The flat /admin route pointing to the deleted AdminView.vue is replaced with a nested route whose component is the lazy-loaded AdminLayout.vue. Five children are registered:
{ path: '', AdminOverviewView.vue } → /admin
{ path: 'users', AdminUsersView.vue } → /admin/users
{ path: 'quotas', AdminQuotasView.vue } → /admin/quotas
{ path: 'ai', AdminAiView.vue } → /admin/ai
{ path: 'audit', AdminAuditView.vue } → /admin/audit
Only the parent carries meta: { requiresAdmin: true }. Child routes intentionally do not repeat it — the guard uses to.matched.some(r => r.meta.requiresAdmin) which walks the full matched ancestors array, making the parent's meta apply to every child.
Guard fix: The old if (to.meta.requiresAdmin && ...) check is replaced with a 5-step guard:
- Silent refresh on non-public routes with no access token
- Compute
isAdminRoute = to.matched.some(r => r.meta.requiresAdmin) - Compute
isAdmin = authStore.user?.role === 'admin' - D-10a:
isAdminRoute && !isAdmin→ redirect{ path: '/' } - D-09:
!isAdminRoute && !to.meta.public && isAdmin→ redirect{ path: '/admin' }
The !to.meta.public clause in step 5 ensures auth routes (/login, /register, /password-reset, /password-reset/confirm) remain reachable for admin users — this is the critical escape hatch that prevents redirect loops when an admin session expires and the guard sends them to /login.
Tailwind safelist: Two regex patterns added covering all dynamic class families:
bg-(blue|sky|green|purple|orange|amber|gray|indigo|red)-(50|100|500|600)— coversproviderBg()(OneDrive=sky, Google Drive=blue, Nextcloud=orange, WebDAV=gray) andactionTypeClass()(auth=blue, folder/share=purple, admin=amber, document=gray)text-(blue|sky|green|purple|orange|amber|gray|indigo|red)-(400|500|600|700)— coversproviderColor()andactionTypeClass()text variants
sky and amber were the two families missing from the original D-14 draft. RESEARCH.md Pattern 7 (confirmed in CONTEXT.md) corrects this — both are included.
Task 2 — Admin Login Redirect (D-08)
The handleLoginResult(!result) branch in LoginView.vue is updated:
const defaultRedirect = authStore.user?.role === 'admin' ? '/admin' : '/'
const redirect = route.query.redirect || defaultRedirect
await router.push(redirect)
authStore.user.role is synchronously populated inside authStore.login() before the store action returns (verified by reading auth.js line 82: user.value = data.user), so the role check fires with a fully populated user object.
The ?redirect= query param is still honored for non-admin users. For admin users, the D-09 guard in the router would intercept any ?redirect=/ attempt and redirect back to /admin anyway — the D-08 check here is belt-and-suspenders.
The requires_totp and requires_password_change branches are byte-for-byte unchanged.
Task 3 — Delete Legacy Files + Migrate Tests
Five files deleted via git rm:
frontend/src/views/AdminView.vue— old tab-container view, replaced byAdminLayout+ nested routesfrontend/src/components/admin/AdminUsersTab.vue— promoted toviews/admin/AdminUsersView.vuein 09-03frontend/src/components/admin/AdminQuotasTab.vue— promoted toviews/admin/AdminQuotasView.vuein 09-03frontend/src/components/admin/AdminAiConfigTab.vue— promoted toviews/admin/AdminAiView.vuein 09-03frontend/src/components/admin/AuditLogTab.vue— promoted toviews/admin/AdminAuditView.vuein 09-03
Test migrations (import path rewrite only — no test logic changed):
__tests__/AdminUsersTab.test.js:import AdminUsersTab from '../AdminUsersTab.vue'→from '../../../views/admin/AdminUsersView.vue'__tests__/AdminQuotasTab.test.js:import AdminQuotasTab from '../AdminQuotasTab.vue'→from '../../../views/admin/AdminQuotasView.vue'__tests__/AdminAiConfigTab.test.js:import AdminAiConfigTab from '../AdminAiConfigTab.vue'→from '../../../views/admin/AdminAiView.vue'
Router guard test updated:
- Replaced
vi.mock('../../views/AdminView.vue', ...)with mocks forlayouts/AdminLayout.vueand all fiveviews/admin/Admin*View.vuefiles - Added D-09 redirect test: admin navigating to
/is redirected to/admin
Two stale comments updated (Rule 2 — correctness):
src/api/admin.jsJSDoc consumer list updated fromAdminXxxTab.vuenames toAdmin*View.vuenamessrc/views/SettingsView.vuestale<!-- Tab strip (copy AdminView pattern verbatim) -->comment simplified to<!-- Tab strip -->
Acceptance Criteria Results
| Criterion | Result |
|---|---|
grep -c "to.matched.some(r => r.meta.requiresAdmin)" router/index.js ≥ 1 |
1 — PASS |
grep -c "to.meta.requiresAdmin" router/index.js = 0 |
0 — PASS |
grep -c "AdminLayout" router/index.js ≥ 1 |
2 — PASS |
| All 5 Admin*View imports present in router | PASS |
grep -c "AdminView" router/index.js = 0 |
0 — PASS |
grep -c "safelist" tailwind.config.js = 1 |
1 — PASS |
grep -c "amber" tailwind.config.js ≥ 1 |
2 — PASS |
grep -c "sky" tailwind.config.js ≥ 1 |
2 — PASS |
grep -c "authStore.user?.role === 'admin'" LoginView.vue = 1 |
1 — PASS |
grep -c "defaultRedirect" LoginView.vue ≥ 2 |
2 — PASS |
| None of 5 deleted files exist | PASS |
No stale AdminXxxTab import references remain |
PASS |
npm run build succeeds |
881 ms, 0 errors — PASS |
npm test passes |
137/137 tests — PASS |
Smoke Check (Build Artifacts)
npm run build produced separate lazy-loaded chunks for all 5 admin routes:
AdminLayout-BbuVmUU8.js(4.46 kB) — AdminLayout entry chunkadmin-BFKH-0jn.js(3.04 kB) — shared admin chunkAdminOverviewView-DUqzmtNA.js(3.49 kB)AdminQuotasView-Cao0PSry.js(4.53 kB)AdminAuditView-DDh7qqQi.js(9.24 kB)AdminUsersView-BLLys8CV.js(12.62 kB)AdminAiView-CZ8yB8IZ.js(16.43 kB)
AdminAuditView chunk contains the full actionTypeClass() logic with bg-amber-50 text-amber-700 and bg-blue-50 text-blue-600 classes — confirmed present in build output. Tailwind safelist ensures these survive tree-shaking in production.
Deviations from Plan
Auto-fixed Issues
[Rule 2 - Missing Critical Functionality] Added D-09 redirect test to router guard test
- Found during: Task 3 (router guard test update)
- Issue: The test file verified the non-admin →
/redirect (D-10a) but had no test for the admin →/adminredirect (D-09). D-09 is a security-relevant guard branch — omitting it from the test suite is a missing correctness invariant. - Fix: Added
it('redirects an admin user away from / to /admin (D-09)', ...)test with admin role mock navigating to/and assertingrouter.currentRoute.value.path === '/admin'. - Files modified:
frontend/src/router/__tests__/router.guard.test.js - Commit:
e6467d1
[Rule 1 - Bug] Stale JSDoc consumer list in api/admin.js
- Found during: Task 3 (grep scan for stale references)
- Issue:
src/api/admin.jsJSDoc listed the deletedAdminXxxTab.vueandAuditLogTab.vuenames as consumers. Post-deletion, these filenames no longer exist — an inaccurate comment is a correctness issue. - Fix: Updated JSDoc consumer list to
AdminUsersView.vue, AdminQuotasView.vue, AdminAiView.vue, AdminAuditView.vue. - Files modified:
frontend/src/api/admin.js - Commit:
e6467d1
[Rule 1 - Bug] Stale AdminView reference in SettingsView.vue comment
- Found during: Task 3 (grep scan for
AdminViewreferences) - Issue:
src/views/SettingsView.vuecontained<!-- Tab strip (copy AdminView pattern verbatim) -->. AfterAdminView.vueis deleted, this comment references a non-existent file. - Fix: Simplified to
<!-- Tab strip -->. - Files modified:
frontend/src/views/SettingsView.vue - Commit:
e6467d1
[Rule 3 - Blocking] npm not installed in worktree frontend — installed deps locally
- Found during: Task 1 verification (npm run build)
- Issue: The worktree's
frontend/directory had nonode_modules/.npm run buildfailed withERR_MODULE_NOT_FOUNDfor vite. - Fix: Ran
npm install+npm approve-scripts --allow-scripts-pendingin the worktree'sfrontend/. Build succeeded after this. - Files modified: None (dev-time infrastructure only —
node_modules/is gitignored).
Known Stubs
None — no hardcoded data or placeholder content introduced. All admin routes resolve to the real view components from 09-02 and 09-03.
Threat Flags
None — no new network endpoints, API routes, or trust boundaries introduced. This plan is purely frontend routing and build configuration.
Threat mitigations from the plan's threat register:
- T-09-04-01 (Elevation of Privilege):
to.matched.some(r => r.meta.requiresAdmin)guard covers all/admin/*child routes — IMPLEMENTED. Backendget_current_admindependency remains the authoritative second gate. - T-09-04-02 (Privilege Escalation via ?redirect=): D-08 puts role check FIRST; even
?redirect=/for an admin routes through the D-09 guard — IMPLEMENTED. - T-09-04-03 (CSS purge of dynamic classes): Tailwind safelist covers
sky(OneDrive) andamber(audit admin badge) — IMPLEMENTED. Build output confirms separate admin chunks exist. - T-09-04-04 (Redirect loop):
isAdminRoutefor/admin/*short-circuits D-09 so admins on admin routes are not redirected; auth routes have!to.meta.publicguard — IMPLEMENTED.
Self-Check: PASSED
frontend/src/router/index.js— MODIFIED (nested /admin + corrected guard)frontend/tailwind.config.js— MODIFIED (safelist added)frontend/src/views/auth/LoginView.vue— MODIFIED (admin login redirect)frontend/src/views/AdminView.vue— DELETEDfrontend/src/components/admin/AdminUsersTab.vue— DELETEDfrontend/src/components/admin/AdminQuotasTab.vue— DELETEDfrontend/src/components/admin/AdminAiConfigTab.vue— DELETEDfrontend/src/components/admin/AuditLogTab.vue— DELETED- Commit
5dbfb6c(Task 1) — FOUND - Commit
bdd68b2(Task 2) — FOUND - Commit
e6467d1(Task 3) — FOUND npm run buildsucceeds (0 errors, 5 admin chunks in output) — PASSEDnpm test— 137/137 tests passed — PASSED- No stale
AdminView/AdminXxxTabimports remain — PASSED