From 5dbfb6c1d86f04d06fa17371f2fecb81cf0c8e7a Mon Sep 17 00:00:00 2001 From: curo1305 Date: Fri, 12 Jun 2026 16:07:56 +0200 Subject: [PATCH] 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: {}, },