ai-chatbot/lib/auth/email-verification.ts
Your Name aa1ec8bd43
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add email verification with resend auth settings
2026-02-23 12:31:41 +08:00

91 lines
2.8 KiB
TypeScript

import crypto from "node:crypto";
export const EMAIL_VERIFICATION_PURPOSE_REGISTER = "register";
export type ResendConfig = {
apiKey: string;
fromEmail: string;
};
function getVerificationSecret(): string {
return (
process.env.EMAIL_VERIFICATION_SECRET ||
process.env.AUTH_SECRET ||
"email-verification-secret"
);
}
export function hashEmailVerificationCode(params: {
email: string;
purpose: string;
code: string;
}): string {
const payload = `${params.email.toLowerCase()}:${params.purpose}:${params.code}:${getVerificationSecret()}`;
return crypto.createHash("sha256").update(payload).digest("hex");
}
export function createEmailVerificationCode(): string {
return String(crypto.randomInt(0, 1000000)).padStart(6, "0");
}
export function isResendConfigured(): boolean {
return Boolean(process.env.RESEND_API_KEY && process.env.RESEND_FROM_EMAIL);
}
export function resolveResendConfig(config?: {
apiKey?: string | null;
fromEmail?: string | null;
}): ResendConfig | null {
const apiKey = String(config?.apiKey || process.env.RESEND_API_KEY || "").trim();
const fromEmail = String(config?.fromEmail || process.env.RESEND_FROM_EMAIL || "").trim();
if (!apiKey || !fromEmail) {
return null;
}
return { apiKey, fromEmail };
}
export async function sendVerificationCodeByResend(params: {
to: string;
code: string;
siteName: string;
expiresInMinutes: number;
config?: ResendConfig;
}): Promise<void> {
const resolvedConfig = params.config || resolveResendConfig();
if (!resolvedConfig) {
throw new Error("Resend is not configured");
}
const subject = `【${params.siteName}】邮箱验证码`;
const html = `
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px; color: #111;">
<h2 style="margin: 0 0 12px; font-size: 18px;">邮箱验证码</h2>
<p style="margin: 0 0 12px; font-size: 14px;">你正在注册 ${params.siteName},验证码如下:</p>
<div style="font-size: 28px; font-weight: 700; letter-spacing: 6px; margin: 10px 0 16px;">${params.code}</div>
<p style="margin: 0; font-size: 13px; color: #666;">验证码 ${params.expiresInMinutes} 分钟内有效,请勿泄露给他人。</p>
</div>
`;
const text = `你正在注册 ${params.siteName},验证码:${params.code}。验证码 ${params.expiresInMinutes} 分钟内有效。`;
const response = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${resolvedConfig.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: resolvedConfig.fromEmail,
to: params.to,
subject,
html,
text,
}),
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`Resend request failed: ${response.status} ${detail}`);
}
}