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:
@@ -1,4 +1,6 @@
|
||||
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';
|
||||
|
||||
@@ -38,8 +40,26 @@ export async function resolveRoutineScope(
|
||||
manageable = [...new Set([actor.userId, ...members.map((m) => m.userId)])];
|
||||
}
|
||||
|
||||
const visible = scopedOwnerIds(actor, 'routines');
|
||||
const viewable = visible === null ? null : [...new Set([...visible, ...manageable])];
|
||||
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,
|
||||
|
||||
@@ -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 } } },
|
||||
|
||||
Reference in New Issue
Block a user