Files
CRM_Montzar/apps/api/src/routines/routine-scope.ts
T
Mahdi 09a60b3b5a feat(acl): deep data access by section for roles and users
Role default + user override with OR rules; covers customers, leads, sales, activities, notes, routines, projects, and tickets.
2026-10-22 11:53:26 +03:30

100 lines
3.6 KiB
TypeScript

import { ForbiddenException } from '@nestjs/common';
import { sectionHasDataAccessLimits } from '@mento/shared';
import { sectionOwnerIdUnion } from '../common/data-access';
import { type Actor, elevated, scopedOwnerIds } from '../common/record-scope';
import type { PrismaService } from '../prisma/prisma.service';
export type RoutineScope = {
userId: string;
organizationId: string;
/** مالک/مدیر سازمان یا مدیر حداقل یک تیم */
isManager: boolean;
/** کاربرانی که می‌تواند برایشان روتین/Task تعریف کند؛ null = همه */
manageableUserIds: string[] | null;
/** کاربرانی که Taskهایشان را می‌بیند؛ null = همه */
viewableUserIds: string[] | null;
};
export async function resolveRoutineScope(
prisma: PrismaService,
actor: Actor,
): Promise<RoutineScope> {
const base = {
userId: actor.userId,
organizationId: actor.organizationId,
};
if (elevated(actor.role)) {
return { ...base, isManager: true, manageableUserIds: null, viewableUserIds: null };
}
const managed = await prisma.teamMember.findMany({
where: { userId: actor.userId, role: 'manager', team: { organizationId: actor.organizationId } },
select: { teamId: true },
});
let manageable: string[] = [actor.userId];
if (managed.length) {
const members = await prisma.teamMember.findMany({
where: { teamId: { in: managed.map((m) => m.teamId) } },
select: { userId: true },
});
manageable = [...new Set([actor.userId, ...members.map((m) => m.userId)])];
}
let viewable: string[] | null;
if (actor.routinesAccess !== undefined) {
if (!sectionHasDataAccessLimits(actor.routinesAccess)) {
viewable = null;
} else {
const owners = sectionOwnerIdUnion(actor.routinesAccess, {
userId: actor.userId,
teamPeerIds: actor.teamPeerIds ?? null,
});
// فقط-مالک: محدود به owners؛ اگر قانون بدون owners (مثلاً دسته) باشد null = همه + فیلتر deepWhere
viewable = owners === null ? null : owners.length ? owners : [actor.userId];
}
} else {
const visible = scopedOwnerIds(actor, 'routines');
viewable = visible === null ? null : [...new Set([...visible, ...manageable])];
}
if (viewable !== null) {
viewable = [...new Set([...viewable, ...manageable])];
}
return {
...base,
isManager: managed.length > 0,
manageableUserIds: manageable,
viewableUserIds: viewable,
};
}
export function canView(scope: RoutineScope, userId: string): boolean {
return scope.viewableUserIds === null || scope.viewableUserIds.includes(userId);
}
export function canManage(scope: RoutineScope, userId: string): boolean {
return scope.manageableUserIds === null || scope.manageableUserIds.includes(userId);
}
export function assertManager(scope: RoutineScope): void {
if (!scope.isManager) {
throw new ForbiddenException('فقط مدیران به این بخش دسترسی دارند');
}
}
/** فیلتر Prisma روی assigneeId بر اساس دید کاربر و فیلتر کاربر درخواستی */
export function assigneeFilter(
scope: RoutineScope,
requestedUserId?: string | null,
): { assigneeId?: string | { in: string[] } } {
if (requestedUserId) {
if (!canView(scope, requestedUserId)) {
throw new ForbiddenException('دسترسی به کارهای این کاربر ندارید');
}
return { assigneeId: requestedUserId };
}
if (scope.viewableUserIds === null) return {};
return { assigneeId: { in: scope.viewableUserIds } };
}