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)
This commit is contained in:
curo1305
2026-06-12 16:07:56 +02:00
parent b19ab8bcd1
commit 5dbfb6c1d8
2 changed files with 34 additions and 4 deletions
+30 -4
View File
@@ -39,7 +39,22 @@ const routes = [
// Phase 2 — authenticated routes // Phase 2 — authenticated routes
{ path: '/account', redirect: '/settings' }, { 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 // Cloud storage overview and folder browser
{ {
@@ -75,11 +90,13 @@ const router = createRouter({
routes, 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 // 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) => { router.beforeEach(async (to) => {
const authStore = useAuthStore() const authStore = useAuthStore()
// Step 1: ensure access token is present for non-public routes.
if (!to.meta.public && !authStore.accessToken) { if (!to.meta.public && !authStore.accessToken) {
try { try {
await authStore.refresh() 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: '/' } 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 export default router
+4
View File
@@ -2,6 +2,10 @@
import forms from '@tailwindcss/forms' import forms from '@tailwindcss/forms'
export default { export default {
content: ['./index.html', './src/**/*.{vue,js}'], 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: { theme: {
extend: {}, extend: {},
}, },