"use server"; import { z } from "zod"; import { and, desc, eq } from "drizzle-orm"; import { createUser, getUser, getAuthSettings } from "@/lib/db/queries"; import { EMAIL_VERIFICATION_PURPOSE_REGISTER, hashEmailVerificationCode, } from "@/lib/auth/email-verification"; import { db } from "@/lib/db"; import { emailVerificationCode } from "@/lib/db/schema"; import { signIn } from "./auth"; const authFormSchema = z.object({ email: z.string().email(), password: z.string().min(6), }); const registerFormSchema = z .object({ email: z.string().email(), password: z.string().min(6), confirmPassword: z.string().min(6), verificationCode: z.string().optional(), }) .refine((data) => data.password === data.confirmPassword, { path: ["confirmPassword"], message: "Password mismatch", }); export type LoginActionState = { status: "idle" | "in_progress" | "success" | "failed" | "invalid_data"; }; export const login = async ( _: LoginActionState, formData: FormData ): Promise => { try { const provider = formData.get("provider") as string; if (provider === "dingtalk") { const token = formData.get("token"); console.log("[DingTalk Login] Received token:", token); if (!token) return { status: "invalid_data" }; try { const result = await signIn("dingtalk", { token, redirect: false, }); console.log("[DingTalk Login] signIn result:", result); return { status: "success" }; } catch (signInError) { console.error("[DingTalk Login] signIn error:", signInError); throw signInError; } } const validatedData = authFormSchema.parse({ email: formData.get("email"), password: formData.get("password"), }); const normalizedEmail = validatedData.email.trim().toLowerCase(); await signIn("credentials", { email: normalizedEmail, password: validatedData.password, redirect: false, }); return { status: "success" }; } catch (error) { if (error instanceof z.ZodError) { return { status: "invalid_data" }; } return { status: "failed" }; } }; export type RegisterActionState = { status: | "idle" | "in_progress" | "success" | "failed" | "user_exists" | "password_mismatch" | "verification_code_required" | "verification_code_invalid" | "verification_code_expired" | "invalid_data"; }; export const register = async ( _: RegisterActionState, formData: FormData ): Promise => { try { // 检查是否开放注册 const settings = await getAuthSettings(); if (settings.isRegistrationEnabled === false) { return { status: "failed" }; } const validatedData = registerFormSchema.parse({ email: formData.get("email"), password: formData.get("password"), confirmPassword: formData.get("confirmPassword"), verificationCode: typeof formData.get("verificationCode") === "string" ? String(formData.get("verificationCode")) : undefined, }); const normalizedEmail = validatedData.email.trim().toLowerCase(); const verificationCodeInput = String( validatedData.verificationCode || "" ).trim(); if (settings.isEmailVerificationEnabled === true) { if (!verificationCodeInput) { return { status: "verification_code_required" }; } if (!/^\d{6}$/.test(verificationCodeInput)) { return { status: "verification_code_invalid" }; } const [latestCode] = await db .select() .from(emailVerificationCode) .where( and( eq(emailVerificationCode.email, normalizedEmail), eq( emailVerificationCode.purpose, EMAIL_VERIFICATION_PURPOSE_REGISTER ) ) ) .orderBy(desc(emailVerificationCode.createdAt)) .limit(1); if (!latestCode) { return { status: "verification_code_invalid" }; } if (new Date(latestCode.expiresAt) <= new Date()) { await db .delete(emailVerificationCode) .where(eq(emailVerificationCode.id, latestCode.id)); return { status: "verification_code_expired" }; } const expectedHash = hashEmailVerificationCode({ email: normalizedEmail, purpose: EMAIL_VERIFICATION_PURPOSE_REGISTER, code: verificationCodeInput, }); if (latestCode.codeHash !== expectedHash) { return { status: "verification_code_invalid" }; } } const [user] = await getUser(normalizedEmail); if (user) { return { status: "user_exists" } as RegisterActionState; } await createUser(normalizedEmail, validatedData.password); await signIn("credentials", { email: normalizedEmail, password: validatedData.password, redirect: false, }); await db .delete(emailVerificationCode) .where( and( eq(emailVerificationCode.email, normalizedEmail), eq(emailVerificationCode.purpose, EMAIL_VERIFICATION_PURPOSE_REGISTER) ) ); return { status: "success" }; } catch (error) { if (error instanceof z.ZodError) { const passwordMismatch = error.issues.some( (issue) => issue.path?.[0] === "confirmPassword" ); if (passwordMismatch) { return { status: "password_mismatch" }; } return { status: "invalid_data" }; } return { status: "failed" }; } };