Merge pull request #13 from Dvorinka/fix/selfhosted-email-optional
CI / Backend Tests (push) Canceled after 0s
CI / Frontend Tests (push) Canceled after 0s
CI / Build All Services (push) Canceled after 0s
CI / Docker Build (auth) (push) Canceled after 0s
CI / Docker Build (backend) (push) Canceled after 0s
CI / Docker Build (frontend) (push) Canceled after 0s
CI / Migration Up/Down (push) Canceled after 0s
CI / Compose E2E Smoke (push) Canceled after 0s
CI / Lint (push) Canceled after 0s
CI / Publish edge images (auth) (push) Canceled after 0s
CI / Publish edge images (backend) (push) Canceled after 0s
CI / Publish edge images (frontend) (push) Canceled after 0s

fix: self-hosted auth without mail transport, password-manager-friendly login
This commit is contained in:
Tomáš Dvořák authored and GitHub committed 2026-09-22 19:50:30 +02:00
commit 61ede5ee99
5 files changed
+23 -5

No files matched your search

+6
View File
@@ -94,12 +94,18 @@ export const auth = betterAuth({
sendOnSignUp: true, sendOnSignUp: true,
autoSignInAfterVerification: true, autoSignInAfterVerification: true,
async sendVerificationEmail({ user, url }) { async sendVerificationEmail({ user, url }) {
// Verification is optional — a misconfigured transport must not fail
// sign-up or sign-in.
try {
await sendTransactionalEmail({ await sendTransactionalEmail({
to: user.email, to: user.email,
subject: "Verify your Primora email", subject: "Verify your Primora email",
text: `Verify your Primora email: ${url}`, text: `Verify your Primora email: ${url}`,
html: `<p>Verify your Primora email.</p><p><a href="${url}">Verify email</a></p>`, html: `<p>Verify your Primora email.</p><p><a href="${url}">Verify email</a></p>`,
}); });
} catch (error) {
console.warn(JSON.stringify({ level: "warn", msg: "verification_email_failed", error }));
}
}, },
}, },
// Enabled unconditionally — better-auth only turns this on when NODE_ENV is // Enabled unconditionally — better-auth only turns this on when NODE_ENV is
+6 -1
View File
@@ -51,7 +51,12 @@ export async function sendTransactionalEmail(input: {
} }
if (!("smtp" in cfg) || !cfg.smtp) { if (!("smtp" in cfg) || !cfg.smtp) {
throw new Error("No mail transport configured for auth service."); // Self-hosted instances may run without mail — auth still works, the
// email simply never goes out.
console.warn(
JSON.stringify({ level: "warn", msg: "mail_skipped_no_transport", to: input.to, subject: input.subject }),
);
return;
} }
await nodemailer.createTransport(cfg.smtp).sendMail({ await nodemailer.createTransport(cfg.smtp).sendMail({
+1 -1
View File
@@ -736,7 +736,7 @@ export default function App() {
try { try {
if (mode() === "sign-up") { if (mode() === "sign-up") {
await authClient.signUp.email({ email: email(), password: password(), name: name() }); await authClient.signUp.email({ email: email(), password: password(), name: name() });
setAuthMessage("Account created. Check Mailpit (or your SMTP provider) for the verification email."); setAuthMessage("Account created — signing you in.");
} else { } else {
await authClient.signIn.email({ email: email(), password: password() }); await authClient.signIn.email({ email: email(), password: password() });
} }
+3 -2
View File
@@ -13,16 +13,17 @@ const sizeClasses = {
}; };
export function Input(props: InputProps) { export function Input(props: InputProps) {
const [local, rest] = splitProps(props, ["label", "error", "size", "class"]); const [local, rest] = splitProps(props, ["label", "error", "size", "class", "id"]);
const size = () => local.size ?? "md"; const size = () => local.size ?? "md";
return ( return (
<div class="w-full"> <div class="w-full">
<Show when={local.label}> <Show when={local.label}>
<label class="label">{local.label}</label> <label class="label" for={local.id}>{local.label}</label>
</Show> </Show>
<input <input
id={local.id}
class={`input ${sizeClasses[size()]} ${local.error ? "border-error focus:border-error focus:ring-error/20" : ""} ${local.class ?? ""}`} class={`input ${sizeClasses[size()]} ${local.error ? "border-error focus:border-error focus:ring-error/20" : ""} ${local.class ?? ""}`}
{...rest} {...rest}
/> />
+7 -1
View File
@@ -124,6 +124,8 @@ export function LoginPage(props: LoginPageProps) {
<Show when={!isSignIn()}> <Show when={!isSignIn()}>
<Input <Input
label="Name" label="Name"
id="name"
name="name"
type="text" type="text"
placeholder="Ada Lovelace" placeholder="Ada Lovelace"
value={props.name} value={props.name}
@@ -134,15 +136,19 @@ export function LoginPage(props: LoginPageProps) {
</Show> </Show>
<Input <Input
label="Email" label="Email"
id="email"
name="email"
type="email" type="email"
placeholder="[email protected]" placeholder="[email protected]"
value={props.email} value={props.email}
onInput={(e) => props.onEmailChange(e.currentTarget.value)} onInput={(e) => props.onEmailChange(e.currentTarget.value)}
autocomplete="email" autocomplete="username"
required required
/> />
<Input <Input
label="Password" label="Password"
id="password"
name="password"
type="password" type="password"
placeholder="••••••••" placeholder="••••••••"
value={props.password} value={props.password}