From 3d5f956eecb2f07803ca13b6e3618b37d33da3eb Mon Sep 17 00:00:00 2001 From: Mariano Date: Fri, 8 May 2026 14:10:05 +0100 Subject: [PATCH 1/2] fix(people): default portal email checkbox to checked and respect user choice Remove auto-send override based on published policies so the checkbox reflects actual behavior. Guard resend endpoint to employee/contractor only. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/api/src/people/people-invite.service.ts | 26 ++++++++++--------- .../all/components/InviteMembersModal.tsx | 2 +- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/apps/api/src/people/people-invite.service.ts b/apps/api/src/people/people-invite.service.ts index d7611a180..4a18baa00 100644 --- a/apps/api/src/people/people-invite.service.ts +++ b/apps/api/src/people/people-invite.service.ts @@ -45,16 +45,6 @@ export class PeopleInviteService { const results: InviteResult[] = []; - const hasPublishedPolicies = await db.policy.findFirst({ - where: { - organizationId, - status: 'published', - isArchived: false, - archivedAt: null, - }, - select: { id: true }, - }); - for (const invite of invites) { try { // Auditors can only invite auditors @@ -81,8 +71,7 @@ export class PeopleInviteService { const isStrictlyEmployee = isEmployee && !isPrivileged; const shouldSendPortalEmail = - (invite.sendPortalEmail || !!hasPublishedPolicies) && - isStrictlyEmployee; + !!invite.sendPortalEmail && isStrictlyEmployee; if (isStrictlyEmployee) { const result = await this.addEmployeeWithoutInvite( @@ -372,6 +361,19 @@ export class PeopleInviteService { throw new BadRequestException('Member not found.'); } + const roles = member.role.split(',').map((r) => r.trim()); + const isPrivileged = roles.some((role) => + ['admin', 'owner', 'auditor'].includes(role), + ); + const isEmployee = roles.some((role) => + ['employee', 'contractor'].includes(role), + ); + if (!isEmployee || isPrivileged) { + throw new BadRequestException( + 'Portal invites can only be sent to employee or contractor members.', + ); + } + const email = member.user.email; const inviteLink = this.buildPortalUrl(organizationId); diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/InviteMembersModal.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/InviteMembersModal.tsx index 6e6675de0..1365a51cf 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/InviteMembersModal.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/InviteMembersModal.tsx @@ -122,7 +122,7 @@ export function InviteMembersModal({ roles: DEFAULT_ROLES, }, ], - sendPortalEmail: false, + sendPortalEmail: true, csvFile: undefined, }, mode: 'onChange', From 10bcffbaef135f60196a5f376e13ec924fa3bb53 Mon Sep 17 00:00:00 2001 From: Mariano Date: Fri, 8 May 2026 14:20:41 +0100 Subject: [PATCH 2/2] refactor(people): use RBAC obligations instead of hardcoded role checks Check BUILT_IN_ROLE_OBLIGATIONS and custom role obligations from the DB to determine compliance obligation, rather than hardcoding role name lists. Also uses RESTRICTED_ROLES for the employee routing check. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/api/src/people/people-invite.service.ts | 60 +++++++++++++++----- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/apps/api/src/people/people-invite.service.ts b/apps/api/src/people/people-invite.service.ts index 4a18baa00..22a9df5ea 100644 --- a/apps/api/src/people/people-invite.service.ts +++ b/apps/api/src/people/people-invite.service.ts @@ -8,6 +8,10 @@ import { db } from '@db'; import { triggerEmail } from '../email/trigger-email'; import { InviteEmail } from '../email/templates/invite-member'; import { InvitePortalEmail } from '@trycompai/email'; +import { + BUILT_IN_ROLE_OBLIGATIONS, + RESTRICTED_ROLES, +} from '@trycompai/auth'; import type { InviteItemDto } from './dto/invite-people.dto'; import { checkAutoCompletePhases } from '../frameworks/frameworks-timeline.helper'; import { TimelinesService } from '../timelines/timelines.service'; @@ -62,16 +66,16 @@ export class PeopleInviteService { } const email = invite.email.toLowerCase(); - const isPrivileged = invite.roles.some((role) => - ['admin', 'owner', 'auditor'].includes(role), - ); - const isEmployee = invite.roles.some((role) => - ['employee', 'contractor'].includes(role), - ); - const isStrictlyEmployee = isEmployee && !isPrivileged; + const restrictedRoles: readonly string[] = RESTRICTED_ROLES; + const isStrictlyEmployee = + invite.roles.every((role) => restrictedRoles.includes(role)); + const hasCompliance = await this.rolesHaveComplianceObligation( + invite.roles, + organizationId, + ); const shouldSendPortalEmail = - !!invite.sendPortalEmail && isStrictlyEmployee; + !!invite.sendPortalEmail && hasCompliance; if (isStrictlyEmployee) { const result = await this.addEmployeeWithoutInvite( @@ -362,15 +366,13 @@ export class PeopleInviteService { } const roles = member.role.split(',').map((r) => r.trim()); - const isPrivileged = roles.some((role) => - ['admin', 'owner', 'auditor'].includes(role), + const hasCompliance = await this.rolesHaveComplianceObligation( + roles, + organizationId, ); - const isEmployee = roles.some((role) => - ['employee', 'contractor'].includes(role), - ); - if (!isEmployee || isPrivileged) { + if (!hasCompliance) { throw new BadRequestException( - 'Portal invites can only be sent to employee or contractor members.', + 'Portal invites can only be sent to members with compliance obligations.', ); } @@ -415,6 +417,34 @@ export class PeopleInviteService { }); } + private async rolesHaveComplianceObligation( + roles: string[], + organizationId: string, + ): Promise { + for (const role of roles) { + if (BUILT_IN_ROLE_OBLIGATIONS[role]?.compliance) return true; + } + + const customRoleNames = roles.filter((r) => !BUILT_IN_ROLE_OBLIGATIONS[r]); + if (customRoleNames.length === 0) return false; + + const customRoles = await db.organizationRole.findMany({ + where: { + organizationId, + name: { in: customRoleNames }, + }, + select: { obligations: true }, + }); + + return customRoles.some((role) => { + const obligations = + typeof role.obligations === 'string' + ? JSON.parse(role.obligations) + : role.obligations || {}; + return !!obligations.compliance; + }); + } + private buildPortalUrl(organizationId: string): string { const portalUrl = process.env.NEXT_PUBLIC_PORTAL_URL ?? 'https://portal.trycomp.ai';