feat(identity): 完成 A2 角色权限与数据范围全栈落地
背景:A2 开卡前完成 H1 权限门,确认权限点目录、特殊权限、SoD 三对、六类数据范围与超管隐式全集口径(ADR-001)。 改动: - shared:30 权限点 / 10 模块目录与元数据、特殊权限 6 编码、SoD 三对、角色与权限树契约;/me 改 dataScopes 多角色并集。 - prisma:expand 迁移新增 permissions / data_scopes;种子按原型口径重写角色权限、数据范围与 8 项特殊授予。 - api:角色 CRUD / 复制 / 停用 / 删除、权限点启动同步、SoD 40304、系统角色保护、六类范围 ScopeContext 与 CustomerDirectory 端口、变更审计与 outbox。 - web:方案 B 主从分栏三角色页面(列表 / 编辑器 / 新建),权限树、范围选择、特殊权限二次确认、保存影响、SoD 冲突、只读态与模板预填。 - 测试:角色集成 13 条 + A2 e2e 1 条;A1 全量回归。 验证:format:check / lint / typecheck / test(306)/ test:int(27)/ A2 e2e(1,复跑通过)/ openapi:export 全绿。
This commit is contained in:
1 parent
f97f97b0cd
commit
61894df401
80 files changed
+14380
-237
No files matched your search
@@ -4,13 +4,14 @@ import {
|
||||
ErrorCode,
|
||||
type ListResponse,
|
||||
Permission,
|
||||
SUPER_ADMIN_SOFT_LIMIT,
|
||||
ulid,
|
||||
type UpdateUserInput,
|
||||
type UserDetail,
|
||||
type UserListQuery,
|
||||
type UserSummary,
|
||||
} from '@lingxi/shared';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import { AppException } from '../../common/errors/app-exception';
|
||||
@@ -31,6 +32,8 @@ function hasPermission(permissions: string[], code: string): boolean {
|
||||
/** 用户管理:列表(游标 + 筛选 + 排序)、详情、新建、更新、停用 / 启用 */
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
private readonly logger = new Logger('UsersService');
|
||||
|
||||
constructor(
|
||||
@Inject(PRISMA) private readonly prisma: PrismaService,
|
||||
@Inject(PasswordService) private readonly passwords: PasswordService,
|
||||
@@ -90,6 +93,7 @@ export class UsersService {
|
||||
} catch (error) {
|
||||
throw this.mapUniqueError(error);
|
||||
}
|
||||
await this.warnIfSuperAdminExceeded(input.roleIds);
|
||||
return this.detail(id, permissions);
|
||||
}
|
||||
|
||||
@@ -125,6 +129,9 @@ export class UsersService {
|
||||
} catch (error) {
|
||||
throw this.mapUniqueError(error);
|
||||
}
|
||||
if (input.roleIds !== undefined) {
|
||||
await this.warnIfSuperAdminExceeded(input.roleIds);
|
||||
}
|
||||
return this.detail(id, permissions);
|
||||
}
|
||||
|
||||
@@ -181,6 +188,28 @@ export class UsersService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 超管人数软上限(PRD §5.4.1 建议 ≤ 2 人):只记录告警,不阻断保存;
|
||||
* 前端「超管授予二次确认」是主要提示手段。A4 阈值迁 system_configs。
|
||||
*/
|
||||
private async warnIfSuperAdminExceeded(roleIds: readonly string[]): Promise<void> {
|
||||
const superAdminRole = await this.prisma.role.findUnique({
|
||||
where: { code: 'super_admin' },
|
||||
select: { id: true },
|
||||
});
|
||||
if (superAdminRole === null || !roleIds.includes(superAdminRole.id)) {
|
||||
return;
|
||||
}
|
||||
const count = await this.prisma.user.count({
|
||||
where: { status: 'ACTIVE', roles: { some: { roleId: superAdminRole.id } } },
|
||||
});
|
||||
if (count > SUPER_ADMIN_SOFT_LIMIT) {
|
||||
this.logger.warn(
|
||||
`超级管理员 ${String(count)} 人已超过建议上限 ${String(SUPER_ADMIN_SOFT_LIMIT)} 人(仅提示,不阻断)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async findOrThrow(id: string): Promise<UserWithRelations> {
|
||||
const user = await this.prisma.user.findUnique({ where: { id }, include: USER_INCLUDE });
|
||||
if (user === null) {
|
||||
|
||||
Reference in new issue
Block a user