diff --git a/src/actions/admin/settings.ts b/src/actions/admin/settings.ts index 2e5b744..af9501b 100644 --- a/src/actions/admin/settings.ts +++ b/src/actions/admin/settings.ts @@ -38,6 +38,30 @@ const settingsSchema = z.object({ smtpFromEmail: z.string().trim().email("发件邮箱格式不正确").optional().or(z.literal("")), }); +const smtpTestEmailSchema = z.string().trim().email("请输入正确的测试邮箱"); +const smtpTestSettingsSchema = settingsSchema.extend({ + smtpTestEmail: smtpTestEmailSchema, +}); + +type AdminSession = Awaited>; +type SettingsActionResult = { ok: true } | { ok: false; error: string }; +type SmtpTestActionResult = + | { ok: true } + | { ok: false; error: string; settingsSaved?: boolean }; + +function formatActionError(error: unknown, fallback: string) { + if (error instanceof z.ZodError) { + return error.issues[0]?.message ?? fallback; + } + if (error instanceof Error && error.message.trim()) { + return error.message.trim(); + } + if (typeof error === "string" && error.trim()) { + return error.trim(); + } + return fallback; +} + function buildSettingsUpdate(parsed: z.infer, current: Awaited>) { const smtpEnabled = parsed.smtpEnabled === "true"; const emailVerificationRequired = parsed.emailVerificationRequired === "true"; @@ -87,9 +111,22 @@ function buildSettingsUpdate(parsed: z.infer, current: Aw return next; } -export async function saveAppSettings(formData: FormData) { - const session = await requireAdmin(); - const parsed = settingsSchema.parse(Object.fromEntries(formData)); +function revalidateSettingsViews() { + revalidatePath("/admin/settings"); + revalidatePath("/login"); + revalidatePath("/register"); + revalidatePath("/dashboard"); + revalidatePath("/subscriptions"); + revalidatePath("/admin/nodes"); + revalidatePath("/account"); + revalidatePath("/admin/commerce"); +} + +async function persistAppSettings( + session: AdminSession, + parsed: z.infer, + message: string, +) { const current = await getAppConfig(); const next = buildSettingsUpdate(parsed, current); @@ -105,26 +142,49 @@ export async function saveAppSettings(formData: FormData) { targetType: "AppConfig", targetId: current.id, targetLabel: next.siteName, - message: "更新系统设置", + message, }); - revalidatePath("/admin/settings"); - revalidatePath("/login"); - revalidatePath("/register"); - revalidatePath("/dashboard"); - revalidatePath("/subscriptions"); - revalidatePath("/admin/nodes"); - revalidatePath("/account"); - revalidatePath("/admin/commerce"); + revalidateSettingsViews(); + + return next; } - -const smtpTestSchema = z.object({ - smtpTestEmail: z.string().trim().email("请输入正确的测试邮箱"), -}); - -export async function sendSmtpTestMessage(formData: FormData) { - await requireAdmin(); - const parsed = smtpTestSchema.parse(Object.fromEntries(formData)); - await sendSmtpTestEmail(parsed.smtpTestEmail); +export async function saveAppSettings(formData: FormData): Promise { + try { + const session = await requireAdmin(); + const parsed = settingsSchema.parse(Object.fromEntries(formData)); + await persistAppSettings(session, parsed, "更新系统设置"); + return { ok: true }; + } catch (error) { + return { ok: false, error: formatActionError(error, "保存失败") }; + } +} + +export async function testSmtpSettings(formData: FormData): Promise { + let parsed: z.infer; + let next: Awaited>; + + try { + const session = await requireAdmin(); + parsed = smtpTestSettingsSchema.parse(Object.fromEntries(formData)); + next = await persistAppSettings(session, parsed, "测试发信前更新系统设置"); + } catch (error) { + return { ok: false, error: formatActionError(error, "测试邮件发送失败") }; + } + + if (!next.smtpEnabled) { + return { ok: false, settingsSaved: true, error: "测试发信前请先开启邮件服务" }; + } + + try { + await sendSmtpTestEmail(parsed.smtpTestEmail); + return { ok: true }; + } catch (error) { + return { + ok: false, + settingsSaved: true, + error: formatActionError(error, "请检查 SMTP 配置后重试"), + }; + } } diff --git a/src/app/(admin)/admin/settings/settings-form.tsx b/src/app/(admin)/admin/settings/settings-form.tsx index 6dcce1f..6d8d106 100644 --- a/src/app/(admin)/admin/settings/settings-form.tsx +++ b/src/app/(admin)/admin/settings/settings-form.tsx @@ -6,7 +6,7 @@ import { Button, buttonVariants } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; -import { saveAppSettings, sendSmtpTestMessage } from "@/actions/admin/settings"; +import { saveAppSettings, testSmtpSettings } from "@/actions/admin/settings"; import { toast } from "sonner"; import { getErrorMessage } from "@/lib/errors"; @@ -52,7 +52,11 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons: async function handleSubmit(formData: FormData) { setSaving(true); try { - await saveAppSettings(formData); + const result = await saveAppSettings(formData); + if (!result.ok) { + toast.error(result.error); + return; + } toast.success("设置已保存"); } catch (error) { toast.error(getErrorMessage(error, "保存失败")); @@ -67,8 +71,16 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons: setTestingEmail(true); try { - await sendSmtpTestMessage(new FormData(form)); - toast.success("测试邮件已发送"); + const result = await testSmtpSettings(new FormData(form)); + if (!result.ok) { + toast.error( + result.settingsSaved + ? `设置已保存,但测试邮件没有发出:${result.error}` + : result.error, + ); + return; + } + toast.success("设置已保存,测试邮件已发送"); } catch (error) { toast.error(getErrorMessage(error, "测试邮件发送失败")); } finally { @@ -208,7 +220,7 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons: SMTP 邮件服务

- 用于注册邮箱验证、忘记密码和账户邮箱变更。密码留空会保留当前配置;测试邮件会使用已保存的配置。 + 用于注册邮箱验证、忘记密码和账户邮箱变更。密码留空会保留当前配置;测试会先保存当前 SMTP 设置,再发送测试邮件。