Files
smart_grid_iot/server/src/middleware/rbac.middleware.ts
T
andyesfly 6aea23c070 merge: 合并 xtzd/main 的报警监控、ICC 网关/模拟量/报警订阅与菜单修复提交
冲突解决:auth.middleware.ts 与 rbac.middleware.ts 的 ignore 白名单取并集,同时保留 /api/customer-app/*(小程序客户鉴权)与 /api/icc-event/callback(ICC 事件回调 URL token 校验)。
2026-10-24 21:21:16 +08:00

132 lines
4.5 KiB
TypeScript

import { Middleware, IMiddleware } from "@midwayjs/core";
import { Context, NextFunction } from "@midwayjs/koa";
import { Provide, Inject } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { PermissionService } from "../service/permission.service";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
// 请求路径前缀 → 菜单权限标识映射
const PERMISSION_MAP: Record<string, string> = {
"/api/users": "system:user",
"/api/roles": "system:role",
"/api/menus": "system:menu",
"/api/dicts": "system:dict",
"/api/logs": "system:log",
"/api/jobs": "system:job",
"/api/notice-rules": "notice:rule",
"/api/notice-batches": "notice:batch",
"/api/customers": "customer:list",
"/api/customer-departments": "customer:department",
"/api/customer-accounts": "customer:account",
"/api/customer-visits": "customer:visit",
"/api/upload": "customer:visit",
"/api/customer-buildings": "customer:building",
"/api/customer-rooms": "customer:room",
// 用电回路接口复用页面级权限码 customer:topology(该菜单已在 sys_menu 登记并分配给角色),
// 因此不再新增按钮级权限码,避免非 ADMIN 角色因权限码未登记而被 403。
"/api/customer-circuits": "customer:topology",
"/api/customer-equipments": "device:list",
"/api/smart-devices": "device:model",
"/api/electric-lines": "device:list",
"/api/electric-gateways": "device:list",
"/api/electricity-data": "device:list",
"/api/icc-event/subscribe": "device:list",
"/api/icc-event/subscribe-status": "device:list",
"/api/device-alarms": "alarm:list",
"/api/alarm-dispatches": "alarm-dispatch:list",
"/api/survey-orders": "project:survey",
"/api/project-plans": "project:plan",
"/api/contracts": "project:contract",
"/api/install-orders": "project:install",
"/api/acceptances": "project:acceptance",
"/api/visit-records": "project:visit",
"/api/repair-requests": "dispatch:repair",
"/api/repair-dispatchs": "dispatch:repair",
"/api/maintenance-orders": "dispatch:maintenance",
"/api/service-prices": "dispatch:price",
"/api/dispatch-prices": "dispatch:cost",
"/api/workers": "dispatch:worker"
};
// 超级管理员角色编码,拥有全部权限
const ADMIN_ROLE_CODE = "ADMIN";
@Provide()
export class RbacMiddleware implements IMiddleware<Context, NextFunction> {
// 中间件匹配范围:全局生效,跳过公开接口、ICC 事件回调(URL token 校验)、仪表盘、电工 APP 与小程序客户接口
// /api/workers/options 与 /api/workers/recommend 放行:业务员在「项目勘察派单」页
// 仍可调用电工下拉与推荐接口,不受 dispatch:worker 权限限制(本次需求有意设计)
ignore = ["/api/auth/*", "/api/health", "/api/dashboard/*", "/api/worker-app/*", "/api/customer-app/*", "/api/workers/options", "/api/workers/recommend", "/api/icc-event/callback"];
@Inject()
permissionService!: PermissionService;
@InjectRepository(SysUserRole)
userRoleModel!: typeof SysUserRole;
@InjectRepository(SysRole)
roleModel!: typeof SysRole;
resolve() {
return async (ctx: Context, next: NextFunction) => {
const user = ctx.state.user;
if (!user) {
ctx.status = 401;
ctx.body = {
code: 401,
message: "未登录"
};
return;
}
// 路径前缀匹配,未匹配到任何权限映射的请求直接放行
const permissionCode = this.matchPermissionCode(ctx.path);
if (!permissionCode) {
await next();
return;
}
// 查询用户角色编码,超级管理员直接放行
const roleCodes = await this.getUserRoleCodes(user.userId);
if (roleCodes.includes(ADMIN_ROLE_CODE)) {
await next();
return;
}
// 检查用户菜单中是否包含该权限标识
const hasPermission = await this.permissionService.checkUserPermission(user.userId, permissionCode);
if (!hasPermission) {
ctx.status = 403;
ctx.body = {
code: 403,
message: "没有访问权限"
};
return;
}
await next();
};
}
private matchPermissionCode(path: string): string {
for (const [prefix, code] of Object.entries(PERMISSION_MAP)) {
if (path === prefix || path.startsWith(prefix + "/")) {
return code;
}
}
return "";
}
private async getUserRoleCodes(userId: number): Promise<string[]> {
const userRoles = await this.userRoleModel.findAll({
where: { userId },
include: [this.roleModel]
});
return userRoles.map((ur) => ur.role?.roleCode).filter(Boolean) as string[];
}
}