Files
podcastdistributiona/app/(app)/settings/actions.ts
T

92 lines
3.3 KiB
TypeScript

"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<string>(LANGUAGES.map((l) => l.code));
const VALID_VOICES = new Set<string>(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<typeof preferencesSchema>;
/**
* 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 };
}