Comprehensive admin + user dashboards (production-ready)
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
"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 };
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { requireAuth } from "@/lib/auth/guards";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { PageHeader } from "@/components/app/page-header";
|
||||
import { SettingsClient } from "@/components/app/settings-client";
|
||||
|
||||
@@ -7,10 +8,24 @@ export const metadata: Metadata = { title: "Settings" };
|
||||
|
||||
export default async function SettingsPage() {
|
||||
const session = await requireAuth();
|
||||
|
||||
const prefs = await prisma.userPreferences.findUnique({
|
||||
where: { userId: session.user.id },
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Settings" description="Manage your account." />
|
||||
<SettingsClient name={session.user.name} email={session.user.email} />
|
||||
<SettingsClient
|
||||
name={session.user.name}
|
||||
email={session.user.email}
|
||||
preferences={{
|
||||
defaultVoiceId: prefs?.defaultVoiceId ?? null,
|
||||
defaultLanguage: prefs?.defaultLanguage ?? "en",
|
||||
emailOnEpisodeReady: prefs?.emailOnEpisodeReady ?? true,
|
||||
productEmails: prefs?.productEmails ?? true,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user