"use server"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import { getServerSession } from "@/lib/auth/guards"; import { prisma } from "@/lib/db"; import { LANGUAGES } from "@/lib/episodes/options"; import { VOICE_CATALOG } from "@/lib/ai/voices"; const VALID_LANGUAGES = new Set(LANGUAGES.map((l) => l.code)); const VALID_VOICES = new Set(VOICE_CATALOG.map((v) => v.id)); const preferencesSchema = z.object({ defaultVoiceId: z.string().nullable().optional(), defaultLanguage: z.string().min(2).max(5).optional(), emailOnEpisodeReady: z.boolean().optional(), productEmails: z.boolean().optional(), }); export type PreferencesInput = z.infer; /** * Persist the current user's editor defaults and notification preferences. * Auth-checked; upserts the single per-user preferences row. Only validated, * known voice/language values are stored. */ export async function savePreferencesAction( input: PreferencesInput ): Promise<{ ok: boolean; error?: string }> { const session = await getServerSession(); if (!session) return { ok: false, error: "You must be signed in." }; const parsed = preferencesSchema.safeParse(input); if (!parsed.success) return { ok: false, error: "Invalid settings." }; const data = parsed.data; if (data.defaultVoiceId && !VALID_VOICES.has(data.defaultVoiceId)) { return { ok: false, error: "Unknown voice." }; } if (data.defaultLanguage && !VALID_LANGUAGES.has(data.defaultLanguage)) { return { ok: false, error: "Unsupported language." }; } const userId = session.user.id; // Normalize "none"/empty voice to null. const defaultVoiceId = data.defaultVoiceId === undefined ? undefined : data.defaultVoiceId || null; await prisma.userPreferences.upsert({ where: { userId }, create: { userId, defaultVoiceId: defaultVoiceId ?? null, defaultLanguage: data.defaultLanguage ?? "en", emailOnEpisodeReady: data.emailOnEpisodeReady ?? true, productEmails: data.productEmails ?? true, }, update: { ...(defaultVoiceId !== undefined ? { defaultVoiceId } : {}), ...(data.defaultLanguage !== undefined ? { defaultLanguage: data.defaultLanguage } : {}), ...(data.emailOnEpisodeReady !== undefined ? { emailOnEpisodeReady: data.emailOnEpisodeReady } : {}), ...(data.productEmails !== undefined ? { productEmails: data.productEmails } : {}), }, }); revalidatePath("/settings"); return { ok: true }; } /** * Permanently delete the current user's account. Auth-checked and gated by a * typed email confirmation that must match the session email. The User delete * cascades to sessions, accounts, episodes, series, usage and preferences. The * client signs out after a successful response. */ export async function deleteAccountAction( confirmEmail: string ): Promise<{ ok: boolean; error?: string }> { const session = await getServerSession(); if (!session) return { ok: false, error: "You must be signed in." }; if (confirmEmail.trim().toLowerCase() !== session.user.email.toLowerCase()) { return { ok: false, error: "The email you typed doesn't match your account." }; } // Deleting the User row cascades to all owned data via onDelete: Cascade. await prisma.user.delete({ where: { id: session.user.id } }); return { ok: true }; }