since3-portal/modules/shared/actions/info.ts
2026-09-29 15:20:50 +02:00

163 lines
5.6 KiB
TypeScript

"use server";
import * as z from "zod";
import { revalidatePath } from "next/cache";
import { createClient } from "@/lib/supabase/server";
import { verifySession } from "@/lib/supabase/dal";
import { getClientId } from "@/modules/shared/queries/profile";
import { NoteSchema, ProductSchema, TaskSchema } from "@/modules/shared/schemas/info";
export type InfoActionState = { error: string } | { success: true } | undefined;
const IdSchema = z.string().uuid();
type Table = "client_products" | "product_tasks" | "internal_notes";
const DELETE_TABLES: Record<string, Table | undefined> = {
product: "client_products",
task: "product_tasks",
note: "internal_notes",
};
function firstError(error: z.ZodError) {
return error.issues[0]?.message ?? "Bitte alle Pflichtfelder ausfüllen.";
}
function revalidateInfo(profileId: string) {
revalidatePath("/client/dashboard");
revalidatePath(`/admin/clients/${profileId}`);
}
// Admin-Check + Client auflösen. Alle Schreibzugriffe laufen danach zusätzlich über RLS (admin_all).
async function resolveAdminTarget(profileId: string): Promise<{ error: string } | { clientId: string }> {
const session = await verifySession();
if (!session.isAdmin) return { error: "Keine Berechtigung." };
if (!IdSchema.safeParse(profileId).success) return { error: "Ungültiger Client." };
const clientId = await getClientId(profileId);
if (!clientId) return { error: "Client hat noch keine Stammdaten hinterlegt." };
return { clientId };
}
async function upsertRow(
table: Table,
clientId: string,
id: string | null,
values: Record<string, unknown>
): Promise<boolean> {
const supabase = await createClient();
if (!id) {
const { error } = await supabase.from(table).insert({ ...values, client_id: clientId });
return !error;
}
if (!IdSchema.safeParse(id).success) return false;
const { data, error } = await supabase
.from(table)
.update(values)
.eq("id", id)
.eq("client_id", clientId)
.select("id");
return !error && Boolean(data?.length);
}
function formString(formData: FormData, key: string) {
return String(formData.get(key) ?? "");
}
function formId(formData: FormData) {
const id = formString(formData, "id");
return id === "" ? null : id;
}
export async function saveProduct(_prev: InfoActionState, formData: FormData): Promise<InfoActionState> {
const profileId = formString(formData, "profileId");
const target = await resolveAdminTarget(profileId);
if ("error" in target) return target;
const parsed = ProductSchema.safeParse({
name: formString(formData, "name"),
category: formString(formData, "category"),
url: formString(formData, "url"),
status: formString(formData, "status"),
statusMessage: formString(formData, "statusMessage"),
uptime30d: formString(formData, "uptime30d"),
});
if (!parsed.success) return { error: firstError(parsed.error) };
const ok = await upsertRow("client_products", target.clientId, formId(formData), {
name: parsed.data.name,
category: parsed.data.category,
url: parsed.data.url,
status: parsed.data.status,
status_message: parsed.data.statusMessage,
uptime_30d: parsed.data.uptime30d,
});
if (!ok) return { error: "Produkt konnte nicht gespeichert werden." };
revalidateInfo(profileId);
return { success: true };
}
export async function saveTask(_prev: InfoActionState, formData: FormData): Promise<InfoActionState> {
const profileId = formString(formData, "profileId");
const target = await resolveAdminTarget(profileId);
if ("error" in target) return target;
const parsed = TaskSchema.safeParse({
name: formString(formData, "name"),
description: formString(formData, "description"),
status: formString(formData, "status"),
since: formString(formData, "since"),
});
if (!parsed.success) return { error: firstError(parsed.error) };
const ok = await upsertRow("product_tasks", target.clientId, formId(formData), parsed.data);
if (!ok) return { error: "Vorgang konnte nicht gespeichert werden." };
revalidateInfo(profileId);
return { success: true };
}
export async function saveNote(_prev: InfoActionState, formData: FormData): Promise<InfoActionState> {
const profileId = formString(formData, "profileId");
const target = await resolveAdminTarget(profileId);
if ("error" in target) return target;
const parsed = NoteSchema.safeParse({ body: formString(formData, "body") });
if (!parsed.success) return { error: firstError(parsed.error) };
const ok = await upsertRow("internal_notes", target.clientId, formId(formData), parsed.data);
if (!ok) return { error: "Notiz konnte nicht gespeichert werden." };
revalidateInfo(profileId);
return { success: true };
}
export async function deleteInfoEntry(
kind: "product" | "task" | "note",
profileId: string,
id: string
): Promise<InfoActionState> {
const target = await resolveAdminTarget(profileId);
if ("error" in target) return target;
const table = Object.hasOwn(DELETE_TABLES, kind) ? DELETE_TABLES[kind] : undefined;
if (!table || !IdSchema.safeParse(id).success) return { error: "Ungültiger Eintrag." };
const supabase = await createClient();
const { data, error } = await supabase
.from(table)
.delete()
.eq("id", id)
.eq("client_id", target.clientId)
.select("id");
if (error || !data?.length) return { error: "Eintrag konnte nicht gelöscht werden." };
revalidateInfo(profileId);
return { success: true };
}