- 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)
123 lines
3.9 KiB
JavaScript
123 lines
3.9 KiB
JavaScript
import { createRouter, createWebHistory } from 'vue-router'
|
|
import { useAuthStore } from '../stores/auth.js'
|
|
import FileManagerView from '../views/FileManagerView.vue'
|
|
import TopicsView from '../views/TopicsView.vue'
|
|
import DocumentView from '../views/DocumentView.vue'
|
|
import SettingsView from '../views/SettingsView.vue'
|
|
import CloudFolderView from '../views/CloudFolderView.vue'
|
|
import CloudStorageView from '../views/CloudStorageView.vue'
|
|
|
|
const routes = [
|
|
// File manager is the home — handles both root and folder views
|
|
{ path: '/', component: FileManagerView },
|
|
{ path: '/topics', component: TopicsView },
|
|
{ path: '/topics/:name', component: TopicsView },
|
|
{ path: '/document/:id', component: DocumentView },
|
|
{ path: '/settings', component: SettingsView },
|
|
|
|
// Phase 2 — public auth routes (no guard)
|
|
{
|
|
path: '/login',
|
|
component: () => import('../views/auth/LoginView.vue'),
|
|
meta: { public: true, layout: 'auth' },
|
|
},
|
|
{
|
|
path: '/register',
|
|
component: () => import('../views/auth/RegisterView.vue'),
|
|
meta: { public: true, layout: 'auth' },
|
|
},
|
|
{
|
|
path: '/password-reset',
|
|
component: () => import('../views/auth/PasswordResetView.vue'),
|
|
meta: { public: true, layout: 'auth' },
|
|
},
|
|
{
|
|
path: '/password-reset/confirm',
|
|
component: () => import('../views/auth/NewPasswordView.vue'),
|
|
meta: { public: true, layout: 'auth' },
|
|
},
|
|
|
|
// Phase 2 — authenticated routes
|
|
{ path: '/account', redirect: '/settings' },
|
|
|
|
// 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
|
|
{
|
|
path: '/cloud',
|
|
name: 'cloud',
|
|
component: CloudStorageView,
|
|
meta: { requiresAuth: true },
|
|
},
|
|
{
|
|
path: '/cloud/:provider/:folderId(.*)',
|
|
name: 'cloud-folder',
|
|
component: CloudFolderView,
|
|
meta: { requiresAuth: true },
|
|
},
|
|
|
|
// Phase 4 — folder and sharing routes
|
|
{
|
|
path: '/folders/:folderId',
|
|
name: 'folder',
|
|
component: FileManagerView,
|
|
meta: { requiresAuth: true },
|
|
},
|
|
{
|
|
path: '/shared',
|
|
name: 'shared',
|
|
component: () => import('../views/SharedView.vue'),
|
|
meta: { requiresAuth: true },
|
|
},
|
|
]
|
|
|
|
const router = createRouter({
|
|
history: createWebHistory(),
|
|
routes,
|
|
})
|
|
|
|
// 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 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()
|
|
} catch {
|
|
return { path: '/login', query: { redirect: to.fullPath } }
|
|
}
|
|
}
|
|
|
|
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
|