From 648a06bea7ff706ccbf080cc98c5a1f811e84eeb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 11:31:23 -0500 Subject: [PATCH 01/16] fix(integration): update task status logic to account for failed checks (#2044) Co-authored-by: Mariano Fuentes --- .../run-task-integration-checks.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/api/src/trigger/integration-platform/run-task-integration-checks.ts b/apps/api/src/trigger/integration-platform/run-task-integration-checks.ts index c9945b617..025366f72 100644 --- a/apps/api/src/trigger/integration-platform/run-task-integration-checks.ts +++ b/apps/api/src/trigger/integration-platform/run-task-integration-checks.ts @@ -134,6 +134,7 @@ export const runTaskIntegrationChecks = task({ // Track overall results across all checks for this task let totalFindings = 0; let totalPassing = 0; + let hasFailedChecks = false; // Run only the checks that apply to this task try { @@ -159,6 +160,9 @@ export const runTaskIntegrationChecks = task({ // Accumulate results totalFindings += checkResult.result.findings.length; totalPassing += checkResult.result.passingResults.length; + if (checkResult.status === 'failed' || checkResult.status === 'error') { + hasFailedChecks = true; + } // Store check run const checkRun = await db.integrationCheckRun.create({ @@ -228,15 +232,15 @@ export const runTaskIntegrationChecks = task({ }); // Update task status based on check results - // If any findings (failures), mark as failed - // If all passing and no findings, mark as done (only if not already done) - if (totalFindings > 0) { + // If any findings or check failures, mark as failed + // If all checks pass with no findings, mark as done (only if not already done) + if (totalFindings > 0 || hasFailedChecks) { await db.task.update({ where: { id: taskId }, data: { status: 'failed' }, }); logger.info( - `Task ${taskId} marked as failed due to ${totalFindings} findings`, + `Task ${taskId} marked as failed due to ${totalFindings} findings${hasFailedChecks ? ' and failed checks' : ''}`, ); } else if (totalPassing > 0) { // Only update to done if not already done @@ -282,7 +286,11 @@ export const runTaskIntegrationChecks = task({ totalPassing, totalFindings, taskStatus: - totalFindings > 0 ? 'failed' : totalPassing > 0 ? 'done' : null, + totalFindings > 0 || hasFailedChecks + ? 'failed' + : totalPassing > 0 + ? 'done' + : null, }; } catch (error) { logger.error(`Failed to run checks for task ${taskId}`, { From a8068c7e24c12ba14eff358b4b6c785a5fcab7b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 14:14:04 -0500 Subject: [PATCH 02/16] [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 --- apps/api/.env.example | 7 +- apps/api/src/app.module.ts | 2 + apps/api/src/email/resend.ts | 13 + .../email/templates/training-completed.tsx | 112 +++++++++ .../dto/send-training-completion.dto.ts | 35 +++ .../training-certificate-pdf.service.ts | 230 ++++++++++++++++++ .../src/training/training-email.service.ts | 60 +++++ apps/api/src/training/training.controller.ts | 120 +++++++++ apps/api/src/training/training.module.ts | 16 ++ apps/api/src/training/training.service.ts | 193 +++++++++++++++ apps/app/.env.example | 5 +- .../actions/download-training-certificate.ts | 68 ++++++ .../[employeeId]/components/Employee.tsx | 5 +- .../[employeeId]/components/EmployeeTasks.tsx | 163 ++++++++++--- .../[orgId]/people/[employeeId]/page.tsx | 10 + apps/portal/.env.example | 4 + apps/portal/package.json | 1 + .../(home)/actions/markVideoAsCompleted.ts | 124 ++++++++-- apps/portal/src/env.mjs | 4 + bun.lock | 1 + packages/docs/openapi.json | 119 +++++++++ packages/email/emails/training-completed.tsx | 120 +++++++++ packages/email/index.ts | 2 + packages/email/lib/resend.ts | 13 + packages/email/lib/training-completed.ts | 45 ++++ 25 files changed, 1416 insertions(+), 56 deletions(-) create mode 100644 apps/api/src/email/templates/training-completed.tsx create mode 100644 apps/api/src/training/dto/send-training-completion.dto.ts create mode 100644 apps/api/src/training/training-certificate-pdf.service.ts create mode 100644 apps/api/src/training/training-email.service.ts create mode 100644 apps/api/src/training/training.controller.ts create mode 100644 apps/api/src/training/training.module.ts create mode 100644 apps/api/src/training/training.service.ts create mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/actions/download-training-certificate.ts create mode 100644 packages/email/emails/training-completed.tsx create mode 100644 packages/email/lib/training-completed.ts diff --git a/apps/api/.env.example b/apps/api/.env.example index 290ce034a..85f9ccd9b 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -28,4 +28,9 @@ TRIGGER_SECRET_KEY= OPENAI_API_KEY= ANTHROPIC_API_KEY= -GROQ_API_KEY= \ No newline at end of file +GROQ_API_KEY= + +# Resend (for sending emails) +RESEND_API_KEY= +RESEND_FROM_SYSTEM= # e.g., noreply@mail.trycomp.ai +RESEND_FROM_DEFAULT= # e.g., hello@mail.trycomp.ai \ No newline at end of file diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index afae6031c..4cfeae05a 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -30,6 +30,7 @@ import { CloudSecurityModule } from './cloud-security/cloud-security.module'; import { BrowserbaseModule } from './browserbase/browserbase.module'; import { TaskManagementModule } from './task-management/task-management.module'; import { AssistantChatModule } from './assistant-chat/assistant-chat.module'; +import { TrainingModule } from './training/training.module'; @Module({ imports: [ @@ -72,6 +73,7 @@ import { AssistantChatModule } from './assistant-chat/assistant-chat.module'; BrowserbaseModule, TaskManagementModule, AssistantChatModule, + TrainingModule, ], controllers: [AppController], providers: [ diff --git a/apps/api/src/email/resend.ts b/apps/api/src/email/resend.ts index b16b232be..998ecff0d 100644 --- a/apps/api/src/email/resend.ts +++ b/apps/api/src/email/resend.ts @@ -5,6 +5,12 @@ export const resend = process.env.RESEND_API_KEY ? new Resend(process.env.RESEND_API_KEY) : null; +export interface EmailAttachment { + filename: string; + content: Buffer | string; + contentType?: string; +} + export const sendEmail = async ({ to, subject, @@ -14,6 +20,7 @@ export const sendEmail = async ({ test, cc, scheduledAt, + attachments, }: { to: string; subject: string; @@ -23,6 +30,7 @@ export const sendEmail = async ({ test?: boolean; cc?: string | string[]; scheduledAt?: string; + attachments?: EmailAttachment[]; }) => { if (!resend) { throw new Error('Resend not initialized - missing API key'); @@ -64,6 +72,11 @@ export const sendEmail = async ({ // @ts-ignore – React node allowed by the SDK react, scheduledAt, + attachments: attachments?.map((att) => ({ + filename: att.filename, + content: att.content, + contentType: att.contentType, + })), }); if (error) { diff --git a/apps/api/src/email/templates/training-completed.tsx b/apps/api/src/email/templates/training-completed.tsx new file mode 100644 index 000000000..0b9da73a8 --- /dev/null +++ b/apps/api/src/email/templates/training-completed.tsx @@ -0,0 +1,112 @@ +import { + Body, + Container, + Font, + Heading, + Html, + 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; + completedAt: Date; +} + +export const TrainingCompletedEmail = ({ + email, + userName, + organizationName, + completedAt, +}: Props) => { + const formattedDate = new Date(completedAt).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); + + return ( + + + + + + + + Congratulations! You've completed your Security Awareness Training + + + + + + + Training Complete! + + + + Hi {userName}, + + + + Congratulations! You have successfully completed all Security + Awareness Training modules for {organizationName} + . + + +
+ + Completion Date: {formattedDate} + +
+ + + Your training completion certificate is attached to this email. + Please save it for your records. + + + + Thank you for your commitment to maintaining security awareness + and helping protect {organizationName}. + + +
+
+ + This notification was intended for{' '} + {email}. + +
+ +
+ +