magic link email tempalte live

This commit is contained in:
Khalim Conn-Kowlessar 2025-10-17 13:55:46 +00:00
parent a264686552
commit 73efe32801
5 changed files with 988 additions and 258 deletions

1145
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -52,7 +52,6 @@
"next-auth": "^4.22.1",
"next-axiom": "^1.9.2",
"next-themes": "^0.3.0",
"nodemailer": "^6.10.1",
"pg": "^8.11.1",
"postcss": "^8.5.6",
"react": "18.3.1",
@ -70,6 +69,7 @@
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@testing-library/cypress": "^10.0.3",
"@types/nodemailer": "^7.0.2",
"@types/pg": "^8.10.2",
"cypress": "^14.5.3",
"cypress-social-logins": "^1.14.1",

View file

@ -82,6 +82,7 @@ export const AuthOptions: NextAuthOptions = {
},
from: EMAIL_FROM,
maxAge: 60 * 60, // magic link valid for 1 hour
sendVerificationRequest: MagicLinksEmail,
}),
],

View file

@ -2,6 +2,97 @@
// click a verification email to sign in to the app, should they choose to sign in with magic
// links
export async function MagicLinksEmail() {
return null;
import { createTransport } from "nodemailer";
export async function MagicLinksEmail({
identifier,
url,
provider,
}: {
identifier: string;
url: string;
provider: { server: any; from: string };
}) {
const { host } = new URL(url);
const transport = createTransport(provider.server);
const brandColor = "#14163d"; // brand blue
const accentColor = "#2d348f"; // deep blue
const brown = "#c4a47c"; // brand brown
const background = "#F9F9F9";
const result = await transport.sendMail({
to: identifier,
from: provider.from,
subject: "Your secure Domna IQ sign-in link",
text: plainText({ url, host }),
html: domnaHtml({ url, host, brandColor, accentColor, brown, background }),
});
const failed = result.rejected.filter(Boolean);
if (failed.length) {
throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`);
}
}
function domnaHtml({
url,
host,
brandColor,
accentColor,
brown,
background,
}: {
url: string;
host: string;
brandColor: string;
accentColor: string;
brown: string;
background: string;
}) {
const escapedHost = host.replace(/\./g, "​.");
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="https://145275138.fs1.hubspotusercontent-eu1.net/hubfs/145275138/base_logo_transparent_background.png"
alt="Domna Logo"
width="120"
height="auto"
style="margin-bottom: 4px;"
/>
</td>
</tr>
<tr>
<td align="center" style="padding: 10px 10px 10px; color: #333;">
<h2 style="color: ${brandColor}; font-size: 22px; margin-bottom: 16px;">Welcome back to Domna IQ</h2>
<p style="font-size: 16px; line-height: 1.6; color: #444; margin-bottom: 32px;">
Click below to securely sign in to your account and continue your retrofit journey.
</p>
<a href="${url}" target="_blank"
style="display: inline-block; padding: 14px 28px; background: ${brown}; color: #fff; text-decoration: none; border-radius: 6px; font-weight: 600; font-size: 16px;">
Sign in to Domna IQ
</a>
<p style="margin-top: 36px; font-size: 13px; color: #777;">
If you didnt 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;">
&copy; ${new Date().getFullYear()} Domna Homes <span style="color: ${accentColor};">${escapedHost}</span>
</td>
</tr>
</table>
</body>
`;
}
function plainText({ url, host }: { url: string; host: string }) {
return `Sign in to Domna IQ\n${url}\n\nIf you did not request this email, you can safely ignore it.\n`;
}

View file

@ -6,6 +6,9 @@ export async function middleware(req: NextRequest) {
const token = await getToken({ req });
const { pathname } = req.nextUrl;
console.log("token", token);
console.log("onboarded", token?.onboarded);
// If no session, send user to sign-in page
if (!token) {
return NextResponse.redirect(new URL("/", req.url));