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

190 lines
5.6 KiB
TypeScript

import { and, desc, eq, gt } from "drizzle-orm";
import { NextResponse } from "next/server";
import { z } from "zod";
import {
createEmailVerificationCode,
EMAIL_VERIFICATION_PURPOSE_REGISTER,
hashEmailVerificationCode,
resolveResendConfig,
sendVerificationCodeByResend,
} from "@/lib/auth/email-verification";
import { db } from "@/lib/db";
import { emailVerificationCode, siteSettings, user } from "@/lib/db/schema";
import { getAuthSettings } from "@/lib/db/queries";
import { checkRateLimit, getClientIdentifier, rateLimiters } from "@/lib/security/rate-limiter";
const payloadSchema = z.object({
email: z.string().email(),
});
const SEND_CODE_COOLDOWN_SECONDS = 60;
const emailCooldownMap = new Map<string, number>();
function getCooldownSeconds(email: string): number {
const expiredAt = emailCooldownMap.get(email.toLowerCase());
if (!expiredAt) {
return 0;
}
const remaining = Math.ceil((expiredAt - Date.now()) / 1000);
return remaining > 0 ? remaining : 0;
}
function setCooldown(email: string): void {
emailCooldownMap.set(
email.toLowerCase(),
Date.now() + SEND_CODE_COOLDOWN_SECONDS * 1000
);
}
export async function POST(request: Request) {
try {
const rateLimit = checkRateLimit(request, rateLimiters.auth);
if (!rateLimit.allowed) {
return NextResponse.json({ error: "请求过于频繁,请稍后重试" }, { status: 429 });
}
const body = await request.json();
const parsed = payloadSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: "邮箱格式不正确" }, { status: 400 });
}
const email = parsed.data.email.trim().toLowerCase();
const clientId = getClientIdentifier(request);
const localCooldown = getCooldownSeconds(`${clientId}:${email}`);
if (localCooldown > 0) {
return NextResponse.json(
{ error: `验证码发送过于频繁,请 ${localCooldown}s 后重试` },
{ status: 429 }
);
}
const authSettings = await getAuthSettings();
if (authSettings.isRegistrationEnabled === false) {
return NextResponse.json({ error: "当前系统未开放注册" }, { status: 403 });
}
if (authSettings.isEmailVerificationEnabled !== true) {
return NextResponse.json({ error: "邮箱验证未启用" }, { status: 400 });
}
const resendConfig = resolveResendConfig({
apiKey: authSettings.resendApiKey,
fromEmail: authSettings.resendFromEmail,
});
if (!resendConfig) {
return NextResponse.json(
{ error: "邮箱服务未配置,请在认证管理中配置 Resend API Key 和发件邮箱" },
{ status: 500 }
);
}
const [existingUser] = await db
.select({ id: user.id })
.from(user)
.where(eq(user.email, email))
.limit(1);
if (existingUser) {
return NextResponse.json({ error: "该邮箱已注册" }, { status: 400 });
}
const now = new Date();
const [latestCode] = await db
.select({ createdAt: emailVerificationCode.createdAt })
.from(emailVerificationCode)
.where(
and(
eq(emailVerificationCode.email, email),
eq(emailVerificationCode.purpose, EMAIL_VERIFICATION_PURPOSE_REGISTER),
gt(emailVerificationCode.expiresAt, now)
)
)
.orderBy(desc(emailVerificationCode.createdAt))
.limit(1);
if (latestCode?.createdAt) {
const createdAtMs = new Date(latestCode.createdAt).getTime();
const waitSeconds = Math.ceil((createdAtMs + SEND_CODE_COOLDOWN_SECONDS * 1000 - Date.now()) / 1000);
if (waitSeconds > 0) {
return NextResponse.json(
{ error: `验证码发送过于频繁,请 ${waitSeconds}s 后重试` },
{ status: 429 }
);
}
}
const expiresInMinutes = Math.min(
Math.max(Number(authSettings.emailVerificationExpiresMinutes) || 10, 3),
30
);
const [siteSettingRecord] = await db
.select({ siteName: siteSettings.siteName })
.from(siteSettings)
.limit(1);
const code = createEmailVerificationCode();
const codeHash = hashEmailVerificationCode({
email,
purpose: EMAIL_VERIFICATION_PURPOSE_REGISTER,
code,
});
const expiresAt = new Date(Date.now() + expiresInMinutes * 60 * 1000);
await db
.delete(emailVerificationCode)
.where(
and(
eq(emailVerificationCode.email, email),
eq(emailVerificationCode.purpose, EMAIL_VERIFICATION_PURPOSE_REGISTER)
)
);
await db.insert(emailVerificationCode).values({
email,
purpose: EMAIL_VERIFICATION_PURPOSE_REGISTER,
codeHash,
expiresAt,
});
try {
await sendVerificationCodeByResend({
to: email,
code,
siteName: siteSettingRecord?.siteName || "AI Chatbot",
expiresInMinutes,
config: resendConfig,
});
} catch (sendError) {
await db
.delete(emailVerificationCode)
.where(
and(
eq(emailVerificationCode.email, email),
eq(emailVerificationCode.purpose, EMAIL_VERIFICATION_PURPOSE_REGISTER)
)
);
console.error("[Email Verification] Send code failed:", sendError);
return NextResponse.json({ error: "验证码发送失败,请稍后重试" }, { status: 500 });
}
setCooldown(`${clientId}:${email}`);
return NextResponse.json({
success: true,
message: "验证码已发送",
cooldownSeconds: SEND_CODE_COOLDOWN_SECONDS,
expiresInMinutes,
});
} catch (error) {
console.error("[Email Verification] Send code error:", error);
return NextResponse.json({ error: "发送验证码失败" }, { status: 500 });
}
}