ec-nest/src/common/base-crud.service.ts
liuyun 513688a5e6 fix(security): P0 安全越权堵漏(C1/H1-H6/H10/H11/H13/M1/M4/M6/M8-M11/M22)
- 平台管理员模型:super_admin=平台超管、tenant_super_admin=租户顶层(保留码),tenant:* 仅平台可授予
- 软删改名回收唯一键 + P2002→409
- 删除角色级联清理关联 + 聚合过滤已删/停用角色资源
- tokenVersion 会话吊销(重置密码/停用/删除即时踢下线)
- data_scope self 按 userId 过滤 + 详情数据权限校验
- 组织树防环、seed 不重置密码、日志脱敏补 phone/email
- JWT 密钥强校验、pageSize 上限、DTO 枚举校验、Prisma 错误映射、授权事务化

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 16:10:00 +08:00

80 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { PrismaService } from '../prisma/prisma.service.js';
import { getRequestContext } from './context/request-context.js';
/**
* 软删时需「改名回收」唯一键的模型映射:modelName → 唯一字段及其列长。
* 目的:软删只置 deletedAt 不释放 @@unique,重建同名会 P2002;删除时把唯一字段改名
* 成「原值(截断) + #id」释放唯一键,展示名字段(name/label)保持不变。
*/
const RECYCLE_FIELDS: Record<string, { field: string; maxLen: number }> = {
user: { field: 'username', maxLen: 64 },
role: { field: 'code', maxLen: 64 },
resource: { field: 'code', maxLen: 128 },
org: { field: 'code', maxLen: 64 },
dictType: { field: 'code', maxLen: 64 },
dictItem: { field: 'value', maxLen: 64 },
config: { field: 'key', maxLen: 128 },
};
/**
* 通用 CRUD 基类:封装分页/创建/更新/软删。
* 创建时自动注入 tenantCode(Tenant 模型除外);软删由子类按需调用 softRemove。
*/
export class BaseCrudService {
constructor(
protected readonly prisma: PrismaService,
private readonly modelName: string,
) {}
protected db(): any {
return (this.prisma.client as any)[this.modelName];
}
async paginate(
page = 1,
pageSize = 20,
where: Record<string, unknown> = {},
orderBy: Record<string, unknown> = { createdAt: 'desc' },
) {
const [list, total] = await Promise.all([
this.db().findMany({ where, skip: (page - 1) * pageSize, take: pageSize, orderBy }),
this.db().count({ where }),
]);
return { list, total, page, pageSize };
}
async list(where: Record<string, unknown> = {}, orderBy?: Record<string, unknown>) {
return this.db().findMany({ where, orderBy });
}
async findById(id: string) {
return this.db().findUnique({ where: { id } });
}
async create(data: Record<string, unknown>) {
const ctx = getRequestContext();
if (ctx && this.modelName !== 'Tenant') data.tenantCode = ctx.tenantCode;
return this.db().create({ data });
}
async update(id: string, data: Record<string, unknown>) {
return this.db().update({ where: { id }, data });
}
async softRemove(id: string) {
const recycle = RECYCLE_FIELDS[this.modelName];
if (!recycle) {
return this.db().update({ where: { id }, data: { deletedAt: new Date() } });
}
// 先读原值,改名回收唯一键,再软删
const record = await this.findById(id);
const original = record?.[recycle.field];
if (original == null) {
return this.db().update({ where: { id }, data: { deletedAt: new Date() } });
}
const suffix = `#${id}`;
const recycled = `${String(original).slice(0, recycle.maxLen - suffix.length)}${suffix}`;
return this.db().update({ where: { id }, data: { [recycle.field]: recycled, deletedAt: new Date() } });
}
}