From 5dbfb6c1d86f04d06fa17371f2fecb81cf0c8e7a Mon Sep 17 00:00:00 2001 From: curo1305 Date: Fri, 12 Jun 2026 16:07:56 +0200 Subject: [PATCH 1/4] feat(09-04): rewire /admin nested route + guard + Tailwind safelist - Replace flat /admin route with nested AdminLayout subtree (5 children) - Fix beforeEach guard to use to.matched.some(r => r.meta.requiresAdmin) - Add D-09 admin-on-user-route redirect and D-10a non-admin-on-admin redirect - Add Tailwind safelist covering sky+amber color families (formatters + AuditLogTab) --- frontend/src/router/index.js | 34 ++++++++++++++++++++++++++++++---- frontend/tailwind.config.js | 4 ++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 4f7226e..c9c8f9f 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -39,7 +39,22 @@ const routes = [ // Phase 2 — authenticated routes { path: '/account', redirect: '/settings' }, - { path: '/admin', component: () => import('../views/AdminView.vue'), meta: { requiresAdmin: true } }, + + // Admin panel — standalone route subtree with AdminLayout as the route component. + // Children do NOT re-declare requiresAdmin — the guard uses to.matched.some() to + // inherit from the parent (Vue Router 4 does not propagate meta to children). + { + 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') }, + ], + }, // Cloud storage overview and folder browser { @@ -75,11 +90,13 @@ const router = createRouter({ routes, }) -// Navigation guard (D-10): redirect unauthenticated users to /login. +// Navigation guard — enforces auth and admin role separation (D-09, D-10). // On page reload the access token is gone (memory-only per CLAUDE.md), so we attempt -// a silent refresh via the httpOnly cookie before concluding the session is gone. +// a silent refresh via the httpOnly cookie before any role checks fire. router.beforeEach(async (to) => { const authStore = useAuthStore() + + // Step 1: ensure access token is present for non-public routes. if (!to.meta.public && !authStore.accessToken) { try { await authStore.refresh() @@ -88,9 +105,18 @@ router.beforeEach(async (to) => { } } - if (to.meta.requiresAdmin && authStore.user?.role !== 'admin') { + const isAdminRoute = to.matched.some(r => r.meta.requiresAdmin) + const isAdmin = authStore.user?.role === 'admin' + + // D-10a: non-admin attempting an admin route → redirect to /. + if (isAdminRoute && !isAdmin) { return { path: '/' } } + + // D-09: admin attempting a non-admin, non-public route → redirect to /admin. + if (!isAdminRoute && !to.meta.public && isAdmin) { + return { path: '/admin' } + } }) export default router diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index d3b42c0..dc246f1 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -2,6 +2,10 @@ 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: {}, }, From bdd68b2edffbcebac25f7bb5e9370f7935f14f01 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Fri, 12 Jun 2026 16:08:25 +0200 Subject: [PATCH 2/4] feat(09-04): wire admin login redirect in LoginView.vue (D-08) - After successful login, admin users redirect to /admin; regular users to / - Honors ?redirect= query param for non-admin users as before - authStore.user.role is synchronously populated before handleLoginResult fires --- frontend/src/views/auth/LoginView.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/auth/LoginView.vue b/frontend/src/views/auth/LoginView.vue index b559433..ab04e3d 100644 --- a/frontend/src/views/auth/LoginView.vue +++ b/frontend/src/views/auth/LoginView.vue @@ -216,8 +216,9 @@ function resetToPassword() { async function handleLoginResult(result) { if (!result) { - // Full success — tokens set in store - const redirect = route.query.redirect || '/' + // Full success — tokens set in store; admin users land at /admin (D-08) + const defaultRedirect = authStore.user?.role === 'admin' ? '/admin' : '/' + const redirect = route.query.redirect || defaultRedirect await router.push(redirect) return } From e6467d18cf48416ea66349554701abc58ae6ee99 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Fri, 12 Jun 2026 16:11:00 +0200 Subject: [PATCH 3/4] feat(09-04): delete AdminView + 4 tab files; update tests + router guard test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - git rm AdminView.vue, AdminUsersTab.vue, AdminQuotasTab.vue, AdminAiConfigTab.vue, AuditLogTab.vue - Migrate tab tests to import from views/admin/Admin*View.vue - Update router.guard.test.js: replace AdminView mock with AdminLayout + 5 admin view mocks - Add D-09 redirect test (admin on / → /admin) - Fix stale api/admin.js JSDoc consumer list - Fix SettingsView.vue stale AdminView comment reference --- frontend/src/api/admin.js | 4 +- .../src/components/admin/AdminAiConfigTab.vue | 391 -------------- .../src/components/admin/AdminQuotasTab.vue | 182 ------- .../src/components/admin/AdminUsersTab.vue | 480 ------------------ frontend/src/components/admin/AuditLogTab.vue | 346 ------------- .../admin/__tests__/AdminAiConfigTab.test.js | 2 +- .../admin/__tests__/AdminQuotasTab.test.js | 2 +- .../admin/__tests__/AdminUsersTab.test.js | 2 +- .../src/router/__tests__/router.guard.test.js | 21 +- frontend/src/views/AdminView.vue | 43 -- frontend/src/views/SettingsView.vue | 2 +- 11 files changed, 26 insertions(+), 1449 deletions(-) delete mode 100644 frontend/src/components/admin/AdminAiConfigTab.vue delete mode 100644 frontend/src/components/admin/AdminQuotasTab.vue delete mode 100644 frontend/src/components/admin/AdminUsersTab.vue delete mode 100644 frontend/src/components/admin/AuditLogTab.vue delete mode 100644 frontend/src/views/AdminView.vue diff --git a/frontend/src/api/admin.js b/frontend/src/api/admin.js index 028ce79..cf25da0 100644 --- a/frontend/src/api/admin.js +++ b/frontend/src/api/admin.js @@ -1,8 +1,8 @@ /** * Admin API — user management, quota, AI config, audit log, daily exports. * - * Consumers: AdminUsersTab.vue, AdminQuotasTab.vue, AdminAiConfigTab.vue, - * AuditLogTab.vue, AdminDailyExportsTab.vue + * Consumers: AdminUsersView.vue, AdminQuotasView.vue, AdminAiView.vue, + * AdminAuditView.vue */ import { request, fetchWithRetry } from './utils.js' diff --git a/frontend/src/components/admin/AdminAiConfigTab.vue b/frontend/src/components/admin/AdminAiConfigTab.vue deleted file mode 100644 index e0eed21..0000000 --- a/frontend/src/components/admin/AdminAiConfigTab.vue +++ /dev/null @@ -1,391 +0,0 @@ - - - diff --git a/frontend/src/components/admin/AdminQuotasTab.vue b/frontend/src/components/admin/AdminQuotasTab.vue deleted file mode 100644 index 7babeb4..0000000 --- a/frontend/src/components/admin/AdminQuotasTab.vue +++ /dev/null @@ -1,182 +0,0 @@ - - - diff --git a/frontend/src/components/admin/AdminUsersTab.vue b/frontend/src/components/admin/AdminUsersTab.vue deleted file mode 100644 index 289ae61..0000000 --- a/frontend/src/components/admin/AdminUsersTab.vue +++ /dev/null @@ -1,480 +0,0 @@ - - - diff --git a/frontend/src/components/admin/AuditLogTab.vue b/frontend/src/components/admin/AuditLogTab.vue deleted file mode 100644 index 6e110c8..0000000 --- a/frontend/src/components/admin/AuditLogTab.vue +++ /dev/null @@ -1,346 +0,0 @@ - - - diff --git a/frontend/src/components/admin/__tests__/AdminAiConfigTab.test.js b/frontend/src/components/admin/__tests__/AdminAiConfigTab.test.js index 8c0d0af..2e0f365 100644 --- a/frontend/src/components/admin/__tests__/AdminAiConfigTab.test.js +++ b/frontend/src/components/admin/__tests__/AdminAiConfigTab.test.js @@ -7,7 +7,7 @@ vi.mock('../../../api/client.js', () => ({ adminUpdateAiConfig: vi.fn(), })) -import AdminAiConfigTab from '../AdminAiConfigTab.vue' +import AdminAiConfigTab from '../../../views/admin/AdminAiView.vue' import * as api from '../../../api/client.js' function makeUser(overrides = {}) { diff --git a/frontend/src/components/admin/__tests__/AdminQuotasTab.test.js b/frontend/src/components/admin/__tests__/AdminQuotasTab.test.js index ed57863..fc82868 100644 --- a/frontend/src/components/admin/__tests__/AdminQuotasTab.test.js +++ b/frontend/src/components/admin/__tests__/AdminQuotasTab.test.js @@ -8,7 +8,7 @@ vi.mock('../../../api/client.js', () => ({ adminUpdateQuota: vi.fn(), })) -import AdminQuotasTab from '../AdminQuotasTab.vue' +import AdminQuotasTab from '../../../views/admin/AdminQuotasView.vue' import * as api from '../../../api/client.js' const MB = 1048576 diff --git a/frontend/src/components/admin/__tests__/AdminUsersTab.test.js b/frontend/src/components/admin/__tests__/AdminUsersTab.test.js index cfa67e9..f116353 100644 --- a/frontend/src/components/admin/__tests__/AdminUsersTab.test.js +++ b/frontend/src/components/admin/__tests__/AdminUsersTab.test.js @@ -11,7 +11,7 @@ vi.mock('../../../api/client.js', () => ({ adminCreateUser: vi.fn(), })) -import AdminUsersTab from '../AdminUsersTab.vue' +import AdminUsersTab from '../../../views/admin/AdminUsersView.vue' import * as api from '../../../api/client.js' function makeUser(overrides = {}) { diff --git a/frontend/src/router/__tests__/router.guard.test.js b/frontend/src/router/__tests__/router.guard.test.js index 99336fc..036a88c 100644 --- a/frontend/src/router/__tests__/router.guard.test.js +++ b/frontend/src/router/__tests__/router.guard.test.js @@ -11,7 +11,12 @@ vi.mock('../../views/auth/LoginView.vue', () => ({ default: { template: '
vi.mock('../../views/auth/RegisterView.vue', () => ({ default: { template: '
' } })) vi.mock('../../views/auth/PasswordResetView.vue', () => ({ default: { template: '
' } })) vi.mock('../../views/auth/NewPasswordView.vue', () => ({ default: { template: '
' } })) -vi.mock('../../views/AdminView.vue', () => ({ default: { template: '
' } })) +vi.mock('../../layouts/AdminLayout.vue', () => ({ default: { template: '
' } })) +vi.mock('../../views/admin/AdminOverviewView.vue', () => ({ default: { template: '
' } })) +vi.mock('../../views/admin/AdminUsersView.vue', () => ({ default: { template: '
' } })) +vi.mock('../../views/admin/AdminQuotasView.vue', () => ({ default: { template: '
' } })) +vi.mock('../../views/admin/AdminAiView.vue', () => ({ default: { template: '
' } })) +vi.mock('../../views/admin/AdminAuditView.vue', () => ({ default: { template: '
' } })) vi.mock('../../views/SharedView.vue', () => ({ default: { template: '
' } })) // Heavy view components imported statically — stub them too @@ -78,4 +83,18 @@ describe('router — admin guard (SEC-07)', () => { await router.push('/admin') expect(router.currentRoute.value.path).toBe('/admin') }) + + it('redirects an admin user away from / to /admin (D-09)', async () => { + // Arrange: admin trying to navigate to a regular user route + useAuthStore.mockReturnValue({ + accessToken: 'fake-token', + user: { id: '1', role: 'admin' }, + refresh: vi.fn().mockResolvedValue(undefined), + }) + + // Act: admin attempts to visit the regular user home + await router.push('/') + // The D-09 guard branch returns { path: '/admin' } + expect(router.currentRoute.value.path).toBe('/admin') + }) }) diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue deleted file mode 100644 index 22dae05..0000000 --- a/frontend/src/views/AdminView.vue +++ /dev/null @@ -1,43 +0,0 @@ - - - diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index ec1bd19..616f5b3 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -27,7 +27,7 @@
- +