feat: 管理端 RBAC 权限体系与员工管理
新增多角色权限控制(赛事/财务/客服管理员),支持员工 CRUD、路由菜单按权限显隐、审计日志范围过滤;登录返回角色与权限列表。玩家端赛事列表增加静默刷新避免图片闪烁。Seed 补充演示员工账号与充值相关权限。附带 RBAC/审计范围单元测试及 UAT 文档更新。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,6 +2,25 @@ import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { useSmokeTestsAllowed } from '../composables/useSmokeTestsAllowed';
|
||||
import { ensureStaffSession } from '../utils/session-hydrate';
|
||||
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<string, string[]> = {
|
||||
'/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(),
|
||||
@@ -15,6 +34,7 @@ const router = createRouter({
|
||||
{
|
||||
path: '',
|
||||
component: () => import('../views/HomeEntry.vue'),
|
||||
meta: { permissions: [AdminPerm.reports] },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
@@ -33,11 +53,12 @@ const router = createRouter({
|
||||
{
|
||||
path: 'users',
|
||||
component: () => import('../views/AgentManager.vue'),
|
||||
meta: { adminOnly: true },
|
||||
meta: { adminOnly: true, permissions: [AdminPerm.usersView, AdminPerm.agentsView] },
|
||||
},
|
||||
{
|
||||
path: 'finance-logs',
|
||||
component: () => import('../views/FinanceLogs.vue'),
|
||||
meta: { permissions: [AdminPerm.reports] },
|
||||
},
|
||||
{
|
||||
path: 'agent-credit-transactions',
|
||||
@@ -53,72 +74,72 @@ const router = createRouter({
|
||||
{
|
||||
path: 'matches',
|
||||
component: () => import('../views/Matches.vue'),
|
||||
meta: { adminOnly: true },
|
||||
meta: { adminOnly: true, permissions: [AdminPerm.matches] },
|
||||
},
|
||||
{
|
||||
path: 'matches/outrights',
|
||||
component: () => import('../views/MatchesOutrights.vue'),
|
||||
meta: { adminOnly: true },
|
||||
meta: { adminOnly: true, permissions: [AdminPerm.matches] },
|
||||
},
|
||||
{
|
||||
path: 'matches/market-templates',
|
||||
component: () => import('../views/MarketTemplates.vue'),
|
||||
meta: { adminOnly: true },
|
||||
meta: { adminOnly: true, permissions: [AdminPerm.matches] },
|
||||
},
|
||||
{
|
||||
path: 'matches/:matchId/edit',
|
||||
name: 'admin-match-edit',
|
||||
component: () => import('../views/matches/MatchEventEditor.vue'),
|
||||
meta: { adminOnly: true },
|
||||
meta: { adminOnly: true, permissions: [AdminPerm.matches] },
|
||||
},
|
||||
{
|
||||
path: 'matches/:matchId/markets',
|
||||
name: 'admin-match-markets',
|
||||
component: () => import('../views/matches/MatchMarketsPage.vue'),
|
||||
meta: { adminOnly: true },
|
||||
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 },
|
||||
meta: { adminOnly: true, permissions: [AdminPerm.matches] },
|
||||
},
|
||||
{ path: 'world-cup-outright', redirect: '/matches/outrights' },
|
||||
{
|
||||
path: 'bets',
|
||||
component: () => import('../views/Bets.vue'),
|
||||
meta: { adminOnly: true },
|
||||
meta: { adminOnly: true, permissions: [AdminPerm.bets] },
|
||||
},
|
||||
{
|
||||
path: 'settlement/:id',
|
||||
component: () => import('../views/Settlement.vue'),
|
||||
meta: { adminOnly: true },
|
||||
meta: { adminOnly: true, permissions: [AdminPerm.settlement, AdminPerm.matches] },
|
||||
},
|
||||
{
|
||||
path: 'cashback',
|
||||
component: () => import('../views/Cashback.vue'),
|
||||
meta: { adminOnly: true },
|
||||
meta: { adminOnly: true, permissions: [AdminPerm.cashback] },
|
||||
},
|
||||
{
|
||||
path: 'contents',
|
||||
component: () => import('../views/Contents.vue'),
|
||||
meta: { adminOnly: true },
|
||||
meta: { adminOnly: true, permissions: [AdminPerm.content] },
|
||||
},
|
||||
{
|
||||
path: 'audit',
|
||||
component: () => import('../views/Audit.vue'),
|
||||
meta: { adminOnly: true },
|
||||
meta: { adminOnly: true, permissions: [AdminPerm.audit] },
|
||||
},
|
||||
{
|
||||
path: 'smoke-tests',
|
||||
component: () => import('../views/SmokeTests.vue'),
|
||||
meta: { adminOnly: true, smokeTestsOnly: true },
|
||||
meta: { adminOnly: true, smokeTestsOnly: true, permissions: [AdminPerm.settings] },
|
||||
},
|
||||
{
|
||||
path: 'media',
|
||||
component: () => import('../views/MediaLibrary.vue'),
|
||||
meta: { adminOnly: true },
|
||||
meta: { adminOnly: true, permissions: [AdminPerm.content, AdminPerm.matches] },
|
||||
},
|
||||
{
|
||||
path: 'payment-methods',
|
||||
@@ -134,7 +155,12 @@ const router = createRouter({
|
||||
{
|
||||
path: 'deposit',
|
||||
component: () => import('../views/DepositManage.vue'),
|
||||
meta: { adminOnly: true },
|
||||
meta: { adminOnly: true, permissions: [AdminPerm.depositManage, AdminPerm.depositReview] },
|
||||
},
|
||||
{
|
||||
path: 'staff',
|
||||
component: () => import('../views/StaffManage.vue'),
|
||||
meta: { adminOnly: true, permissions: [AdminPerm.settings] },
|
||||
},
|
||||
{
|
||||
path: 'my-players',
|
||||
@@ -179,6 +205,23 @@ router.beforeEach(async (to) => {
|
||||
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 '/';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user