since3-portal/modules/admin/actions/settings.ts
2026-09-29 16:15:28 +02:00

155 lines
6.7 KiB
TypeScript

"use server";
import * as z from "zod";
import { revalidatePath } from "next/cache";
import { createClient as createSupabaseClient } from "@supabase/supabase-js";
import { createClient } from "@/lib/supabase/server";
import { verifySession } from "@/lib/supabase/dal";
import { MIN_PASSWORD_LENGTH } from "@/modules/admin/lib/constants";
export type SettingsState = { error: string } | { success: true; message?: string } | undefined;
const IdSchema = z.string().uuid();
async function requireAdmin() {
const session = await verifySession();
return session.isAdmin ? session : null;
}
function firstIssue(error: z.ZodError) {
return error.issues[0]?.message ?? "Ungültige Eingabe.";
}
export async function updateOwnProfile(_prev: SettingsState, formData: FormData): Promise<SettingsState> {
const session = await requireAdmin();
if (!session) return { error: "Keine Berechtigung." };
const name = z.string().trim().min(2, { error: "Mindestens zwei Zeichen eingeben." }).max(100).safeParse(formData.get("name"));
if (!name.success) return { error: firstIssue(name.error) };
const supabase = await createClient();
const { error } = await supabase.from("profiles").update({ name: name.data }).eq("id", session.userId);
if (error) return { error: "Name konnte nicht gespeichert werden." };
await supabase.auth.updateUser({ data: { name: name.data } });
revalidatePath("/admin", "layout");
return { success: true, message: "Name gespeichert." };
}
const PasswordSchema = z
.object({
current: z.string().min(1, { error: "Bitte das aktuelle Passwort eingeben." }),
next: z.string().min(MIN_PASSWORD_LENGTH, { error: `Das neue Passwort braucht mindestens ${MIN_PASSWORD_LENGTH} Zeichen.` }).max(72),
confirm: z.string(),
})
.refine((data) => data.next === data.confirm, { path: ["confirm"], error: "Die Passwörter stimmen nicht überein." })
.refine((data) => data.next !== data.current, { path: ["next"], error: "Das neue Passwort muss sich vom alten unterscheiden." });
export async function changeOwnPassword(_prev: SettingsState, formData: FormData): Promise<SettingsState> {
const session = await requireAdmin();
if (!session) return { error: "Keine Berechtigung." };
const parsed = PasswordSchema.safeParse({
current: formData.get("current"),
next: formData.get("next"),
confirm: formData.get("confirm"),
});
if (!parsed.success) return { error: firstIssue(parsed.error) };
// Aktuelles Passwort mit einem separaten Client prüfen, damit die Session des Admins unberührt bleibt.
const verifier = createSupabaseClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{ auth: { persistSession: false, autoRefreshToken: false } }
);
const { error: signInError } = await verifier.auth.signInWithPassword({ email: session.email, password: parsed.data.current });
if (signInError) return { error: "Das aktuelle Passwort ist falsch." };
await verifier.auth.signOut({ scope: "local" });
const supabase = await createClient();
const { error } = await supabase.auth.updateUser({ password: parsed.data.next });
if (error) return { error: "Passwort konnte nicht geändert werden." };
return { success: true, message: "Passwort geändert." };
}
const TemplateSchema = z.object({
name: z.string().trim().min(1, { error: "Name ist erforderlich." }).max(200),
description: z
.string()
.trim()
.max(500)
.transform((value) => (value === "" ? null : value)),
unitPrice: z.number({ error: "Bitte einen gültigen Preis eingeben." }).min(0).lt(1e10).multipleOf(0.01, { error: "Preis: maximal 2 Nachkommastellen." }),
taxRate: z.number({ error: "Bitte eine gültige MwSt eingeben." }).min(0).max(100).multipleOf(0.01),
});
function parseDecimal(value: FormDataEntryValue | null) {
const trimmed = String(value ?? "").trim();
if (trimmed === "") return Number.NaN;
return Number(trimmed.includes(",") ? trimmed.replace(/\./g, "").replace(",", ".") : trimmed);
}
export async function saveProductTemplate(_prev: SettingsState, formData: FormData): Promise<SettingsState> {
if (!(await requireAdmin())) return { error: "Keine Berechtigung." };
const parsed = TemplateSchema.safeParse({
name: formData.get("name") ?? "",
description: formData.get("description") ?? "",
unitPrice: parseDecimal(formData.get("unitPrice")),
taxRate: parseDecimal(formData.get("taxRate")),
});
if (!parsed.success) return { error: firstIssue(parsed.error) };
const values = {
name: parsed.data.name,
description: parsed.data.description,
unit_price: parsed.data.unitPrice,
tax_rate: parsed.data.taxRate,
};
const id = String(formData.get("id") ?? "");
const supabase = await createClient();
if (id === "") {
const { error } = await supabase.from("product_templates").insert(values);
if (error) return { error: "Vorlage konnte nicht gespeichert werden." };
} else {
if (!IdSchema.safeParse(id).success) return { error: "Ungültige Vorlage." };
const { data, error } = await supabase.from("product_templates").update(values).eq("id", id).select("id");
if (error || !data?.length) return { error: "Vorlage konnte nicht gespeichert werden." };
}
revalidatePath("/admin/settings");
return { success: true };
}
export async function setProductTemplateActive(id: string, isActive: boolean): Promise<SettingsState> {
if (!(await requireAdmin())) return { error: "Keine Berechtigung." };
if (!IdSchema.safeParse(id).success) return { error: "Ungültige Vorlage." };
const supabase = await createClient();
const { data, error } = await supabase
.from("product_templates")
.update({ is_active: Boolean(isActive) })
.eq("id", id)
.select("id");
if (error || !data?.length) return { error: "Vorlage konnte nicht geändert werden." };
revalidatePath("/admin/settings");
return { success: true };
}
// Bestehende Rechnungen bleiben unverändert: invoice_items speichern Bezeichnung und Preis als Snapshot.
export async function deleteProductTemplate(id: string): Promise<SettingsState> {
if (!(await requireAdmin())) return { error: "Keine Berechtigung." };
if (!IdSchema.safeParse(id).success) return { error: "Ungültige Vorlage." };
const supabase = await createClient();
const { data, error } = await supabase.from("product_templates").delete().eq("id", id).select("id");
if (error || !data?.length) return { error: "Vorlage konnte nicht gelöscht werden." };
revalidatePath("/admin/settings");
return { success: true };
}