From 455301cac2f5617ed3df2360543d986df1272e62 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Thu, 6 Aug 2026 10:44:09 +0200 Subject: [PATCH 1/3] refactor: extract magic-link email payload builder Move the SendGrid mail/send request body into a pure buildMagicLinkPayload util so it can be unit-tested without a SendGrid call or the auth instance's DB pool. --- src/app/utils/magic-link-email.js | 14 ++++++++++++++ src/auth.js | 20 +++++++++----------- test/unit/magic-link-email.test.js | 18 ++++++++++++++++++ 3 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 src/app/utils/magic-link-email.js create mode 100644 test/unit/magic-link-email.test.js diff --git a/src/app/utils/magic-link-email.js b/src/app/utils/magic-link-email.js new file mode 100644 index 0000000..690c69f --- /dev/null +++ b/src/app/utils/magic-link-email.js @@ -0,0 +1,14 @@ +// Build the SendGrid v3 mail/send request body for a magic-link email. +// Tracked separately so the request shape can be unit-tested without a +// SendGrid API call or the auth instance's DB pool. +export const buildMagicLinkPayload = ({ email, url, fromEmail, subject }) => ({ + personalizations: [{ to: [{ email }] }], + from: { email: fromEmail }, + subject, + content: [ + { + type: "text/plain", + value: `Click the link below to sign in to codebar:\n\n${url}\n\nThis link expires in 5 minutes.`, + }, + ], +}); diff --git a/src/auth.js b/src/auth.js index e7d8477..596290b 100644 --- a/src/auth.js +++ b/src/auth.js @@ -4,6 +4,7 @@ import { admin, magicLink, jwt } from "better-auth/plugins"; import { oauthProvider } from "@better-auth/oauth-provider"; import appConfig from "./config.js"; import { devMagicLinks } from "./dev/magic-links.js"; +import { buildMagicLinkPayload } from "./app/utils/magic-link-email.js"; // PostgreSQL connection pool for CI/production and local dev // SSL only for non-local connections (Heroku requires it; local/CI does not) @@ -103,17 +104,14 @@ export const auth = betterAuth({ Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, - body: JSON.stringify({ - personalizations: [{ to: [{ email }] }], - from: { email: fromEmail }, - subject: "Sign in to codebar", - content: [ - { - type: "text/plain", - value: `Click the link below to sign in to codebar:\n\n${url}\n\nThis link expires in 5 minutes.`, - }, - ], - }), + body: JSON.stringify( + buildMagicLinkPayload({ + email, + url, + fromEmail, + subject: "Sign in to codebar", + }), + ), }); if (!res.ok) { diff --git a/test/unit/magic-link-email.test.js b/test/unit/magic-link-email.test.js new file mode 100644 index 0000000..7faaadc --- /dev/null +++ b/test/unit/magic-link-email.test.js @@ -0,0 +1,18 @@ +import { test } from "tap"; +import { buildMagicLinkPayload } from "../../src/app/utils/magic-link-email.js"; + +test("buildMagicLinkPayload embeds the real URL in the plain-text body", async (t) => { + const url = "https://auth.codebar.io/api/auth/magic-link/verify?token=xyz"; + const payload = buildMagicLinkPayload({ + email: "user@codebar.io", + url, + fromEmail: "auth-noreply@codebar.io", + subject: "Sign in to codebar", + }); + + t.equal(payload.personalizations[0].to[0].email, "user@codebar.io"); + t.equal(payload.from.email, "auth-noreply@codebar.io"); + t.equal(payload.subject, "Sign in to codebar"); + t.equal(payload.content[0].type, "text/plain"); + t.ok(payload.content[0].value.includes(url)); +}); From 469629b11b3c7646960b530d9f8f25a3d5693fa4 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Thu, 6 Aug 2026 10:44:32 +0200 Subject: [PATCH 2/3] fix: disable SendGrid link tracking on magic-link emails Pass per-request tracking_settings in the mail/send body so the magic-link URL reads as the real codebar.io link instead of a long /LsClick redirect. --- src/app/utils/magic-link-email.js | 6 ++++++ test/unit/magic-link-email.test.js | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/app/utils/magic-link-email.js b/src/app/utils/magic-link-email.js index 690c69f..79c4c8e 100644 --- a/src/app/utils/magic-link-email.js +++ b/src/app/utils/magic-link-email.js @@ -5,6 +5,12 @@ export const buildMagicLinkPayload = ({ email, url, fromEmail, subject }) => ({ personalizations: [{ to: [{ email }] }], from: { email: fromEmail }, subject, + tracking_settings: { + // Magic-link URL must read as the real codebar.io link, + // not a Sendgrid /LsClick tracking redirect. + click_tracking: { enable: false }, + open_tracking: { enable: false }, + }, content: [ { type: "text/plain", diff --git a/test/unit/magic-link-email.test.js b/test/unit/magic-link-email.test.js index 7faaadc..dd44614 100644 --- a/test/unit/magic-link-email.test.js +++ b/test/unit/magic-link-email.test.js @@ -1,6 +1,18 @@ import { test } from "tap"; import { buildMagicLinkPayload } from "../../src/app/utils/magic-link-email.js"; +test("buildMagicLinkPayload disables SendGrid click tracking", async (t) => { + const payload = buildMagicLinkPayload({ + email: "user@codebar.io", + url: "https://auth.codebar.io/api/auth/magic-link/verify?token=abc", + fromEmail: "auth-noreply@codebar.io", + subject: "Sign in to codebar", + }); + + t.equal(payload.tracking_settings.click_tracking.enable, false); + t.equal(payload.tracking_settings.open_tracking.enable, false); +}); + test("buildMagicLinkPayload embeds the real URL in the plain-text body", async (t) => { const url = "https://auth.codebar.io/api/auth/magic-link/verify?token=xyz"; const payload = buildMagicLinkPayload({ From 22d317396bed9b00129c51d2de0364c06583ea3b Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Thu, 6 Aug 2026 10:55:31 +0200 Subject: [PATCH 3/3] feat: add HTML part to magic-link email Render the sign-in link as a button in an HTML part alongside the existing plain-text part. HTML-escape the href so the token/query string survives attribute embedding. --- src/app/utils/magic-link-email.js | 12 ++++++++++++ test/unit/magic-link-email.test.js | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/app/utils/magic-link-email.js b/src/app/utils/magic-link-email.js index 79c4c8e..79a10ea 100644 --- a/src/app/utils/magic-link-email.js +++ b/src/app/utils/magic-link-email.js @@ -1,3 +1,11 @@ +// Escape a URL for safe embedding in an HTML attribute. +const escapeHtml = (value) => + value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); + // Build the SendGrid v3 mail/send request body for a magic-link email. // Tracked separately so the request shape can be unit-tested without a // SendGrid API call or the auth instance's DB pool. @@ -16,5 +24,9 @@ export const buildMagicLinkPayload = ({ email, url, fromEmail, subject }) => ({ type: "text/plain", value: `Click the link below to sign in to codebar:\n\n${url}\n\nThis link expires in 5 minutes.`, }, + { + type: "text/html", + value: `

Click the button below to sign in to codebar.

Sign in to codebar

This link expires in 5 minutes.

`, + }, ], }); diff --git a/test/unit/magic-link-email.test.js b/test/unit/magic-link-email.test.js index dd44614..b6f209b 100644 --- a/test/unit/magic-link-email.test.js +++ b/test/unit/magic-link-email.test.js @@ -28,3 +28,24 @@ test("buildMagicLinkPayload embeds the real URL in the plain-text body", async ( t.equal(payload.content[0].type, "text/plain"); t.ok(payload.content[0].value.includes(url)); }); + +test("buildMagicLinkPayload adds HTML part with escaped href", async (t) => { + const url = + "https://auth.codebar.io/api/auth/magic-link/verify?token=a+b&x=1"; + const payload = buildMagicLinkPayload({ + email: "user@codebar.io", + url, + fromEmail: "auth-noreply@codebar.io", + subject: "Sign in to codebar", + }); + + const html = payload.content.find((c) => c.type === "text/html"); + t.ok(html, "has an HTML part"); + t.match(html.value, /