mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 21:41:16 +00:00
155 lines
6.8 KiB
TypeScript
155 lines
6.8 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";
|
|
import { parseDecimalInput } from "@/modules/shared/lib/parse-decimal";
|
|
|
|
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." };
|
|
|
|
const { error: authError } = await supabase.auth.updateUser({ data: { name: name.data } });
|
|
|
|
revalidatePath("/admin", "layout");
|
|
if (authError) return { error: "Name konnte nicht vollständig gespeichert werden. Bitte erneut speichern." };
|
|
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) {
|
|
return parseDecimalInput(String(value ?? ""));
|
|
}
|
|
|
|
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 };
|
|
}
|