mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
Email body now contains the 6-digit code only. The /verify/[token] route and the EmailProvider link callback are intentionally left wired (just not advertised in the email) so reverting to a link-bearing template is a content-only change if the all-code variant doesn't improve deliverability for the Atkins-style blocked recipients. Hypothesis being tested: corporate gateway URL scanners are part of why some emails got silently quarantined, and a short transactional body without an auth-token URL clears more filters. Two small UX bugs surfaced in preview testing also fixed here: - Paste of "482 911" (with the visual space from the email's formatted code) was dropping a digit. Root cause: maxLength=6 on the input truncated the 7-char paste before our \D-stripping ran. Drop the attribute and let the existing .slice(0, 6) after stripping handle the bound. Pasting the formatted code now works. - After requesting a resend and then typing into the code field, the green "we've sent a new code" notice would re-appear as soon as the previous error message cleared. handleChange now clears the resend notice on the next keystroke, alongside the error clear it already did. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
132 lines
4.2 KiB
TypeScript
132 lines
4.2 KiB
TypeScript
// Email template for user sign-in. Body contains a 6-digit code only — the
|
||
// magic-link path is still wired (see /verify/[token] and the EmailProvider
|
||
// callback) but the URL is intentionally omitted from the email to reduce
|
||
// the content-scanner surface area for corporate email gateways.
|
||
|
||
import { createTransport } from "nodemailer";
|
||
import { buildMailHeaders } from "./buildMailHeaders";
|
||
|
||
export async function MagicLinksEmail({
|
||
identifier,
|
||
url,
|
||
provider,
|
||
code,
|
||
}: {
|
||
identifier: string;
|
||
url: string;
|
||
provider: { server: any; from: string };
|
||
code: string;
|
||
}): Promise<{ messageId: string }> {
|
||
const parsed = new URL(url);
|
||
const host = parsed.host;
|
||
const logoUrl = `${parsed.origin}/domna-email-logo.png`;
|
||
|
||
const transport = createTransport(provider.server);
|
||
|
||
const brandColor = "#14163d";
|
||
const accentColor = "#2d348f";
|
||
const background = "#F9F9F9";
|
||
|
||
const result = await transport.sendMail({
|
||
to: identifier,
|
||
from: provider.from,
|
||
subject: "Sign in to Ara",
|
||
text: plainText({ code, host }),
|
||
html: domnaHtml({
|
||
code,
|
||
logoUrl,
|
||
host,
|
||
brandColor,
|
||
accentColor,
|
||
background,
|
||
}),
|
||
headers: buildMailHeaders({
|
||
fromAddress: provider.from,
|
||
sesConfigurationSet: process.env.SES_CONFIGURATION_SET,
|
||
}),
|
||
});
|
||
|
||
const failed = result.rejected.filter(Boolean);
|
||
if (failed.length) {
|
||
throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`);
|
||
}
|
||
|
||
return { messageId: result.messageId };
|
||
}
|
||
|
||
function formatCodeForDisplay(code: string): string {
|
||
// Insert a non-breaking space for readability without breaking copy-paste
|
||
// semantics in mail clients that strip the visual grouping.
|
||
return `${code.slice(0, 3)} ${code.slice(3)}`;
|
||
}
|
||
|
||
function domnaHtml({
|
||
code,
|
||
logoUrl,
|
||
host,
|
||
brandColor,
|
||
accentColor,
|
||
background,
|
||
}: {
|
||
code: string;
|
||
logoUrl: string;
|
||
host: string;
|
||
brandColor: string;
|
||
accentColor: string;
|
||
background: string;
|
||
}) {
|
||
const escapedHost = host.replace(/\./g, "​.");
|
||
const codeDisplay = formatCodeForDisplay(code);
|
||
|
||
return `
|
||
<body style="background: ${background}; font-family: Helvetica, Arial, sans-serif; margin: 0; padding: 0;">
|
||
<table width="100%" border="0" cellspacing="0" cellpadding="0"
|
||
style="max-width: 600px; margin: 40px auto; background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.05);">
|
||
<tr>
|
||
<td align="center" style="background: linear-gradient(90deg, ${brandColor}, ${accentColor}); padding: 12px 8px;">
|
||
<img
|
||
src="${logoUrl}"
|
||
alt="Domna Logo"
|
||
width="120"
|
||
height="auto"
|
||
style="margin-bottom: 4px;"
|
||
/>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td align="center" style="padding: 28px 24px 28px; color: #333;">
|
||
<h2 style="color: ${brandColor}; font-size: 22px; margin: 0 0 8px;">Your sign-in code</h2>
|
||
<p style="font-size: 14px; line-height: 1.5; color: #666; margin: 0 0 20px;">
|
||
Enter this code at <span style="color: ${accentColor};">${escapedHost}</span> to sign in to Ara.
|
||
</p>
|
||
<div style="font-family: 'Courier New', Courier, monospace; font-size: 36px; font-weight: 700; letter-spacing: 4px; color: ${brandColor}; padding: 18px 24px; background: #f3f4f8; border-radius: 8px; display: inline-block;">
|
||
${codeDisplay}
|
||
</div>
|
||
<p style="font-size: 12px; color: #888; margin: 12px 0 0;">
|
||
This code expires in 10 minutes.
|
||
</p>
|
||
<p style="margin-top: 28px; font-size: 13px; color: #777;">
|
||
If you didn’t request this email, you can safely ignore it.
|
||
</p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td align="center" style="padding: 20px; font-size: 12px; color: #999; border-top: 1px solid #eee;">
|
||
© ${new Date().getFullYear()} Domna Homes • <span style="color: ${accentColor};">${escapedHost}</span>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</body>
|
||
`;
|
||
}
|
||
|
||
function plainText({ code, host }: { code: string; host: string }) {
|
||
return `Sign in to Ara by Domna
|
||
|
||
Your sign-in code: ${code}
|
||
|
||
Enter this code at ${host}/auth/verify-code to sign in.
|
||
|
||
This code expires in 10 minutes. If you did not request this email, you can safely ignore it.
|
||
`;
|
||
}
|