feat: 管理端 RBAC 权限体系与员工管理
新增多角色权限控制(赛事/财务/客服管理员),支持员工 CRUD、路由菜单按权限显隐、审计日志范围过滤;登录返回角色与权限列表。玩家端赛事列表增加静默刷新避免图片闪烁。Seed 补充演示员工账号与充值相关权限。附带 RBAC/审计范围单元测试及 UAT 文档更新。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import { jsonResponse } from '../../shared/common/filters';
|
||||
import { appBadRequest, appForbidden } from '../../shared/common/app-error';
|
||||
import { getUploadRoot } from '../../shared/uploads/upload-paths';
|
||||
import { UsersService } from '../../domains/identity/users.service';
|
||||
import { AdminStaffService } from '../../domains/identity/admin-staff.service';
|
||||
import { AgentsService } from '../../domains/agent/agents.service';
|
||||
import { WalletService } from '../../domains/ledger/wallet.service';
|
||||
import { MatchesService } from '../../domains/catalog/matches.service';
|
||||
@@ -255,6 +256,40 @@ class UpdatePlayerAdminDto {
|
||||
cashbackRate?: number | null;
|
||||
}
|
||||
|
||||
class CreateStaffDto {
|
||||
@IsString()
|
||||
username!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
password!: string;
|
||||
|
||||
@IsString()
|
||||
roleCode!: string;
|
||||
}
|
||||
|
||||
class UpdateStaffDto {
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'SUSPENDED', 'DISABLED'])
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
roleCode?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
password?: string;
|
||||
}
|
||||
|
||||
class ResetPlayerPasswordDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
password?: string;
|
||||
}
|
||||
|
||||
class PlatformDirectCashbackSettingsDto {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@@ -1168,6 +1203,7 @@ export class AdminController {
|
||||
private databaseReset: DatabaseResetService,
|
||||
private smokeTests: SmokeTestService,
|
||||
private depositService: DepositService,
|
||||
private staff: AdminStaffService,
|
||||
) {}
|
||||
|
||||
@Get('dashboard')
|
||||
@@ -1178,7 +1214,7 @@ export class AdminController {
|
||||
}
|
||||
|
||||
@Get('users/page-init')
|
||||
@RequirePermissions(P.agentsView)
|
||||
@RequirePermissions(P.agentsView, P.usersView)
|
||||
async getUsersPageInit() {
|
||||
const [
|
||||
playerSettings,
|
||||
@@ -1380,6 +1416,84 @@ export class AdminController {
|
||||
return jsonResponse(detail);
|
||||
}
|
||||
|
||||
@Post('users/:id/reset-password')
|
||||
@RequirePermissions(P.usersResetPassword)
|
||||
async resetPlayerPassword(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: ResetPlayerPasswordDto,
|
||||
) {
|
||||
const { password } = await this.staff.resetPlayerPassword(BigInt(id), dto.password);
|
||||
await this.audit.log({
|
||||
operatorId,
|
||||
operatorType: 'ADMIN',
|
||||
action: 'RESET_PLAYER_PASSWORD',
|
||||
module: 'USERS',
|
||||
targetId: id,
|
||||
});
|
||||
const detail = await this.users.getPlayerAdminDetail(BigInt(id));
|
||||
return jsonResponse({ ...detail, password });
|
||||
}
|
||||
|
||||
@Get('staff/roles')
|
||||
@RequirePermissions(P.settings)
|
||||
async listStaffRoles() {
|
||||
const roles = await this.staff.listRoles();
|
||||
return jsonResponse(roles);
|
||||
}
|
||||
|
||||
@Get('staff')
|
||||
@RequirePermissions(P.settings)
|
||||
async listStaff(
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
) {
|
||||
const result = await this.staff.listStaff(
|
||||
page ? parseInt(page, 10) : 1,
|
||||
pageSize ? parseInt(pageSize, 10) : 20,
|
||||
keyword,
|
||||
);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Post('staff')
|
||||
@RequirePermissions(P.settings)
|
||||
async createStaff(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Body() dto: CreateStaffDto,
|
||||
) {
|
||||
const created = await this.staff.createStaff(dto);
|
||||
await this.audit.log({
|
||||
operatorId,
|
||||
operatorType: 'ADMIN',
|
||||
action: 'CREATE_STAFF',
|
||||
module: 'STAFF',
|
||||
targetId: created.id,
|
||||
afterData: { username: created.username, role: created.role },
|
||||
});
|
||||
return jsonResponse(created);
|
||||
}
|
||||
|
||||
@Patch('staff/:id')
|
||||
@RequirePermissions(P.settings)
|
||||
async updateStaff(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateStaffDto,
|
||||
) {
|
||||
const updated = await this.staff.updateStaff(BigInt(id), dto);
|
||||
await this.audit.log({
|
||||
operatorId,
|
||||
operatorType: 'ADMIN',
|
||||
action: 'UPDATE_STAFF',
|
||||
module: 'STAFF',
|
||||
targetId: id,
|
||||
afterData: JSON.stringify({ status: dto.status, roleCode: dto.roleCode }),
|
||||
});
|
||||
return jsonResponse(updated);
|
||||
}
|
||||
|
||||
@Delete('users/:id')
|
||||
@RequirePermissions(P.usersCreate)
|
||||
async deletePlayer(
|
||||
@@ -2803,6 +2917,9 @@ export class AdminController {
|
||||
@Get('audit-logs')
|
||||
@RequirePermissions(P.audit)
|
||||
async auditLogs(
|
||||
@CurrentUser('id') viewerId: bigint,
|
||||
@CurrentUser('role') viewerRole: string | undefined,
|
||||
@CurrentUser('userType') viewerUserType: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('module') module?: string,
|
||||
@@ -2811,6 +2928,7 @@ export class AdminController {
|
||||
page ? parseInt(page, 10) : 1,
|
||||
pageSize ? parseInt(pageSize, 10) : 10,
|
||||
module || undefined,
|
||||
{ viewerId, viewerRole, viewerUserType },
|
||||
);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user