Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/app/utils/magic-link-email.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Escape a URL for safe embedding in an HTML attribute.
const escapeHtml = (value) =>
value
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");

// 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,
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",
value: `Click the link below to sign in to codebar:\n\n${url}\n\nThis link expires in 5 minutes.`,
},
{
type: "text/html",
value: `<p>Click the button below to sign in to codebar.</p><p><a href="${escapeHtml(url)}" style="display:inline-block;padding:10px 18px;background-color:#2e5fa3;color:#ffffff;border-radius:4px;text-decoration:none;">Sign in to codebar</a></p><p>This link expires in 5 minutes.</p>`,
},
],
});
20 changes: 9 additions & 11 deletions src/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
51 changes: 51 additions & 0 deletions test/unit/magic-link-email.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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({
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));
});

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, /<a href="/);
t.match(html.value, /Sign in to codebar<\/a>/);

// HTML-escaped href: & and + must be encoded so the link parses intact
const escaped =
"https://auth.codebar.io/api/auth/magic-link/verify?token=a+b&amp;x=1";
t.ok(html.value.includes(escaped));
});