Tier 1.6: S3Backend.put now sets ServerSideEncryption=AES256 — closes the cleartext-at-rest gap for signed contracts, GDPR exports, pg_dumps. Tier 3.7: New safeUrl() helper in lib/email/shell.ts. Scheme allow-list (http/https/mailto/tel/relative only — javascript:/data:/vbscript:/file: rewritten to about:blank) + HTML-attribute escape. Retrofitted across all 7 transactional templates (crm-invite, portal-auth, document-signing, notification-digest, residential-inquiry, admin-email-change). Tier 4.2: /api/v1/alerts GET now gated on admin.view_audit_log. Tier 4.3: Documenso webhook handler emits captureErrorEvent on catch. Admin/errors no longer silent on webhook crashes. Tier 4.6: Inquiry-funnel email dedup is now case-insensitive (LOWER(value)) and stores normalized email on insert. Capital-letter resubmissions no longer spawn duplicate client+yacht+interest rows. Tier 5.6 + data-model H1: migration 0056 adds FK user_permission_overrides.user_id → user(id) cascade, same for user_port_roles.userId, plus partial unique index on user_email_changes pending rows. Tier 7.6: @types/node bumped from ^25 to ^20.19.0 — matches the runtime. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
import { brandingPrimaryColor, renderShell, safeUrl, type BrandingShell } from '@/lib/email/shell';
|
|
|
|
interface InviteData {
|
|
link: string;
|
|
ttlHours: number;
|
|
recipientName?: string;
|
|
isSuperAdmin: boolean;
|
|
/** Display name for the port — falls back to "Port Nimara" so the
|
|
* pre-multi-tenant default still reads correctly. */
|
|
portName?: string;
|
|
}
|
|
|
|
interface RenderOpts {
|
|
branding?: BrandingShell | null;
|
|
}
|
|
|
|
export function crmInviteEmail(
|
|
data: InviteData,
|
|
overrides?: RenderOpts,
|
|
): {
|
|
subject: string;
|
|
html: string;
|
|
text: string;
|
|
} {
|
|
const portName = data.portName ?? 'Port Nimara';
|
|
const subject = `You're invited to the ${portName} CRM`;
|
|
const greeting = data.recipientName ? `Dear ${escapeHtml(data.recipientName)},` : 'Welcome,';
|
|
const role = data.isSuperAdmin ? 'super administrator' : 'administrator';
|
|
const accent = brandingPrimaryColor(overrides?.branding);
|
|
|
|
const body = `
|
|
<p style="margin-bottom:10px; font-size:18px; font-weight:bold; color:${accent};">
|
|
Welcome to the ${escapeHtml(portName)} CRM
|
|
</p>
|
|
<p style="margin-bottom:10px; font-size:16px; line-height:1.5;">${greeting}</p>
|
|
<p style="margin-bottom:20px; font-size:16px; line-height:1.5;">
|
|
You've been invited to the ${escapeHtml(portName)} CRM as a ${role}. Click the
|
|
button below to set your password and activate your account. The
|
|
link expires in ${data.ttlHours} hours.
|
|
</p>
|
|
<p style="text-align:center; margin:30px 0;">
|
|
<a href="${safeUrl(data.link)}" style="display:inline-block; background-color:${accent}; color:#ffffff; text-decoration:none; padding:14px 35px; border-radius:5px; font-weight:bold; font-size:16px;">
|
|
Set up your account
|
|
</a>
|
|
</p>
|
|
<p style="font-size:14px; color:#666; line-height:1.5; padding:15px 0; border-top:1px solid #eee; margin-top:20px;">
|
|
If the button doesn't work, paste this link into your browser:<br />
|
|
<a href="${safeUrl(data.link)}" style="color:${accent}; text-decoration:underline; word-break:break-all;">${data.link}</a>
|
|
</p>
|
|
<p style="font-size:16px; margin-top:30px;">
|
|
Thank you,<br />
|
|
<strong>${escapeHtml(portName)} CRM</strong>
|
|
</p>`;
|
|
|
|
const text = [
|
|
`Welcome to the ${portName} CRM`,
|
|
'',
|
|
`You've been invited as a ${role}.`,
|
|
`Set up your account: ${data.link}`,
|
|
'',
|
|
`The link expires in ${data.ttlHours} hours.`,
|
|
'',
|
|
`Thank you,`,
|
|
`${portName} CRM`,
|
|
].join('\n');
|
|
|
|
return {
|
|
subject,
|
|
html: renderShell({ title: subject, body, branding: overrides?.branding }),
|
|
text,
|
|
};
|
|
}
|
|
|
|
function escapeHtml(str: string): string {
|
|
return str
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|