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:
2026-11-03 13:15:21 +03:30
co-authored by Cursor
parent 79733e6c96
commit 534dae45b2
25 changed files with 355 additions and 84 deletions
+14 -4
View File
@@ -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);
+14 -4
View File
@@ -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);