Commit Graph

2 Commits

Author SHA1 Message Date
b2588ecdd8 fix(audit-wave-1): route all email-template URLs through safeUrl
Closes Wave 1.4 (CRITICAL). Three templates still inlined URLs
directly into `href` without the existing safeUrl() helper:

- inquiry-client-confirmation: `mailto:${contactEmail}` href —
  user-supplied email straight to an HTML attribute.
- inquiry-sales-notification: `${crmUrl}` from inquiry form input.
- residential-inquiry: same `mailto:${contactEmail}` pattern.

Each call now passes through `safeUrl()` from `@/lib/email/shell`,
which (a) scheme-allow-lists to http(s)/mailto/tel/root-relative and
(b) HTML-attribute-escapes the result. A stray `"` in any URL would
have escaped the attribute; a `javascript:` scheme would have
triggered XSS in webmail clients that run scripts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:08:51 +02:00
d8f1c0c34e feat(email): port remaining 7 templates to react-email
Phase 2 (single commit) — applies the portal-auth.tsx pattern to every
hand-strung transactional email template. JSX components rendered via
@react-email/components' render() replace inline-style string templates
+ hand-rolled escapeHtml().

Ported (.ts → .tsx, public function signatures become async):
  crm-invite.tsx                — admin/super-admin CRM invite
  admin-email-change.tsx        — sign-in email changed notification
  inquiry-client-confirmation.tsx — public berth inquiry receipt
  inquiry-sales-notification.tsx  — internal sales alert for inquiries
  residential-inquiry.tsx       — pair: client confirmation + sales alert
  notification-digest.tsx       — daily/hourly unread-notification digest
  document-signing.tsx          — triplet: invitation + completed + reminder

Each template now defines its body as a typed React component, drops
escapeHtml() entirely (react-email auto-escapes string interpolation
in JSX text + attributes), and passes the rendered HTML to the existing
renderShell() for shell wrapping. The shell + branding flow is unchanged.

Caller migration (all sync → async):
  src/app/api/public/residential-inquiries/route.ts
  src/lib/queue/workers/email.ts
  src/lib/services/notification-digest.service.ts
  src/lib/services/users.service.ts
  src/lib/services/document-signing-emails.service.ts
  src/lib/services/crm-invite.service.ts

All call sites already lived inside async functions; only the await was
needed. No public API shape changes other than return type (now Promise).

The pattern now applies uniformly across all 8 email templates (portal-
auth.tsx + the 7 in this commit). Email template directory is fully
react-email-based.

1298/1298 vitest green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:19:52 +02:00