190 lines
5.6 KiB
TypeScript
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 });
|
|
}
|
|
}
|