Implement a complete investor access system for the pitch deck: - Passwordless magic link auth (jose JWT + nodemailer SMTP) - Per-investor audit logging (slide views, assumption changes, chat) - Financial model snapshot persistence (auto-save/restore per investor) - PWA support (manifest, service worker, offline caching, icons) - Security safeguards (watermark overlay, rate limiting, anti-scraping headers, content protection, single-session enforcement) - Admin API for invite/revoke/audit-log management - Integrated into docker-compose.coolify.yml for production deployment Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
92 lines
3.9 KiB
TypeScript
92 lines
3.9 KiB
TypeScript
import nodemailer from 'nodemailer'
|
|
|
|
const transporter = nodemailer.createTransport({
|
|
host: process.env.SMTP_HOST,
|
|
port: parseInt(process.env.SMTP_PORT || '587'),
|
|
secure: process.env.SMTP_PORT === '465',
|
|
auth: {
|
|
user: process.env.SMTP_USERNAME,
|
|
pass: process.env.SMTP_PASSWORD,
|
|
},
|
|
})
|
|
|
|
const fromName = process.env.SMTP_FROM_NAME || 'BreakPilot'
|
|
const fromAddr = process.env.SMTP_FROM_ADDR || 'noreply@breakpilot.ai'
|
|
|
|
export async function sendMagicLinkEmail(
|
|
to: string,
|
|
investorName: string | null,
|
|
magicLinkUrl: string
|
|
): Promise<void> {
|
|
const greeting = investorName ? `Hello ${investorName}` : 'Hello'
|
|
|
|
await transporter.sendMail({
|
|
from: `"${fromName}" <${fromAddr}>`,
|
|
to,
|
|
subject: 'Your BreakPilot Pitch Deck Access',
|
|
html: `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
</head>
|
|
<body style="margin:0;padding:0;background:#0a0a1a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#0a0a1a;padding:40px 20px;">
|
|
<tr>
|
|
<td align="center">
|
|
<table width="560" cellpadding="0" cellspacing="0" style="background:#111127;border-radius:12px;border:1px solid rgba(99,102,241,0.2);">
|
|
<tr>
|
|
<td style="padding:40px 40px 20px;">
|
|
<h1 style="margin:0;font-size:24px;color:#e0e0ff;font-weight:600;">
|
|
BreakPilot ComplAI
|
|
</h1>
|
|
<p style="margin:8px 0 0;font-size:13px;color:rgba(255,255,255,0.4);">
|
|
Investor Pitch Deck
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding:20px 40px;">
|
|
<p style="margin:0 0 16px;font-size:16px;color:rgba(255,255,255,0.8);line-height:1.6;">
|
|
${greeting},
|
|
</p>
|
|
<p style="margin:0 0 24px;font-size:16px;color:rgba(255,255,255,0.8);line-height:1.6;">
|
|
You have been invited to view the BreakPilot ComplAI investor pitch deck.
|
|
Click the button below to access the interactive presentation.
|
|
</p>
|
|
<table cellpadding="0" cellspacing="0" style="margin:0 0 24px;">
|
|
<tr>
|
|
<td style="background:linear-gradient(135deg,#6366f1,#8b5cf6);border-radius:8px;padding:14px 32px;">
|
|
<a href="${magicLinkUrl}" style="color:#ffffff;font-size:16px;font-weight:600;text-decoration:none;display:inline-block;">
|
|
View Pitch Deck
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<p style="margin:0 0 8px;font-size:13px;color:rgba(255,255,255,0.4);line-height:1.5;">
|
|
This link expires in ${process.env.MAGIC_LINK_TTL_HOURS || '72'} hours and can only be used once.
|
|
</p>
|
|
<p style="margin:0;font-size:13px;color:rgba(255,255,255,0.3);line-height:1.5;word-break:break-all;">
|
|
${magicLinkUrl}
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding:20px 40px 40px;border-top:1px solid rgba(255,255,255,0.05);">
|
|
<p style="margin:0;font-size:12px;color:rgba(255,255,255,0.25);line-height:1.5;">
|
|
If you did not expect this email, you can safely ignore it.
|
|
This is an AI-first company — we don't do PDFs.
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</body>
|
|
</html>
|
|
`,
|
|
})
|
|
}
|