mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
"use server";
|
|
|
|
import bcrypt from "bcryptjs";
|
|
import { headers } from "next/headers";
|
|
import { z } from "zod";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { rateLimit } from "@/lib/rate-limit";
|
|
import { getAppConfig } from "@/services/app-config";
|
|
import { isSmtpConfigured, normalizeEmailAddress, sendPasswordResetEmail, sendRegistrationVerificationEmail, consumePasswordResetToken, verifyEmailToken } from "@/services/email";
|
|
|
|
const emailSchema = z.object({
|
|
email: z.string().trim().email("请输入正确的邮箱"),
|
|
});
|
|
|
|
const resetPasswordSchema = z.object({
|
|
token: z.string().trim().min(20, "重设链接无效"),
|
|
password: z.string().min(6, "新密码至少 6 位"),
|
|
confirmPassword: z.string().min(6, "确认密码至少 6 位"),
|
|
});
|
|
|
|
async function requestContext() {
|
|
const headerList = await headers();
|
|
return { headers: headerList };
|
|
}
|
|
|
|
async function assertMailAvailable() {
|
|
const config = await getAppConfig();
|
|
if (!isSmtpConfigured(config)) {
|
|
throw new Error("站点尚未启用邮件服务,请联系管理员");
|
|
}
|
|
}
|
|
|
|
export async function requestPasswordReset(formData: FormData) {
|
|
const parsed = emailSchema.parse(Object.fromEntries(formData));
|
|
const email = normalizeEmailAddress(parsed.email);
|
|
const { success } = await rateLimit(`ratelimit:password-reset:${email}`, 3, 10 * 60);
|
|
if (!success) {
|
|
throw new Error("请求过于频繁,请稍后再试");
|
|
}
|
|
|
|
await assertMailAvailable();
|
|
const user = await prisma.user.findUnique({
|
|
where: { email },
|
|
select: { id: true, email: true, status: true },
|
|
});
|
|
|
|
if (user?.status === "ACTIVE") {
|
|
await sendPasswordResetEmail({
|
|
userId: user.id,
|
|
email: user.email,
|
|
...(await requestContext()),
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function requestRegistrationVerification(formData: FormData) {
|
|
const parsed = emailSchema.parse(Object.fromEntries(formData));
|
|
const email = normalizeEmailAddress(parsed.email);
|
|
const { success } = await rateLimit(`ratelimit:email-verify:${email}`, 3, 10 * 60);
|
|
if (!success) {
|
|
throw new Error("请求过于频繁,请稍后再试");
|
|
}
|
|
|
|
await assertMailAvailable();
|
|
const user = await prisma.user.findUnique({
|
|
where: { email },
|
|
select: { id: true, email: true, status: true, emailVerifiedAt: true },
|
|
});
|
|
|
|
if (user?.status === "ACTIVE" && !user.emailVerifiedAt) {
|
|
await sendRegistrationVerificationEmail({
|
|
userId: user.id,
|
|
email: user.email,
|
|
...(await requestContext()),
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function resetPasswordByEmail(formData: FormData) {
|
|
const parsed = resetPasswordSchema.parse(Object.fromEntries(formData));
|
|
if (parsed.password !== parsed.confirmPassword) {
|
|
throw new Error("两次输入的新密码不一致");
|
|
}
|
|
|
|
const record = await consumePasswordResetToken(parsed.token);
|
|
const hashedPassword = await bcrypt.hash(parsed.password, 12);
|
|
await prisma.user.update({
|
|
where: { id: record.userId },
|
|
data: { password: hashedPassword },
|
|
});
|
|
}
|
|
|
|
|
|
const confirmEmailSchema = z.object({
|
|
token: z.string().trim().min(20, "验证链接无效"),
|
|
});
|
|
|
|
export async function confirmEmailToken(formData: FormData) {
|
|
const parsed = confirmEmailSchema.parse(Object.fromEntries(formData));
|
|
return verifyEmailToken(parsed.token);
|
|
}
|