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
48 changes: 47 additions & 1 deletion apps/app/src/actions/policies/publish-all.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use server';

import { db, PolicyStatus } from '@db';
import { sendPublishAllPoliciesEmail } from '@/jobs/tasks/email/publish-all-policies-email';
import { db, PolicyStatus, Role } from '@db';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
import { authActionClient } from '../safe-action';
Expand Down Expand Up @@ -93,6 +94,51 @@ export const publishAllPoliciesAction = authActionClient
}
}

// Get organization info and all members to send emails
const organization = await db.organization.findUnique({
where: { id: parsedInput.organizationId },
select: { name: true },
});

const members = await db.member.findMany({
where: {
organizationId: parsedInput.organizationId,
isActive: true,
role: {
contains: Role.employee,
},
},
include: {
user: {
select: {
email: true,
name: true,
},
},
},
});

// Trigger email tasks for all employees using batchTrigger
const emailPayloads = members
.filter((orgMember) => orgMember.user.email)
.map((orgMember) => ({
payload: {
email: orgMember.user.email,
userName: orgMember.user.name || 'there',
organizationName: organization?.name || 'Your organization',
organizationId: parsedInput.organizationId,
},
}));

if (emailPayloads.length > 0) {
try {
await sendPublishAllPoliciesEmail.batchTrigger(emailPayloads);
} catch (emailError) {
console.error('[publish-all-policies] Failed to trigger bulk emails:', emailError);
// Don't throw - the policies are published successfully
}
}

revalidatePath(`/${parsedInput.organizationId}/policies`);
revalidatePath(`/${parsedInput.organizationId}/frameworks`);
return {
Expand Down
48 changes: 48 additions & 0 deletions apps/app/src/jobs/tasks/email/publish-all-policies-email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { sendAllPolicyNotificationEmail } from '@comp/email';
import { logger, queue, task } from '@trigger.dev/sdk';

// Queue with concurrency limit to ensure rate limiting
const allPolicyEmailQueue = queue({
name: 'all-policy-email-queue',
concurrencyLimit: 2,
});

interface AllPolicyEmailPayload {
email: string;
userName: string;
organizationId: string;
organizationName: string;
}

export const sendPublishAllPoliciesEmail = task({
id: 'send-publish-all-policies-email',
queue: allPolicyEmailQueue,
run: async (payload: AllPolicyEmailPayload) => {
logger.info('Sending all policies published email', {
email: payload.email,
organizationName: payload.organizationName,
});

try {
await sendAllPolicyNotificationEmail(payload);

logger.info('Successfully sent all policies email', {
email: payload.email,
organizationName: payload.organizationName,
});

return {
success: true,
email: payload.email,
};
} catch (error) {
logger.error('Failed to send all policies email', {
email: payload.email,
error: error instanceof Error ? error.message : String(error),
});

throw error;
}
},
});

4 changes: 2 additions & 2 deletions packages/docs/cloud-tests/aws.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ Before setting up the integration, ensure you have:
- On the permissions screen, click **Attach policies directly**
- Attach the following AWS managed policies:
- `SecurityAudit`
- `ReadOnlyAccess`
- `AmazonEC2ReadOnlyAccess & AWSSecurityHubReadOnlyAccess`

_(Or use a custom least-privilege policy — see example below)_
_(Or use a custom least-privilege policy — see example below)_
6. **Create the User**
- Click **Next**, then **Create user**
- Copy and securely store the **Access Key ID** and **Secret Access Key**
Expand Down
118 changes: 118 additions & 0 deletions packages/email/emails/all-policy-notification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {
Body,
Button,
Container,
Font,
Heading,
Html,
Link,
Preview,
Section,
Tailwind,
Text,
} from '@react-email/components';
import { Footer } from '../components/footer';
import { Logo } from '../components/logo';

interface Props {
email: string;
userName: string;
organizationName: string;
organizationId: string;
}

export const AllPolicyNotificationEmail = ({
email,
userName,
organizationName,
organizationId,
}: Props) => {
const link = `${process.env.NEXT_PUBLIC_PORTAL_URL ?? 'https://portal.trycomp.ai'}/${organizationId}`;
const subjectText = 'Please review and accept the policies';

return (
<Html>
<Tailwind>
<head>
<Font
fontFamily="Geist"
fallbackFontFamily="Helvetica"
webFont={{
url: 'https://app.trycomp.ai/fonts/geist/geist-sans-latin-400-normal.woff2',
format: 'woff2',
}}
fontWeight={400}
fontStyle="normal"
/>

<Font
fontFamily="Geist"
fallbackFontFamily="Helvetica"
webFont={{
url: 'https://app.trycomp.ai/fonts/geist/geist-sans-latin-500-normal.woff2',
format: 'woff2',
}}
fontWeight={500}
fontStyle="normal"
/>
</head>

<Preview>{subjectText}</Preview>

<Body className="mx-auto my-auto bg-[#fff] font-sans">
<Container
className="mx-auto my-[40px] max-w-[600px] border-transparent p-[20px] md:border-[#E8E7E1]"
style={{ borderStyle: 'solid', borderWidth: 1 }}
>
<Logo />
<Heading className="mx-0 my-[30px] p-0 text-center text-[24px] font-normal text-[#121212]">
{subjectText}
</Heading>

<Text className="text-[14px] leading-[24px] text-[#121212]">
Hi {userName},
</Text>

<Text className="text-[14px] leading-[24px] text-[#121212]">
All policies have been published and require your review.
</Text>

<Text className="text-[14px] leading-[24px] text-[#121212]">
Your organization <strong>{organizationName}</strong> requires all employees to review and accept these policies.
</Text>

<Section className="mt-[32px] mb-[42px] text-center">
<Button
className="text-primary border border-solid border-[#121212] bg-transparent px-6 py-3 text-center text-[14px] font-medium text-[#121212] no-underline"
href={link}
>
Review & Accept Policies
</Button>
</Section>

<Text className="text-[14px] leading-[24px] break-all text-[#707070]">
or copy and paste this URL into your browser{' '}
<Link href={link} className="text-[#707070] underline">
{link}
</Link>
</Text>

<br />
<Section>
<Text className="text-[12px] leading-[24px] text-[#666666]">
This notification was intended for <span className="text-[#121212]">{email}</span>.
</Text>
</Section>

<br />

<Footer />
</Container>
</Body>
</Tailwind>
</Html>
);
};

export default AllPolicyNotificationEmail;

2 changes: 2 additions & 0 deletions packages/email/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ export * from './emails/magic-link';
export * from './emails/marketing/welcome';
export * from './emails/otp';
export * from './emails/policy-notification';
export * from './emails/all-policy-notification';
export * from './emails/waitlist';

// Email sending functions
export * from './lib/invite-member';
export * from './lib/magic-link';
export * from './lib/policy-notification';
export * from './lib/all-policy-notification';
export * from './lib/resend';
export * from './lib/waitlist';
42 changes: 42 additions & 0 deletions packages/email/lib/all-policy-notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import AllPolicyNotificationEmail from '../emails/all-policy-notification';
import { sendEmail } from './resend';

export const sendAllPolicyNotificationEmail = async (params: {
email: string;
userName: string;
organizationName: string;
organizationId: string;
}) => {
const {
email,
userName,
organizationName,
organizationId,
} = params;
const subjectText = 'Please review and accept the policies';

try {
const sent = await sendEmail({
to: email,
subject: subjectText,
react: AllPolicyNotificationEmail({
email,
userName,
organizationName,
organizationId,
}),
system: true, // Use system email address
});

if (!sent) {
console.error('Failed to send all-policy notification email');
return { success: false };
}

return { success: true };
} catch (error) {
console.error('Error sending all-policy notification email:', error);
return { success: false };
}
};

Loading