import { type DataScope, ErrorCode } from '@lingxi/shared'; import { AppException } from '../../errors/app-exception'; /** * 请求作用域上下文(技术架构 §5.5):登录用户 + 多角色并集后的可见客户集合。 * * 解析顺序(技术架构 §7.1): * 1. JWT / 账号状态由 JwtAuthGuard 校验; * 2. 角色数据范围 → 客户集合(六类,CustomerDirectory 端口解析); * 3. 多角色取并集;任一角色为「全部客户」即全部; * 4. 单个资源请求显式指定客户时,必须落在集合内。 * * 失败处理:解析失败一律 403(40302),**不降级为全部客户**。 */ export interface ScopeContext { userId: string; /** 是否拥有 is_system 超管角色(隐式全部数据范围) */ isSuperAdmin: boolean; /** 解析后的数据范围(每个元素带 customerIds;超管为 [{ type: 'all' }]) */ dataScopes: DataScope[]; /** null = 全部客户可见;否则为可见客户 id 集合(已排序去重;A3 前为空数组) */ customerIds: readonly string[] | null; } /** * Repository 客户维度过滤参数(Prisma where 片段)。 * 使用方必须把返回值展开进 where:`where: { ...customerScopeWhere(ctx), ... }`, * 禁止「先查全量再内存过滤」(AGENTS §3.5)。 */ export interface CustomerScopeWhere { customerId?: { in: string[] }; } /** 是否可见全部客户 */ export function isAllCustomers(ctx: ScopeContext): boolean { return ctx.customerIds === null; } /** 构造客户维度强制过滤片段:全部客户返回空对象,受限范围返回 `{ customerId: { in } }` */ export function customerScopeWhere(ctx: ScopeContext): CustomerScopeWhere { return ctx.customerIds === null ? {} : { customerId: { in: [...ctx.customerIds] } }; } /** 判断单个客户是否在范围内(用于单条资源的 404 / 403 语义选择) */ export function isCustomerVisible(ctx: ScopeContext, customerId: string): boolean { return ctx.customerIds === null || ctx.customerIds.includes(customerId); } /** * 断言对指定客户有写权限;范围外抛 40302(不泄露资源是否存在)。 * 单条**读**接口按 §10.3 应返回 404(避免探测存在性),调用方可先用 isCustomerVisible 判定。 */ export function assertCustomerAccess(ctx: ScopeContext, customerId: string): void { if (!isCustomerVisible(ctx, customerId)) { throw new AppException({ code: ErrorCode.OUT_OF_SCOPE, message: '客户不在当前数据范围之内', }); } } /** 断言存在客户维度上下文(Repository 方法入口守卫,防止漏注入) */ export function requireScopeContext(ctx: ScopeContext | null | undefined): ScopeContext { if (ctx === null || ctx === undefined) { throw new AppException({ code: ErrorCode.OUT_OF_SCOPE, message: '缺少数据范围上下文,已拒绝访问', }); } return ctx; }