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.
This commit is contained in:
2026-10-22 11:53:26 +03:30
parent cb5944e767
commit 09a60b3b5a
25 changed files with 1997 additions and 122 deletions
+24 -2
View File
@@ -16,7 +16,7 @@ import type {
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { NotificationsService } from '../notifications/notifications.service';
import type { Actor } from '../common/record-scope';
import { type Actor, routineTaskAccessWhere } from '../common/record-scope';
import type {
CreateRoutineTaskDto,
RoutineEntityQueryDto,
@@ -63,6 +63,10 @@ export class RoutineTasksService {
return resolveRoutineScope(this.prisma, actor);
}
private deepWhere(actor: Actor): Prisma.RoutineTaskWhereInput {
return routineTaskAccessWhere(actor);
}
/* ───────── خواندن ───────── */
async today(actor: Actor, userId?: string, date?: string): Promise<RoutineTodayDto> {
@@ -71,6 +75,7 @@ export class RoutineTasksService {
if (!canView(scope, target)) throw new ForbiddenException('دسترسی ندارید');
const day = date && isDayKey(date) ? date : tehranDayKey();
const dayDate = dayKeyToDate(day);
const deep = this.deepWhere(actor);
const [todayRows, overdueRows, score] = await Promise.all([
this.prisma.routineTask.findMany({
@@ -79,6 +84,7 @@ export class RoutineTasksService {
assigneeId: target,
scheduledFor: dayDate,
status: { not: 'cancelled' },
...deep,
},
include: routineTaskInclude,
orderBy: [{ dueAt: 'asc' }, { createdAt: 'asc' }],
@@ -89,6 +95,7 @@ export class RoutineTasksService {
assigneeId: target,
status: 'overdue',
scheduledFor: { lt: dayDate },
...deep,
},
include: { ...routineTaskInclude, routine: { select: { ...routineTaskInclude.routine.select, carryOverOverdue: true } } },
orderBy: [{ dueAt: 'asc' }],
@@ -148,7 +155,12 @@ export class RoutineTasksService {
const filter =
userId === 'all' ? assigneeFilter(scope) : assigneeFilter(scope, userId || actor.userId);
const rows = await this.prisma.routineTask.findMany({
where: { organizationId: actor.organizationId, status: 'overdue', ...filter },
where: {
organizationId: actor.organizationId,
status: 'overdue',
...filter,
...this.deepWhere(actor),
},
include: routineTaskInclude,
orderBy: [{ dueAt: 'asc' }],
take: 300,
@@ -166,6 +178,7 @@ export class RoutineTasksService {
organizationId: actor.organizationId,
scheduledFor: { gte: dayKeyToDate(from), lte: dayKeyToDate(to) },
...filter,
...this.deepWhere(actor),
...(q.categoryId ? { categoryId: q.categoryId } : {}),
...(q.status ? { status: q.status } : {}),
...(q.source ? { source: q.source } : {}),
@@ -196,6 +209,7 @@ export class RoutineTasksService {
OR: or,
status: { not: 'cancelled' },
...assigneeFilter(scope),
...this.deepWhere(actor),
},
include: routineTaskInclude,
orderBy: [{ scheduledFor: 'desc' }, { dueAt: 'desc' }],
@@ -208,6 +222,14 @@ export class RoutineTasksService {
const scope = await this.scope(actor);
const row = await this.findTask(actor.organizationId, id);
if (!canView(scope, row.assigneeId)) throw new ForbiddenException('دسترسی ندارید');
const deep = this.deepWhere(actor);
if (Object.keys(deep).length) {
const allowed = await this.prisma.routineTask.findFirst({
where: { id, organizationId: actor.organizationId, ...deep },
select: { id: true },
});
if (!allowed) throw new ForbiddenException('دسترسی ندارید');
}
const events = await this.prisma.routineTaskEvent.findMany({
where: { taskId: id },
include: { actor: { select: { id: true, fullName: true } } },