feat(roles): let each section decide who may assign work to others
Section access now only creates a record for yourself. Assigning it to a colleague requires a separate permission on that section. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,6 +30,7 @@ import {
|
||||
dealAccessWhere,
|
||||
ownerWhere,
|
||||
} from '../common/record-scope';
|
||||
import { assertAssigneeSet } from '../common/assign-access';
|
||||
import { normalizeTags } from '../common/tags';
|
||||
import { GamificationService } from '../gamification/gamification.service';
|
||||
import { tehranDayKey } from '../routines/routine-dates';
|
||||
@@ -566,6 +567,12 @@ export class ActivitiesService {
|
||||
? [dto.assigneeId]
|
||||
: [userId],
|
||||
);
|
||||
await assertAssigneeSet(
|
||||
this.prisma,
|
||||
{ userId, role: linkActor.role, organizationId },
|
||||
'activities',
|
||||
assigneeIds,
|
||||
);
|
||||
const assigneeId = assigneeIds[0] ?? userId;
|
||||
const initialStageId = emptyToNull(dto.stageId ?? null);
|
||||
const initialStage = initialStageId
|
||||
@@ -587,6 +594,12 @@ export class ActivitiesService {
|
||||
resolved.fields,
|
||||
dto.fields,
|
||||
);
|
||||
await assertAssigneeSet(
|
||||
this.prisma,
|
||||
{ userId, role: linkActor.role, organizationId },
|
||||
'activities',
|
||||
participantIds,
|
||||
);
|
||||
|
||||
const row = await this.prisma.activity.create({
|
||||
data: {
|
||||
@@ -723,6 +736,13 @@ export class ActivitiesService {
|
||||
data.typeDefId = resolved.typeDefId;
|
||||
data.fields = fields as Prisma.InputJsonValue;
|
||||
data.participantIds = participantIds;
|
||||
await assertAssigneeSet(
|
||||
this.prisma,
|
||||
{ userId: actor.userId, role: actor.role, organizationId },
|
||||
'activities',
|
||||
participantIds,
|
||||
existing?.participantIds ?? [],
|
||||
);
|
||||
const before = new Set(existing?.participantIds ?? []);
|
||||
addedParticipants = participantIds.filter((uid) => !before.has(uid) && uid !== actor.userId);
|
||||
}
|
||||
@@ -748,22 +768,47 @@ export class ActivitiesService {
|
||||
if ('companyId' in dto) data.companyId = emptyToNull(dto.companyId ?? null);
|
||||
if ('dealId' in dto) data.dealId = emptyToNull(dto.dealId ?? null);
|
||||
let assigneeConnect: Array<{ id: string }> | undefined;
|
||||
if (dto.assigneeIds !== undefined) {
|
||||
const assigneeIds = await this.normalizeAssigneeIds(
|
||||
organizationId,
|
||||
dto.assigneeIds,
|
||||
);
|
||||
if (!assigneeIds.length) {
|
||||
throw new BadRequestException('حداقل یک مسئول لازم است');
|
||||
if (dto.assigneeIds !== undefined || 'assigneeId' in dto) {
|
||||
const current = await this.prisma.activity.findFirst({
|
||||
where: { id, organizationId },
|
||||
select: { assigneeId: true, assignees: { select: { id: true } } },
|
||||
});
|
||||
const previous = current?.assignees.length
|
||||
? current.assignees.map((item) => item.id)
|
||||
: current?.assigneeId
|
||||
? [current.assigneeId]
|
||||
: [];
|
||||
if (dto.assigneeIds !== undefined) {
|
||||
const assigneeIds = await this.normalizeAssigneeIds(
|
||||
organizationId,
|
||||
dto.assigneeIds,
|
||||
);
|
||||
if (!assigneeIds.length) {
|
||||
throw new BadRequestException('حداقل یک مسئول لازم است');
|
||||
}
|
||||
await assertAssigneeSet(
|
||||
this.prisma,
|
||||
{ userId: actor.userId, role: actor.role, organizationId },
|
||||
'activities',
|
||||
assigneeIds,
|
||||
previous,
|
||||
);
|
||||
data.assigneeId = assigneeIds[0]!;
|
||||
assigneeConnect = assigneeIds.map((uid) => ({ id: uid }));
|
||||
} else {
|
||||
const assigneeId = emptyToNull(dto.assigneeId ?? null);
|
||||
if (assigneeId) await this.assertUserInOrg(organizationId, assigneeId);
|
||||
await assertAssigneeSet(
|
||||
this.prisma,
|
||||
{ userId: actor.userId, role: actor.role, organizationId },
|
||||
'activities',
|
||||
[assigneeId],
|
||||
previous,
|
||||
);
|
||||
data.assigneeId = assigneeId;
|
||||
if (assigneeId) assigneeConnect = [{ id: assigneeId }];
|
||||
else assigneeConnect = [];
|
||||
}
|
||||
data.assigneeId = assigneeIds[0]!;
|
||||
assigneeConnect = assigneeIds.map((uid) => ({ id: uid }));
|
||||
} else if ('assigneeId' in dto) {
|
||||
const assigneeId = emptyToNull(dto.assigneeId ?? null);
|
||||
if (assigneeId) await this.assertUserInOrg(organizationId, assigneeId);
|
||||
data.assigneeId = assigneeId;
|
||||
if (assigneeId) assigneeConnect = [{ id: assigneeId }];
|
||||
else assigneeConnect = [];
|
||||
}
|
||||
if (dto.status !== undefined) {
|
||||
data.status = dto.status;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { ForbiddenException } from '@nestjs/common';
|
||||
import {
|
||||
ASSIGN_SECTION_PERMISSION,
|
||||
type AssignSection,
|
||||
} from '@mento/shared';
|
||||
import type { PrismaService } from '../prisma/prisma.service';
|
||||
import { elevated } from './record-scope';
|
||||
|
||||
const ASSIGN_NOUN: Record<AssignSection, string> = {
|
||||
customers: 'مشتری',
|
||||
leads: 'لید',
|
||||
sales: 'معامله',
|
||||
activities: 'فعالیت',
|
||||
routines: 'کار روتین',
|
||||
projects: 'کارت پروژه',
|
||||
tickets: 'تیکت',
|
||||
};
|
||||
|
||||
type AssignActor = { userId: string; role: string; organizationId: string };
|
||||
|
||||
export function assignOthersDeniedMessage(section: AssignSection): string {
|
||||
return `تخصیص ${ASSIGN_NOUN[section]} به دیگران برای نقش شما مجاز نیست. فقط میتوانید برای خودتان ثبت کنید.`;
|
||||
}
|
||||
|
||||
export async function actorCanAssignToOthers(
|
||||
prisma: PrismaService,
|
||||
actor: AssignActor,
|
||||
section: AssignSection,
|
||||
): Promise<boolean> {
|
||||
if (elevated(actor.role)) return true;
|
||||
const membership = await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId: actor.userId,
|
||||
organizationId: actor.organizationId,
|
||||
isActive: true,
|
||||
},
|
||||
select: { orgRole: { select: { permissions: true } } },
|
||||
});
|
||||
const permissions = membership?.orgRole?.permissions;
|
||||
const key = ASSIGN_SECTION_PERMISSION[section];
|
||||
return Array.isArray(permissions) && permissions.includes(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* بدون مجوز همان بخش، مجموعهٔ مسئولانِ غیر از خود کاربر نباید عوض شود.
|
||||
* ثبت تازه یعنی قبلی خالی است، پس فقط خود کاربر مجاز است.
|
||||
*/
|
||||
export async function assertAssigneeSet(
|
||||
prisma: PrismaService,
|
||||
actor: AssignActor,
|
||||
section: AssignSection,
|
||||
nextIds: Array<string | null | undefined>,
|
||||
previousIds: Array<string | null | undefined> = [],
|
||||
): Promise<void> {
|
||||
if (await actorCanAssignToOthers(prisma, actor, section)) return;
|
||||
const prev = new Set(
|
||||
previousIds.filter((id): id is string => Boolean(id) && id !== actor.userId),
|
||||
);
|
||||
const next = nextIds.filter((id): id is string => Boolean(id) && id !== actor.userId);
|
||||
const changed = next.length !== prev.size || next.some((id) => !prev.has(id));
|
||||
if (changed) throw new ForbiddenException(assignOthersDeniedMessage(section));
|
||||
}
|
||||
@@ -225,8 +225,10 @@ export function noteAccessWhere(
|
||||
export function resolveOwnerId(
|
||||
actor: Pick<Actor, 'userId' | 'role'>,
|
||||
requested?: string | null,
|
||||
allowOthers?: boolean,
|
||||
): string {
|
||||
if (elevated(actor.role) && requested?.trim()) {
|
||||
const allowed = allowOthers ?? elevated(actor.role);
|
||||
if (allowed && requested?.trim()) {
|
||||
return requested.trim();
|
||||
}
|
||||
return actor.userId;
|
||||
|
||||
@@ -20,10 +20,10 @@ import {
|
||||
import { customersCompanyWhere } from '../common/data-access';
|
||||
import {
|
||||
type Actor,
|
||||
canReassignOwner,
|
||||
elevated,
|
||||
resolveOwnerId,
|
||||
} from '../common/record-scope';
|
||||
import { actorCanAssignToOthers, assertAssigneeSet } from '../common/assign-access';
|
||||
import { normalizeTags } from '../common/tags';
|
||||
import { normalizePersonImportSource } from './person-source';
|
||||
import { buildSearchWhere, ci, splitCsv } from '../common/search';
|
||||
@@ -201,7 +201,11 @@ export class CompaniesService {
|
||||
notes: emptyToNull(dto.notes),
|
||||
registeredAt: parseDateOrNull(dto.registeredAt),
|
||||
tags: normalizeTags(dto.tags),
|
||||
ownerId: resolveOwnerId(actor, dto.ownerId),
|
||||
ownerId: resolveOwnerId(
|
||||
actor,
|
||||
dto.ownerId,
|
||||
await actorCanAssignToOthers(this.prisma, actor, 'customers'),
|
||||
),
|
||||
customFields: toJsonCustomFields(dto.customFields),
|
||||
};
|
||||
await this.assertNoDuplicate(actor.organizationId, data);
|
||||
@@ -266,8 +270,14 @@ export class CompaniesService {
|
||||
if ('customFields' in dto) {
|
||||
data.customFields = toJsonCustomFields(dto.customFields);
|
||||
}
|
||||
if (dto.ownerId !== undefined && canReassignOwner(actor.role)) {
|
||||
data.ownerId = resolveOwnerId(actor, dto.ownerId);
|
||||
if (dto.ownerId !== undefined) {
|
||||
const current = await this.prisma.company.findFirst({
|
||||
where: { id, organizationId: actor.organizationId },
|
||||
select: { ownerId: true },
|
||||
});
|
||||
const nextOwner = emptyToNull(dto.ownerId ?? null);
|
||||
await assertAssigneeSet(this.prisma, actor, 'customers', [nextOwner], [current?.ownerId]);
|
||||
data.ownerId = nextOwner ?? actor.userId;
|
||||
}
|
||||
|
||||
await this.assertNoDuplicate(actor.organizationId, data, id);
|
||||
|
||||
@@ -38,9 +38,9 @@ import {
|
||||
import { customersPersonWhere } from '../common/data-access';
|
||||
import {
|
||||
type Actor,
|
||||
canReassignOwner,
|
||||
resolveOwnerId,
|
||||
} from '../common/record-scope';
|
||||
import { actorCanAssignToOthers, assertAssigneeSet } from '../common/assign-access';
|
||||
import { LoyaltyService } from '../loyalty/loyalty.service';
|
||||
import { AutomationsService } from '../automations/automations.service';
|
||||
import { MessageAutomationsService } from '../message-automations/message-automations.service';
|
||||
@@ -453,7 +453,11 @@ export class PeopleService implements OnModuleInit {
|
||||
mood: dto.mood ?? 'newcomer',
|
||||
tags: normalizeTags(dto.tags),
|
||||
companyId,
|
||||
ownerId: resolveOwnerId(actor, dto.ownerId),
|
||||
ownerId: resolveOwnerId(
|
||||
actor,
|
||||
dto.ownerId,
|
||||
await actorCanAssignToOthers(this.prisma, actor, 'customers'),
|
||||
),
|
||||
customFields: toJsonCustomFields(dto.customFields),
|
||||
...(registeredAt && !Number.isNaN(registeredAt.getTime())
|
||||
? { createdAt: registeredAt }
|
||||
@@ -574,8 +578,14 @@ export class PeopleService implements OnModuleInit {
|
||||
}
|
||||
data.companyId = nextCompanyId;
|
||||
}
|
||||
if (dto.ownerId !== undefined && canReassignOwner(actor.role)) {
|
||||
data.ownerId = resolveOwnerId(actor, dto.ownerId);
|
||||
if (dto.ownerId !== undefined) {
|
||||
const current = await this.prisma.person.findFirst({
|
||||
where: { id, organizationId: actor.organizationId },
|
||||
select: { ownerId: true },
|
||||
});
|
||||
const nextOwner = emptyToNull(dto.ownerId ?? null);
|
||||
await assertAssigneeSet(this.prisma, actor, 'customers', [nextOwner], [current?.ownerId]);
|
||||
data.ownerId = nextOwner ?? actor.userId;
|
||||
}
|
||||
|
||||
await this.assertNoDuplicate(actor.organizationId, data, id);
|
||||
|
||||
@@ -302,8 +302,6 @@ export class LeadsController {
|
||||
}
|
||||
|
||||
@Post('assign')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles('owner', 'admin')
|
||||
assign(@CurrentUser() user: Actor, @Body() dto: AssignLeadsDto) {
|
||||
return this.leads
|
||||
.assign(user, dto.leadIds, dto.assigneeId)
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
elevated,
|
||||
forScope,
|
||||
} from '../common/record-scope';
|
||||
import { actorCanAssignToOthers, assertAssigneeSet } from '../common/assign-access';
|
||||
import {
|
||||
asAddresses,
|
||||
collectAddresses,
|
||||
@@ -379,10 +380,11 @@ export class LeadsService {
|
||||
const stageId = dto.stageId || stages[0]?.id;
|
||||
if (!stageId) throw new BadRequestException('مرحلهای تعریف نشده');
|
||||
|
||||
const allowOthers = await actorCanAssignToOthers(this.prisma, actor, 'leads');
|
||||
let assigneeId: string | null = null;
|
||||
if (elevated(actor.role) && dto.assigneeId) {
|
||||
if (allowOthers && dto.assigneeId) {
|
||||
assigneeId = dto.assigneeId;
|
||||
} else if (!elevated(actor.role)) {
|
||||
} else if (!allowOthers) {
|
||||
assigneeId = actor.userId;
|
||||
}
|
||||
|
||||
@@ -570,8 +572,9 @@ export class LeadsService {
|
||||
if (typeof dto.stageId === 'string' && dto.stageId !== existing.stageId) {
|
||||
data.stage = { connect: { id: dto.stageId } };
|
||||
}
|
||||
if (elevated(actor.role) && dto.assigneeId !== undefined) {
|
||||
if (dto.assigneeId !== undefined) {
|
||||
const aid = dto.assigneeId ? String(dto.assigneeId) : null;
|
||||
await assertAssigneeSet(this.prisma, actor, 'leads', [aid], [existing.assigneeId]);
|
||||
data.assignee = aid
|
||||
? { connect: { id: aid } }
|
||||
: { disconnect: true };
|
||||
@@ -641,9 +644,7 @@ export class LeadsService {
|
||||
leadIds: string[],
|
||||
assigneeId: string,
|
||||
): Promise<number> {
|
||||
if (!elevated(actor.role)) {
|
||||
throw new BadRequestException('فقط مدیر میتواند تخصیص دهد');
|
||||
}
|
||||
await assertAssigneeSet(this.prisma, actor, 'leads', [assigneeId]);
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
id: assigneeId,
|
||||
|
||||
@@ -41,7 +41,9 @@ export const SYSTEM_ROLE_SEEDS: Array<{
|
||||
name: 'کارشناس فروش',
|
||||
description: 'امکان دسترسی به بخشهای فروش و مشتریان برای انجام فعالیتهای روزمره.',
|
||||
color: 'teal',
|
||||
permissions: ALL_ROLE_PERMISSIONS.filter((key) => key !== 'audit'),
|
||||
permissions: ALL_ROLE_PERMISSIONS.filter(
|
||||
(key) => key !== 'audit' && !key.startsWith('assign_'),
|
||||
),
|
||||
},
|
||||
{
|
||||
systemKey: 'support',
|
||||
|
||||
@@ -22,11 +22,11 @@ import { WebhooksService } from '../public-api/webhooks.service';
|
||||
import { GamificationService } from '../gamification/gamification.service';
|
||||
import {
|
||||
type Actor,
|
||||
canReassignOwner,
|
||||
dealAccessWhere,
|
||||
ownerWhere,
|
||||
resolveOwnerId,
|
||||
} from '../common/record-scope';
|
||||
import { actorCanAssignToOthers, assertAssigneeSet } from '../common/assign-access';
|
||||
|
||||
const dealInclude = {
|
||||
person: { select: { id: true, firstName: true, lastName: true } },
|
||||
@@ -106,6 +106,10 @@ export class DealsService {
|
||||
organizationId,
|
||||
dto.assigneeIds,
|
||||
);
|
||||
const assignActor = { userId: actor.userId, role: actor.role, organizationId };
|
||||
const allowOthers = await actorCanAssignToOthers(this.prisma, assignActor, 'sales');
|
||||
const ownerId = resolveOwnerId(actor, dto.ownerId, allowOthers);
|
||||
await assertAssigneeSet(this.prisma, assignActor, 'sales', [ownerId, ...assigneeIds]);
|
||||
|
||||
const maxPos = await this.prisma.deal.aggregate({
|
||||
where: { organizationId, stageId: stage.id },
|
||||
@@ -156,7 +160,7 @@ export class DealsService {
|
||||
createdById: actor.userId,
|
||||
personId: emptyToNull(dto.personId ?? null),
|
||||
companyId: emptyToNull(dto.companyId ?? null),
|
||||
ownerId: resolveOwnerId(actor, dto.ownerId),
|
||||
ownerId,
|
||||
position: (maxPos._max.position ?? -1) + 1,
|
||||
...(assigneeIds.length
|
||||
? { assignees: { connect: assigneeIds.map((id) => ({ id })) } }
|
||||
@@ -259,17 +263,38 @@ export class DealsService {
|
||||
}
|
||||
if ('personId' in dto) data.personId = emptyToNull(dto.personId ?? null);
|
||||
if ('companyId' in dto) data.companyId = emptyToNull(dto.companyId ?? null);
|
||||
if (dto.ownerId !== undefined && canReassignOwner(actor.role)) {
|
||||
data.ownerId = resolveOwnerId(actor, dto.ownerId);
|
||||
}
|
||||
|
||||
let assigneeConnect: { id: string }[] | undefined;
|
||||
if (dto.assigneeIds !== undefined) {
|
||||
const assigneeIds = await this.normalizeAssigneeIds(
|
||||
organizationId,
|
||||
dto.assigneeIds,
|
||||
);
|
||||
assigneeConnect = assigneeIds.map((uid) => ({ id: uid }));
|
||||
if (dto.ownerId !== undefined || dto.assigneeIds !== undefined) {
|
||||
const current = await this.prisma.deal.findFirst({
|
||||
where: { id, organizationId },
|
||||
select: { ownerId: true, assignees: { select: { id: true } } },
|
||||
});
|
||||
const assignActor = { userId: actor.userId, role: actor.role, organizationId };
|
||||
if (dto.ownerId !== undefined) {
|
||||
const nextOwner = emptyToNull(dto.ownerId ?? null);
|
||||
await assertAssigneeSet(
|
||||
this.prisma,
|
||||
assignActor,
|
||||
'sales',
|
||||
[nextOwner],
|
||||
[current?.ownerId],
|
||||
);
|
||||
data.ownerId = nextOwner ?? actor.userId;
|
||||
}
|
||||
if (dto.assigneeIds !== undefined) {
|
||||
const assigneeIds = await this.normalizeAssigneeIds(
|
||||
organizationId,
|
||||
dto.assigneeIds,
|
||||
);
|
||||
await assertAssigneeSet(
|
||||
this.prisma,
|
||||
assignActor,
|
||||
'sales',
|
||||
assigneeIds,
|
||||
current?.assignees.map((item) => item.id) ?? [],
|
||||
);
|
||||
assigneeConnect = assigneeIds.map((uid) => ({ id: uid }));
|
||||
}
|
||||
}
|
||||
|
||||
if (pricedFromLines && dto.lineItems) {
|
||||
|
||||
@@ -15,12 +15,12 @@ import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import {
|
||||
type Actor,
|
||||
canReassignOwner,
|
||||
dealAccessWhere,
|
||||
ownerWhere,
|
||||
projectCardAccessWhere,
|
||||
resolveOwnerId,
|
||||
} from '../common/record-scope';
|
||||
import { actorCanAssignToOthers, assertAssigneeSet } from '../common/assign-access';
|
||||
import { normalizeTags } from '../common/tags';
|
||||
|
||||
const DEFAULT_STAGES = ['صف', 'در حال انجام', 'بازبینی', 'انجامشده'] as const;
|
||||
@@ -206,6 +206,13 @@ export class ProjectBoardsService {
|
||||
});
|
||||
|
||||
const assigneeIds = [...new Set((dto.assigneeIds ?? []).map((id) => id.trim()).filter(Boolean))];
|
||||
const assignActor = { userId: actor.userId, role: actor.role, organizationId };
|
||||
const ownerId = resolveOwnerId(
|
||||
actor,
|
||||
dto.ownerId,
|
||||
await actorCanAssignToOthers(this.prisma, assignActor, 'projects'),
|
||||
);
|
||||
await assertAssigneeSet(this.prisma, assignActor, 'projects', [ownerId, ...assigneeIds]);
|
||||
const created = await this.prisma.projectCard.create({
|
||||
data: {
|
||||
organizationId,
|
||||
@@ -217,7 +224,7 @@ export class ProjectBoardsService {
|
||||
personId: emptyToNull(dto.personId),
|
||||
companyId: emptyToNull(dto.companyId),
|
||||
dealId: emptyToNull(dto.dealId),
|
||||
ownerId: resolveOwnerId(actor, dto.ownerId),
|
||||
ownerId,
|
||||
position: (maxPos._max.position ?? -1) + 1,
|
||||
...(assigneeIds.length
|
||||
? { assignees: { connect: assigneeIds.map((id) => ({ id })) } }
|
||||
@@ -257,24 +264,40 @@ export class ProjectBoardsService {
|
||||
if ('companyId' in dto) data.companyId = emptyToNull(dto.companyId ?? null);
|
||||
if ('dealId' in dto) data.dealId = emptyToNull(dto.dealId ?? null);
|
||||
if ('ownerId' in dto) {
|
||||
if (!canReassignOwner(actor.role) && dto.ownerId !== existing.ownerId) {
|
||||
throw new BadRequestException('تغییر مسئول مجاز نیست');
|
||||
}
|
||||
data.ownerId = emptyToNull(dto.ownerId ?? null);
|
||||
const nextOwner = emptyToNull(dto.ownerId ?? null);
|
||||
await assertAssigneeSet(
|
||||
this.prisma,
|
||||
{ userId: actor.userId, role: actor.role, organizationId },
|
||||
'projects',
|
||||
[nextOwner],
|
||||
[existing.ownerId],
|
||||
);
|
||||
data.ownerId = nextOwner;
|
||||
}
|
||||
|
||||
const nextAssigneeIds = dto.assigneeIds
|
||||
? [...new Set(dto.assigneeIds.map((x) => x.trim()).filter(Boolean))]
|
||||
: undefined;
|
||||
if (nextAssigneeIds) {
|
||||
const currentAssignees = await this.prisma.projectCard.findFirst({
|
||||
where: { id },
|
||||
select: { assignees: { select: { id: true } } },
|
||||
});
|
||||
await assertAssigneeSet(
|
||||
this.prisma,
|
||||
{ userId: actor.userId, role: actor.role, organizationId },
|
||||
'projects',
|
||||
nextAssigneeIds,
|
||||
currentAssignees?.assignees.map((item) => item.id) ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
const row = await this.prisma.projectCard.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...data,
|
||||
...(dto.assigneeIds
|
||||
? {
|
||||
assignees: {
|
||||
set: [...new Set(dto.assigneeIds.map((x) => x.trim()).filter(Boolean))].map(
|
||||
(uid) => ({ id: uid }),
|
||||
),
|
||||
},
|
||||
}
|
||||
...(nextAssigneeIds
|
||||
? { assignees: { set: nextAssigneeIds.map((uid) => ({ id: uid })) } }
|
||||
: {}),
|
||||
},
|
||||
include: cardInclude,
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import type { Actor } from '../common/record-scope';
|
||||
import { elevated } from '../common/record-scope';
|
||||
import { actorCanAssignToOthers, assignOthersDeniedMessage } from '../common/assign-access';
|
||||
import {
|
||||
ChangeRoutineAssigneeDto,
|
||||
ListRoutinesQueryDto,
|
||||
@@ -360,6 +361,11 @@ export class RoutinesService {
|
||||
assigneeId?: string | null,
|
||||
teamId?: string | null,
|
||||
): Promise<{ assigneeId: string | null; teamId: string | null }> {
|
||||
if ((assigneeId && assigneeId !== actor.userId) || teamId) {
|
||||
if (!(await actorCanAssignToOthers(this.prisma, actor, 'routines'))) {
|
||||
throw new ForbiddenException(assignOthersDeniedMessage('routines'));
|
||||
}
|
||||
}
|
||||
if (assigneeId) {
|
||||
if (!canManage(scope, assigneeId)) {
|
||||
throw new ForbiddenException('اجازه تعریف روتین برای این کاربر را ندارید');
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
elevated,
|
||||
ticketAccessWhere,
|
||||
} from '../common/record-scope';
|
||||
import { assertAssigneeSet } from '../common/assign-access';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { AutomationsService } from '../automations/automations.service';
|
||||
import { MessageAutomationsService } from '../message-automations/message-automations.service';
|
||||
@@ -136,9 +137,7 @@ export class TicketsService {
|
||||
if (assigneeId) {
|
||||
await this.assertUser(actor.organizationId, assigneeId);
|
||||
}
|
||||
if (!elevated(actor.role) && assigneeId && assigneeId !== actor.userId) {
|
||||
assigneeId = actor.userId;
|
||||
}
|
||||
await assertAssigneeSet(this.prisma, actor, 'tickets', [assigneeId]);
|
||||
|
||||
const number = await this.nextNumber(actor.organizationId);
|
||||
const status = (dto.status as never) || 'open';
|
||||
@@ -270,9 +269,7 @@ export class TicketsService {
|
||||
if ('assigneeId' in dto) {
|
||||
const next = emptyToNull(dto.assigneeId ?? null);
|
||||
if (next) await this.assertUser(actor.organizationId, next);
|
||||
if (!elevated(actor.role) && next && next !== actor.userId) {
|
||||
throw new BadRequestException('فقط میتوانید تیکت را به خودتان ارجاع دهید');
|
||||
}
|
||||
await assertAssigneeSet(this.prisma, actor, 'tickets', [next], [existing.assigneeId]);
|
||||
data.assigneeId = next;
|
||||
if (next && next !== existing.assigneeId && next !== actor.userId) {
|
||||
await this.notifications.create({
|
||||
|
||||
Reference in New Issue
Block a user