import { createRouter, createWebHistory } from 'vue-router'; import { useAuthStore } from '../stores/auth'; import { useSmokeTestsAllowed } from '../composables/useSmokeTestsAllowed'; import { ensureStaffSession, isSessionFresh } from '../utils/session-hydrate'; import { reconcileStaffSessionFromToken } from '../stores/auth'; import { AdminPerm } from '../constants/permissions'; import { adminCanAccess, firstAdminFallback } from '../utils/admin-access'; /** Paths denied for specific admin roles (SUPER_ADMIN bypasses). */ const ROLE_ROUTE_DENY: Record = { '/finance-logs': ['MATCH_ADMIN'], '/cashback': ['MATCH_ADMIN', 'SUPPORT'], }; function pathDeniedForRole(path: string, role?: string): boolean { if (!role || role === 'SUPER_ADMIN') return false; const base = path.split('?')[0]; for (const [prefix, roles] of Object.entries(ROLE_ROUTE_DENY)) { if ((base === prefix || base.startsWith(`${prefix}/`)) && roles.includes(role)) { return true; } } return false; } const router = createRouter({ history: createWebHistory(), routes: [ { path: '/login', component: () => import('../views/Login.vue'), meta: { public: true } }, { path: '/', component: () => import('../layouts/ManageLayout.vue'), meta: { auth: true }, children: [ { path: '', component: () => import('../views/HomeEntry.vue'), meta: { permissions: [AdminPerm.reports] }, children: [ { path: '', component: () => import('../views/dashboard/DashboardMatches.vue'), }, { path: 'dashboard/players', component: () => import('../views/dashboard/DashboardPlayers.vue'), }, ], }, { path: 'invites', redirect: '/users', }, { path: 'users', component: () => import('../views/AgentManager.vue'), meta: { adminOnly: true, permissions: [AdminPerm.usersView, AdminPerm.agentsView] }, }, { path: 'finance-logs', component: () => import('../views/FinanceLogs.vue'), meta: { permissions: [AdminPerm.reports] }, }, { path: 'agent-credit-transactions', redirect: (to) => ({ path: '/finance-logs', query: { ...to.query, tab: 'credit' }, }), }, { path: 'agents', redirect: '/users', }, { path: 'matches', component: () => import('../views/Matches.vue'), meta: { adminOnly: true, permissions: [AdminPerm.matches] }, }, { path: 'matches/outrights', component: () => import('../views/MatchesOutrights.vue'), meta: { adminOnly: true, permissions: [AdminPerm.matches] }, }, { path: 'matches/market-templates', component: () => import('../views/MarketTemplates.vue'), meta: { adminOnly: true, permissions: [AdminPerm.matches] }, }, { path: 'matches/audit-logs', component: () => import('../views/MatchesAudit.vue'), meta: { adminOnly: true, permissions: [AdminPerm.matches] }, }, { path: 'matches/:matchId/edit', name: 'admin-match-edit', component: () => import('../views/matches/MatchEventEditor.vue'), meta: { adminOnly: true, permissions: [AdminPerm.matches] }, }, { path: 'matches/:matchId/markets', name: 'admin-match-markets', component: () => import('../views/matches/MatchMarketsPage.vue'), meta: { adminOnly: true, permissions: [AdminPerm.matches] }, }, { path: 'outrights', redirect: '/matches/outrights' }, { path: 'outrights/:matchId/edit', name: 'admin-outright-edit', component: () => import('../views/outrights/OutrightEditRedirect.vue'), meta: { adminOnly: true, permissions: [AdminPerm.matches] }, }, { path: 'world-cup-outright', redirect: '/matches/outrights' }, { path: 'bets', component: () => import('../views/Bets.vue'), meta: { adminOnly: true, permissions: [AdminPerm.bets] }, }, { path: 'settlement/:id', component: () => import('../views/Settlement.vue'), meta: { adminOnly: true, permissions: [AdminPerm.settlement, AdminPerm.matches] }, }, { path: 'cashback', component: () => import('../views/Cashback.vue'), meta: { adminOnly: true, permissions: [AdminPerm.cashback] }, }, { path: 'contents', component: () => import('../views/Contents.vue'), meta: { adminOnly: true, permissions: [AdminPerm.content] }, }, { path: 'audit', component: () => import('../views/Audit.vue'), meta: { adminOnly: true, permissions: [AdminPerm.audit] }, }, { path: 'smoke-tests', component: () => import('../views/SmokeTests.vue'), meta: { adminOnly: true, smokeTestsOnly: true, permissions: [AdminPerm.settings] }, }, { path: 'media', component: () => import('../views/MediaLibrary.vue'), meta: { adminOnly: true, permissions: [AdminPerm.content, AdminPerm.matches] }, }, { path: 'payment-methods', redirect: (to) => ({ path: '/deposit', query: { ...to.query, tab: 'methods' }, }), }, { path: 'deposit-orders', redirect: '/deposit', }, { path: 'deposit', component: () => import('../views/DepositManage.vue'), meta: { adminOnly: true, permissions: [AdminPerm.depositManage, AdminPerm.depositReview] }, }, { path: 'staff', component: () => import('../views/StaffManage.vue'), meta: { adminOnly: true, permissions: [AdminPerm.settings] }, }, { path: 'my-players', component: () => import('../views/agent/Players.vue'), meta: { agentOnly: true }, }, { path: 'sub-agents', redirect: '/my-players', }, { path: 'my-bets', component: () => import('../views/agent/Bets.vue'), meta: { agentOnly: true }, }, ], }, ], }); router.beforeEach(async (to) => { const auth = useAuthStore(); const hasToken = !!auth.token.value; if (hasToken) { if (isSessionFresh()) { // Session 在 TTL 内且完整:只做同步 reconcile,跳过网络请求 reconcileStaffSessionFromToken(); } else { // Session 过期或不完整:才 await 远程刷新 await ensureStaffSession(); } } const hasUser = !!auth.user.value?.userType; if (to.meta.public) { if (hasToken && hasUser) return '/'; return true; } if (!hasToken || !hasUser) { auth.clearStaffSession(); return { path: '/login', query: { redirect: to.fullPath } }; } if (to.meta.adminOnly && !auth.isAdmin.value) { return '/'; } if (auth.isAdmin.value && to.meta.permissions) { const required = to.meta.permissions as string[]; const role = auth.user.value?.role; const permissions = auth.user.value?.permissions; if (pathDeniedForRole(to.path, role)) { const fallback = firstAdminFallback(role, permissions); if (fallback && to.path !== fallback) return fallback; } if (!adminCanAccess(role, permissions, required)) { const fallback = firstAdminFallback(role, permissions); if (!fallback) { return true; } if (to.path !== fallback) return fallback; } } if (to.path.startsWith('/dashboard/') && !auth.isAdmin.value) { return '/'; } if (to.meta.agentOnly && !auth.isAgent.value) { return '/'; } if (to.meta.tier1AgentOnly && !auth.isTier1Agent.value) { return '/'; } if (to.meta.smokeTestsOnly) { const { ensureLoaded, allowed } = useSmokeTestsAllowed(); await ensureLoaded(); if (!allowed.value) return '/'; } return true; }); export default router;