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
10 changes: 7 additions & 3 deletions src/app/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { GitHubButton, MagicLinkButton } from "../components/login.js";
import { getAuthFromContext } from "../utils/auth.js";
import { logout } from "../handlers/logout.js";
import { getCallbackURL } from "../utils/callback-url.js";
import { friendlyError } from "../utils/friendly-error.js";
import appConfig from "../../config.js";

function showLogin(c) {
const error = c.req.query("error");
const success = c.req.query("success");
const callbackURL = getCallbackURL(c);
const friendly = error ? friendlyError(decodeURIComponent(error)) : null;

return c.html(
Layout({
Expand All @@ -22,7 +24,7 @@ function showLogin(c) {
<h1 class="h3 mb-3 fw-semibold">Sign In</h1>
${Navigation({ back: { href: "/", text: "Back to Home" } })}
<hr class="my-3" />
${Message({ error, success })}
${Message({ error: friendly, success })}
<div class="row g-3">
<div class="col-sm-6">${GitHubButton({ callbackURL })}</div>
<div class="col-sm-6">${MagicLinkButton({ callbackURL })}</div>
Expand All @@ -36,6 +38,8 @@ function showLogin(c) {

function showMagicLinkForm(c) {
const callbackURL = c.req.query("callbackURL") || getCallbackURL(c);
const error = c.req.query("error");
const friendly = error ? friendlyError(decodeURIComponent(error)) : null;

return c.html(
Layout({
Expand All @@ -47,7 +51,7 @@ function showMagicLinkForm(c) {
${Navigation({ back: { href: "/login", text: "Back to Login" } })}
<hr class="my-3" />
${Message({
error: c.req.query("error"),
error: friendly,
success: c.req.query("success"),
})}
<form method="post" action="/login/magic-link">
Expand Down Expand Up @@ -83,7 +87,7 @@ async function sendMagicLink(c) {
body: {
email,
callbackURL,
errorCallbackURL: `${appConfig.base_url}/login?error=${encodeURIComponent("The magic link has expired or already been used")}`,
errorCallbackURL: `${appConfig.base_url}/login`,
},
headers: c.req.raw.headers,
});
Expand Down
13 changes: 13 additions & 0 deletions src/app/utils/friendly-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Better Auth redirects magic-link errors by setting the `error` query param
// to a short code (e.g. INVALID_TOKEN), overwriting whatever message we set
// in errorCallbackURL. Map codes to human-friendly text at render time.
const FRIENDLY = {
INVALID_TOKEN:
"This sign-in link has expired or already been used. Please request a new one.",
new_user_signup_disabled:
"New sign-ups are currently disabled. Please contact support.",
failed_to_create_user: "We couldn't create your account. Please try again.",
failed_to_create_session: "We couldn't start a session. Please try again.",
};

export const friendlyError = (code) => FRIENDLY[code] ?? code;
29 changes: 9 additions & 20 deletions test/features/magic-links.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,30 +63,19 @@ test("magic links feature tests", async (t) => {
t.match(html, /email/i, "has email input");
});

t.test("magic link URL is captured in test", async (t) => {
t.test("login page shows friendly message for INVALID_TOKEN", async (t) => {
const testInstance = await getTestInstance(t);
const app = createApp(testInstance.auth);
const { getMagicLinks } = testInstance;

const formData = new URLSearchParams();
formData.append("email", "magic-url@example.com");
const res = await app.request("/login?error=INVALID_TOKEN");

await app.request("/login/magic-link", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: formData.toString(),
});

const magicLinks = getMagicLinks();
t.ok(magicLinks.length > 0, "magic link was generated");
t.equal(
magicLinks[magicLinks.length - 1].email,
"magic-url@example.com",
"magic link for correct email",
t.equal(res.status, 200, "login page loads");
const html = await res.text();
t.match(
html,
/expired or already been used/,
"shows friendly message instead of raw code",
);
t.ok(magicLinks[magicLinks.length - 1].url, "magic link has URL");
t.ok(magicLinks[magicLinks.length - 1].token, "magic link has token");
t.ok(!html.includes("INVALID_TOKEN"), "does not show raw error code");
});
});
23 changes: 23 additions & 0 deletions test/unit/friendly-error.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { test } from "tap";
import { friendlyError } from "../../src/app/utils/friendly-error.js";

test("friendlyError maps known Better Auth codes", async (t) => {
t.match(
friendlyError("INVALID_TOKEN"),
/expired or already been used/,
"INVALID_TOKEN gets friendly text",
);
t.equal(
friendlyError("new_user_signup_disabled"),
"New sign-ups are currently disabled. Please contact support.",
);
t.equal(
friendlyError("failed_to_create_session"),
"We couldn't start a session. Please try again.",
);
});

test("friendlyError passes through unknown codes", async (t) => {
t.equal(friendlyError("SOME_OTHER_CODE"), "SOME_OTHER_CODE");
t.equal(friendlyError(""), "");
});