feat: 管理端 RBAC 权限体系与员工管理

新增多角色权限控制(赛事/财务/客服管理员),支持员工 CRUD、路由菜单按权限显隐、审计日志范围过滤;登录返回角色与权限列表。玩家端赛事列表增加静默刷新避免图片闪烁。Seed 补充演示员工账号与充值相关权限。附带 RBAC/审计范围单元测试及 UAT 文档更新。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-15 17:52:39 +08:00
parent be5b4a4921
commit 567ec9ec8a
44 changed files with 1717 additions and 125 deletions

View File

@@ -0,0 +1,80 @@
import { AdminPerm } from '../constants/permissions';
/** Mirrors API seed role assignments — used when /me has not yet returned permissions. */
export const ROLE_PERMISSIONS: Record<string, string[]> = {
MATCH_ADMIN: [
AdminPerm.matches,
AdminPerm.settlement,
AdminPerm.content,
AdminPerm.bets,
AdminPerm.reports,
AdminPerm.audit,
],
FINANCE_ADMIN: [
AdminPerm.walletDeposit,
AdminPerm.walletWithdraw,
AdminPerm.cashback,
AdminPerm.agentsView,
AdminPerm.agentsCredit,
AdminPerm.usersView,
AdminPerm.usersCreate,
AdminPerm.depositManage,
AdminPerm.depositReview,
AdminPerm.reports,
AdminPerm.bets,
AdminPerm.audit,
],
SUPPORT: [
AdminPerm.usersView,
AdminPerm.usersResetPassword,
AdminPerm.bets,
AdminPerm.reports,
AdminPerm.audit,
],
};
export function effectivePermissions(
role: string | undefined,
permissions: string[] | undefined,
): string[] {
if (permissions?.length) return permissions;
if (role && ROLE_PERMISSIONS[role]) return ROLE_PERMISSIONS[role];
return [];
}
export function adminCanAccess(
role: string | undefined,
permissions: string[] | undefined,
required: string[] | undefined,
): boolean {
if (!required?.length) return true;
if (role === 'SUPER_ADMIN') return true;
const perms = effectivePermissions(role, permissions);
return required.some((p) => perms.includes(p));
}
export const ADMIN_ROUTE_FALLBACKS: { path: string; permissions: string[]; roles?: string[] }[] = [
{ path: '/', permissions: [AdminPerm.reports], roles: ['MATCH_ADMIN'] },
{ path: '/dashboard/players', permissions: [AdminPerm.reports], roles: ['FINANCE_ADMIN'] },
{ path: '/users', permissions: [AdminPerm.usersView, AdminPerm.agentsView], roles: ['SUPPORT'] },
{ path: '/finance-logs', permissions: [AdminPerm.reports], roles: ['SUPPORT'] },
{ path: '/', permissions: [AdminPerm.reports] },
{ path: '/matches', permissions: [AdminPerm.matches] },
{ path: '/bets', permissions: [AdminPerm.bets] },
{ path: '/audit', permissions: [AdminPerm.audit] },
];
export function firstAdminFallback(role?: string, permissions?: string[]) {
if (role) {
for (const item of ADMIN_ROUTE_FALLBACKS) {
if (item.roles?.includes(role) && adminCanAccess(role, permissions, item.permissions)) {
return item.path;
}
}
}
for (const item of ADMIN_ROUTE_FALLBACKS) {
if (item.roles) continue;
if (adminCanAccess(role, permissions, item.permissions)) return item.path;
}
return null;
}

View File

@@ -58,6 +58,7 @@ export async function hydrateStaffSession(): Promise<boolean> {
userType: raw.userType,
locale: raw.locale,
role: raw.role,
permissions: Array.isArray(raw.permissions) ? raw.permissions : auth.user.value?.permissions,
agentLevel: typeof raw.agentLevel === 'number' ? raw.agentLevel : null,
maxAgentLevel: typeof raw.maxAgentLevel === 'number' ? raw.maxAgentLevel : null,
canManageSubAgents: raw.canManageSubAgents === true,