Skip to content

Commit c413a9a

Browse files
Marfuenclaude
andauthored
fix(people): default portal email checkbox to checked, respect user choice (#2806)
* 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 871d706 commit c413a9a

2 files changed

Lines changed: 52 additions & 20 deletions

File tree

apps/api/src/people/people-invite.service.ts

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import { db } from '@db';
88
import { triggerEmail } from '../email/trigger-email';
99
import { InviteEmail } from '../email/templates/invite-member';
1010
import { InvitePortalEmail } from '@trycompai/email';
11+
import {
12+
BUILT_IN_ROLE_OBLIGATIONS,
13+
RESTRICTED_ROLES,
14+
} from '@trycompai/auth';
1115
import type { InviteItemDto } from './dto/invite-people.dto';
1216
import { checkAutoCompletePhases } from '../frameworks/frameworks-timeline.helper';
1317
import { TimelinesService } from '../timelines/timelines.service';
@@ -45,16 +49,6 @@ export class PeopleInviteService {
4549

4650
const results: InviteResult[] = [];
4751

48-
const hasPublishedPolicies = await db.policy.findFirst({
49-
where: {
50-
organizationId,
51-
status: 'published',
52-
isArchived: false,
53-
archivedAt: null,
54-
},
55-
select: { id: true },
56-
});
57-
5852
for (const invite of invites) {
5953
try {
6054
// Auditors can only invite auditors
@@ -72,17 +66,16 @@ export class PeopleInviteService {
7266
}
7367

7468
const email = invite.email.toLowerCase();
75-
const isPrivileged = invite.roles.some((role) =>
76-
['admin', 'owner', 'auditor'].includes(role),
77-
);
78-
const isEmployee = invite.roles.some((role) =>
79-
['employee', 'contractor'].includes(role),
80-
);
81-
const isStrictlyEmployee = isEmployee && !isPrivileged;
69+
const restrictedRoles: readonly string[] = RESTRICTED_ROLES;
70+
const isStrictlyEmployee =
71+
invite.roles.every((role) => restrictedRoles.includes(role));
8272

73+
const hasCompliance = await this.rolesHaveComplianceObligation(
74+
invite.roles,
75+
organizationId,
76+
);
8377
const shouldSendPortalEmail =
84-
(invite.sendPortalEmail || !!hasPublishedPolicies) &&
85-
isStrictlyEmployee;
78+
!!invite.sendPortalEmail && hasCompliance;
8679

8780
if (isStrictlyEmployee) {
8881
const result = await this.addEmployeeWithoutInvite(
@@ -372,6 +365,17 @@ export class PeopleInviteService {
372365
throw new BadRequestException('Member not found.');
373366
}
374367

368+
const roles = member.role.split(',').map((r) => r.trim());
369+
const hasCompliance = await this.rolesHaveComplianceObligation(
370+
roles,
371+
organizationId,
372+
);
373+
if (!hasCompliance) {
374+
throw new BadRequestException(
375+
'Portal invites can only be sent to members with compliance obligations.',
376+
);
377+
}
378+
375379
const email = member.user.email;
376380
const inviteLink = this.buildPortalUrl(organizationId);
377381

@@ -413,6 +417,34 @@ export class PeopleInviteService {
413417
});
414418
}
415419

420+
private async rolesHaveComplianceObligation(
421+
roles: string[],
422+
organizationId: string,
423+
): Promise<boolean> {
424+
for (const role of roles) {
425+
if (BUILT_IN_ROLE_OBLIGATIONS[role]?.compliance) return true;
426+
}
427+
428+
const customRoleNames = roles.filter((r) => !BUILT_IN_ROLE_OBLIGATIONS[r]);
429+
if (customRoleNames.length === 0) return false;
430+
431+
const customRoles = await db.organizationRole.findMany({
432+
where: {
433+
organizationId,
434+
name: { in: customRoleNames },
435+
},
436+
select: { obligations: true },
437+
});
438+
439+
return customRoles.some((role) => {
440+
const obligations =
441+
typeof role.obligations === 'string'
442+
? JSON.parse(role.obligations)
443+
: role.obligations || {};
444+
return !!obligations.compliance;
445+
});
446+
}
447+
416448
private buildPortalUrl(organizationId: string): string {
417449
const portalUrl =
418450
process.env.NEXT_PUBLIC_PORTAL_URL ?? 'https://portal.trycomp.ai';

apps/app/src/app/(app)/[orgId]/people/all/components/InviteMembersModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export function InviteMembersModal({
122122
roles: DEFAULT_ROLES,
123123
},
124124
],
125-
sendPortalEmail: false,
125+
sendPortalEmail: true,
126126
csvFile: undefined,
127127
},
128128
mode: 'onChange',

0 commit comments

Comments
 (0)