Skip to content

Commit a8068c7

Browse files
[dev] [tofikwest] tofik/auditor-training-timestamps (#2046)
* feat(training): implement training completion email with certificate attachment * feat(training): enhance training certificate generation with member validation * feat(training): add internal API token validation for certificate generation --------- Co-authored-by: Tofik Hasanov <annexcies@gmail.com>
1 parent 648a06b commit a8068c7

25 files changed

Lines changed: 1416 additions & 56 deletions

apps/api/.env.example

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,9 @@ TRIGGER_SECRET_KEY=
2828

2929
OPENAI_API_KEY=
3030
ANTHROPIC_API_KEY=
31-
GROQ_API_KEY=
31+
GROQ_API_KEY=
32+
33+
# Resend (for sending emails)
34+
RESEND_API_KEY=
35+
RESEND_FROM_SYSTEM= # e.g., noreply@mail.trycomp.ai
36+
RESEND_FROM_DEFAULT= # e.g., hello@mail.trycomp.ai

apps/api/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { CloudSecurityModule } from './cloud-security/cloud-security.module';
3030
import { BrowserbaseModule } from './browserbase/browserbase.module';
3131
import { TaskManagementModule } from './task-management/task-management.module';
3232
import { AssistantChatModule } from './assistant-chat/assistant-chat.module';
33+
import { TrainingModule } from './training/training.module';
3334

3435
@Module({
3536
imports: [
@@ -72,6 +73,7 @@ import { AssistantChatModule } from './assistant-chat/assistant-chat.module';
7273
BrowserbaseModule,
7374
TaskManagementModule,
7475
AssistantChatModule,
76+
TrainingModule,
7577
],
7678
controllers: [AppController],
7779
providers: [

apps/api/src/email/resend.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ export const resend = process.env.RESEND_API_KEY
55
? new Resend(process.env.RESEND_API_KEY)
66
: null;
77

8+
export interface EmailAttachment {
9+
filename: string;
10+
content: Buffer | string;
11+
contentType?: string;
12+
}
13+
814
export const sendEmail = async ({
915
to,
1016
subject,
@@ -14,6 +20,7 @@ export const sendEmail = async ({
1420
test,
1521
cc,
1622
scheduledAt,
23+
attachments,
1724
}: {
1825
to: string;
1926
subject: string;
@@ -23,6 +30,7 @@ export const sendEmail = async ({
2330
test?: boolean;
2431
cc?: string | string[];
2532
scheduledAt?: string;
33+
attachments?: EmailAttachment[];
2634
}) => {
2735
if (!resend) {
2836
throw new Error('Resend not initialized - missing API key');
@@ -64,6 +72,11 @@ export const sendEmail = async ({
6472
// @ts-ignore – React node allowed by the SDK
6573
react,
6674
scheduledAt,
75+
attachments: attachments?.map((att) => ({
76+
filename: att.filename,
77+
content: att.content,
78+
contentType: att.contentType,
79+
})),
6780
});
6881

6982
if (error) {
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {
2+
Body,
3+
Container,
4+
Font,
5+
Heading,
6+
Html,
7+
Preview,
8+
Section,
9+
Tailwind,
10+
Text,
11+
} from '@react-email/components';
12+
import { Footer } from '../components/footer';
13+
import { Logo } from '../components/logo';
14+
15+
interface Props {
16+
email: string;
17+
userName: string;
18+
organizationName: string;
19+
completedAt: Date;
20+
}
21+
22+
export const TrainingCompletedEmail = ({
23+
email,
24+
userName,
25+
organizationName,
26+
completedAt,
27+
}: Props) => {
28+
const formattedDate = new Date(completedAt).toLocaleDateString('en-US', {
29+
year: 'numeric',
30+
month: 'long',
31+
day: 'numeric',
32+
});
33+
34+
return (
35+
<Html>
36+
<Tailwind>
37+
<head>
38+
<Font
39+
fontFamily="Geist"
40+
fallbackFontFamily="Helvetica"
41+
fontWeight={400}
42+
fontStyle="normal"
43+
/>
44+
<Font
45+
fontFamily="Geist"
46+
fallbackFontFamily="Helvetica"
47+
fontWeight={500}
48+
fontStyle="normal"
49+
/>
50+
</head>
51+
<Preview>
52+
Congratulations! You've completed your Security Awareness Training
53+
</Preview>
54+
55+
<Body className="mx-auto my-auto bg-[#fff] font-sans">
56+
<Container
57+
className="mx-auto my-[40px] max-w-[600px] border-transparent p-[20px] md:border-[#E8E7E1]"
58+
style={{ borderStyle: 'solid', borderWidth: 1 }}
59+
>
60+
<Logo />
61+
<Heading className="mx-0 my-[30px] p-0 text-center text-[24px] font-normal text-[#121212]">
62+
Training Complete!
63+
</Heading>
64+
65+
<Text className="text-[14px] leading-[24px] text-[#121212]">
66+
Hi {userName},
67+
</Text>
68+
69+
<Text className="text-[14px] leading-[24px] text-[#121212]">
70+
Congratulations! You have successfully completed all Security
71+
Awareness Training modules for <strong>{organizationName}</strong>
72+
.
73+
</Text>
74+
75+
<Section
76+
className="mt-[24px] mb-[24px] rounded-[8px] p-[24px] text-center"
77+
style={{ backgroundColor: '#f0fdf4', border: '1px solid #bbf7d0' }}
78+
>
79+
<Text className="m-0 text-[16px] font-medium text-[#166534]">
80+
Completion Date: {formattedDate}
81+
</Text>
82+
</Section>
83+
84+
<Text className="text-[14px] leading-[24px] text-[#121212]">
85+
Your training completion certificate is attached to this email.
86+
Please save it for your records.
87+
</Text>
88+
89+
<Text className="text-[14px] leading-[24px] text-[#121212]">
90+
Thank you for your commitment to maintaining security awareness
91+
and helping protect {organizationName}.
92+
</Text>
93+
94+
<br />
95+
<Section>
96+
<Text className="text-[12px] leading-[24px] text-[#666666]">
97+
This notification was intended for{' '}
98+
<span className="text-[#121212]">{email}</span>.
99+
</Text>
100+
</Section>
101+
102+
<br />
103+
104+
<Footer />
105+
</Container>
106+
</Body>
107+
</Tailwind>
108+
</Html>
109+
);
110+
};
111+
112+
export default TrainingCompletedEmail;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsString, IsNotEmpty } from 'class-validator';
3+
4+
export class SendTrainingCompletionDto {
5+
@ApiProperty({
6+
description: 'The member ID who completed training',
7+
example: 'mem_abc123',
8+
})
9+
@IsString()
10+
@IsNotEmpty()
11+
memberId: string;
12+
13+
@ApiProperty({
14+
description: 'The organization ID',
15+
example: 'org_abc123',
16+
})
17+
@IsString()
18+
@IsNotEmpty()
19+
organizationId: string;
20+
}
21+
22+
export class SendTrainingCompletionResponseDto {
23+
@ApiProperty({
24+
description: 'Whether the email was sent',
25+
example: true,
26+
})
27+
sent: boolean;
28+
29+
@ApiProperty({
30+
description: 'Reason if email was not sent',
31+
example: 'training_not_complete',
32+
required: false,
33+
})
34+
reason?: string;
35+
}

0 commit comments

Comments
 (0)