91 lines
2.8 KiB
TypeScript
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}`);
|
|
}
|
|
}
|