Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 51 additions & 19 deletions apps/api/src/people/people-invite.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -45,16 +49,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
Expand All @@ -72,17 +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 || !!hasPublishedPolicies) &&
isStrictlyEmployee;
!!invite.sendPortalEmail && hasCompliance;

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

const roles = member.role.split(',').map((r) => r.trim());
const hasCompliance = await this.rolesHaveComplianceObligation(
roles,
organizationId,
);
if (!hasCompliance) {
Comment thread
Marfuen marked this conversation as resolved.
throw new BadRequestException(
'Portal invites can only be sent to members with compliance obligations.',
);
}

const email = member.user.email;
const inviteLink = this.buildPortalUrl(organizationId);

Expand Down Expand Up @@ -413,6 +417,34 @@ export class PeopleInviteService {
});
}

private async rolesHaveComplianceObligation(
roles: string[],
organizationId: string,
): Promise<boolean> {
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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export function InviteMembersModal({
roles: DEFAULT_ROLES,
},
],
sendPortalEmail: false,
sendPortalEmail: true,
csvFile: undefined,
},
mode: 'onChange',
Expand Down
Loading