76f266645d
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
106 lines
3.5 KiB
TypeScript
106 lines
3.5 KiB
TypeScript
import { ForbiddenException } from '@nestjs/common';
|
|
import type { Prisma } from '@prisma/client';
|
|
import type { AuthUser } from './decorators/current-user.decorator';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
|
|
export type DataScope = 'ALL' | 'DEPT' | 'SELF';
|
|
|
|
const RANK: Record<DataScope, number> = { SELF: 0, DEPT: 1, ALL: 2 };
|
|
|
|
export function mergeDataScope(scopes: string[]): DataScope {
|
|
let best: DataScope = 'SELF';
|
|
for (const s of scopes) {
|
|
const v = s as DataScope;
|
|
if (RANK[v] > RANK[best]) best = v;
|
|
}
|
|
return best;
|
|
}
|
|
|
|
export function effectiveScope(user: AuthUser): DataScope {
|
|
if (user.roles.includes('admin') || user.roles.includes('owner')) return 'ALL';
|
|
return user.dataScope || 'SELF';
|
|
}
|
|
|
|
export async function descendantDeptIds(prisma: PrismaService, rootId?: string | null) {
|
|
if (!rootId) return [] as string[];
|
|
const all = await prisma.department.findMany({
|
|
where: { tenantId: '1' },
|
|
select: { id: true, parentId: true },
|
|
});
|
|
const ids = new Set<string>([rootId]);
|
|
let grew = true;
|
|
while (grew) {
|
|
grew = false;
|
|
for (const d of all) {
|
|
if (d.parentId && ids.has(d.parentId) && !ids.has(d.id)) {
|
|
ids.add(d.id);
|
|
grew = true;
|
|
}
|
|
}
|
|
}
|
|
return [...ids];
|
|
}
|
|
|
|
function empInScope(user: AuthUser, deptIds: string[]): Prisma.EmployeeWhereInput {
|
|
return {
|
|
OR: [
|
|
{ departmentId: { in: deptIds } },
|
|
...(user.employeeId
|
|
? [{ managerId: user.employeeId }, { managerIds: { contains: user.employeeId } }]
|
|
: []),
|
|
],
|
|
};
|
|
}
|
|
|
|
/** 本人 / 本部门及下级 / 全公司 */
|
|
export async function ownerScope(
|
|
prisma: PrismaService,
|
|
user: AuthUser,
|
|
owner: 'createdById' | 'applicantId',
|
|
): Promise<Prisma.BidCaseWhereInput | Prisma.ExpenseClaimWhereInput | Record<string, never>> {
|
|
const scope = effectiveScope(user);
|
|
if (scope === 'ALL') return {};
|
|
if (scope === 'SELF' || !user.departmentId) return { [owner]: user.id };
|
|
const deptIds = await descendantDeptIds(prisma, user.departmentId);
|
|
const rel = owner === 'createdById' ? 'createdBy' : 'applicant';
|
|
return {
|
|
OR: [{ [owner]: user.id }, { [rel]: { employee: empInScope(user, deptIds) } }],
|
|
};
|
|
}
|
|
|
|
export function withAnd<T extends { AND?: unknown }>(where: T, extra: Record<string, unknown>): T {
|
|
if (!extra || !Object.keys(extra).length) return where;
|
|
const prev = where.AND;
|
|
const list = Array.isArray(prev) ? [...prev] : prev ? [prev] : [];
|
|
list.push(extra);
|
|
where.AND = list;
|
|
return where;
|
|
}
|
|
|
|
export async function canSeeOwner(prisma: PrismaService, user: AuthUser, ownerId?: string | null) {
|
|
if (!ownerId) return true;
|
|
const scope = effectiveScope(user);
|
|
if (scope === 'ALL') return true;
|
|
if (ownerId === user.id) return true;
|
|
if (scope !== 'DEPT' || !user.departmentId) return false;
|
|
const owner = await prisma.user.findUnique({
|
|
where: { id: ownerId },
|
|
include: { employee: true },
|
|
});
|
|
if (!owner?.employee) return false;
|
|
const deptIds = await descendantDeptIds(prisma, user.departmentId);
|
|
if (owner.employee.departmentId && deptIds.includes(owner.employee.departmentId)) return true;
|
|
const extra = String(owner.employee.managerIds || '')
|
|
.split(/[,,、\s]+/)
|
|
.filter(Boolean);
|
|
return Boolean(
|
|
user.employeeId &&
|
|
(owner.employee.managerId === user.employeeId || extra.includes(user.employeeId)),
|
|
);
|
|
}
|
|
|
|
export async function assertCanSeeOwner(prisma: PrismaService, user: AuthUser, ownerId?: string | null) {
|
|
if (await canSeeOwner(prisma, user, ownerId)) return;
|
|
throw new ForbiddenException('超出数据范围,无权查看');
|
|
}
|