feat: 管理端 RBAC 权限体系与员工管理
新增多角色权限控制(赛事/财务/客服管理员),支持员工 CRUD、路由菜单按权限显隐、审计日志范围过滤;登录返回角色与权限列表。玩家端赛事列表增加静默刷新避免图片闪烁。Seed 补充演示员工账号与充值相关权限。附带 RBAC/审计范围单元测试及 UAT 文档更新。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
80
apps/admin/src/utils/admin-access.ts
Normal file
80
apps/admin/src/utils/admin-access.ts
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user