feat: add auth email adapter
This commit is contained in:
80
src/email.ts
Normal file
80
src/email.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { Env } from "./env";
|
||||
|
||||
export type AuthEmailKind = "verify-email" | "reset-password" | "email-otp" | "magic-link";
|
||||
|
||||
export interface AuthEmailInput {
|
||||
kind: AuthEmailKind;
|
||||
to: string;
|
||||
url?: string;
|
||||
otp?: string;
|
||||
}
|
||||
|
||||
export interface EmailMessage {
|
||||
to: string;
|
||||
subject: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export function buildAuthEmail(input: AuthEmailInput): EmailMessage {
|
||||
if (input.kind === "email-otp") {
|
||||
return {
|
||||
to: input.to,
|
||||
subject: "Your sign-in code",
|
||||
text: `Your sign-in code is ${input.otp ?? ""}.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (input.kind === "reset-password") {
|
||||
return {
|
||||
to: input.to,
|
||||
subject: "Reset your password",
|
||||
text: `Use this link to reset your password: ${input.url ?? ""}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (input.kind === "magic-link") {
|
||||
return {
|
||||
to: input.to,
|
||||
subject: "Sign in to your account",
|
||||
text: `Use this link to sign in: ${input.url ?? ""}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
to: input.to,
|
||||
subject: "Verify your email",
|
||||
text: `Use this link to verify your email: ${input.url ?? ""}`,
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendEmail(env: Env, message: EmailMessage): Promise<void> {
|
||||
if (!env.MAIL_PROVIDER) {
|
||||
throw new Error("MAIL_PROVIDER is not configured");
|
||||
}
|
||||
|
||||
if (env.MAIL_PROVIDER !== "resend") {
|
||||
throw new Error(`Unsupported MAIL_PROVIDER: ${env.MAIL_PROVIDER}`);
|
||||
}
|
||||
|
||||
if (!env.RESEND_API_KEY || !env.MAIL_FROM) {
|
||||
throw new Error("RESEND_API_KEY and MAIL_FROM are required for resend");
|
||||
}
|
||||
|
||||
const response = await fetch("https://api.resend.com/emails", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.RESEND_API_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: env.MAIL_FROM,
|
||||
to: message.to,
|
||||
subject: message.subject,
|
||||
text: message.text,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Email provider failed: ${response.status}`);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
trustedOrigins,
|
||||
type Env,
|
||||
} from "../src/env";
|
||||
import { buildAuthEmail, sendEmail } from "../src/email";
|
||||
|
||||
const env: Env = {
|
||||
BETTER_AUTH_URL: "http://localhost:8788",
|
||||
@@ -40,3 +41,32 @@ describe("auth env helpers", () => {
|
||||
expect(optionalEnv({}, "MAIL_FROM")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("auth email adapter", () => {
|
||||
it("builds a verification email", () => {
|
||||
const message = buildAuthEmail({
|
||||
kind: "verify-email",
|
||||
to: "user@example.com",
|
||||
url: "https://auth.example.com/verify",
|
||||
});
|
||||
expect(message.subject).toBe("Verify your email");
|
||||
expect(message.to).toBe("user@example.com");
|
||||
expect(message.text).toContain("https://auth.example.com/verify");
|
||||
});
|
||||
|
||||
it("fails clearly when no mail provider is configured", async () => {
|
||||
await expect(
|
||||
sendEmail(
|
||||
{
|
||||
BETTER_AUTH_URL: "http://localhost:8788",
|
||||
TRUSTED_ORIGINS: "http://localhost:8787",
|
||||
},
|
||||
{
|
||||
to: "user@example.com",
|
||||
subject: "Test",
|
||||
text: "Test",
|
||||
},
|
||||
),
|
||||
).rejects.toThrow("MAIL_PROVIDER is not configured");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user