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/package.json b/apps/api/package.json index a821d9618..acc62c3f0 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -54,6 +54,7 @@ "reflect-metadata": "^0.2.2", "resend": "^6.4.2", "rxjs": "^7.8.1", + "safe-stable-stringify": "^2.5.0", "swagger-ui-express": "^5.0.1", "xlsx": "^0.18.5", "zod": "^4.0.14" diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index afae6031c..31be83c2c 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -17,10 +17,13 @@ import { OrganizationModule } from './organization/organization.module'; import { PoliciesModule } from './policies/policies.module'; import { RisksModule } from './risks/risks.module'; import { TasksModule } from './tasks/tasks.module'; +import { EvidenceExportModule } from './tasks/evidence-export/evidence-export.module'; import { VendorsModule } from './vendors/vendors.module'; import { ContextModule } from './context/context.module'; import { TrustPortalModule } from './trust-portal/trust-portal.module'; import { TaskTemplateModule } from './framework-editor/task-template/task-template.module'; +import { FindingTemplateModule } from './finding-template/finding-template.module'; +import { FindingsModule } from './findings/findings.module'; import { QuestionnaireModule } from './questionnaire/questionnaire.module'; import { VectorStoreModule } from './vector-store/vector-store.module'; import { KnowledgeBaseModule } from './knowledge-base/knowledge-base.module'; @@ -30,6 +33,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: [ @@ -59,10 +63,13 @@ import { AssistantChatModule } from './assistant-chat/assistant-chat.module'; DeviceAgentModule, AttachmentsModule, TasksModule, + EvidenceExportModule, CommentsModule, HealthModule, TrustPortalModule, TaskTemplateModule, + FindingTemplateModule, + FindingsModule, QuestionnaireModule, VectorStoreModule, KnowledgeBaseModule, @@ -72,6 +79,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/finding-notification.tsx b/apps/api/src/email/templates/finding-notification.tsx new file mode 100644 index 000000000..5d85b22bd --- /dev/null +++ b/apps/api/src/email/templates/finding-notification.tsx @@ -0,0 +1,147 @@ +import * as React from 'react'; +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'; +import { getUnsubscribeUrl } from '@trycompai/email'; + +interface Props { + toName: string; + toEmail: string; + heading: string; + message: string; + taskTitle: string; + organizationName: string; + findingType: string; + findingContent: string; + newStatus?: string; + findingUrl: string; +} + +export const FindingNotificationEmail = ({ + toName, + toEmail, + heading, + message, + taskTitle, + organizationName, + findingType, + findingContent, + newStatus, + findingUrl, +}: Props) => { + const unsubscribeUrl = getUnsubscribeUrl(toEmail); + return ( + + + + + + + + {heading}: {taskTitle} + + + + + + + {heading} + + + + Hello {toName}, + + + + {message} + + + {/* Finding Details Box */} +
+ + Finding Details + + + + Organization: {organizationName} + + + + Task: {taskTitle} + + + + Type: {findingType} + {newStatus && <> | Status: {newStatus}} + + + + "{findingContent}" + +
+ +
+ +
+ + + or copy and paste this URL into your browser:{' '} + + {findingUrl} + + + +
+ + Don't want to receive finding notifications?{' '} + + Manage your email preferences + + . + +
+ +
+ +