v1.5: Alle Manager-Komponenten auf eigene API-Routen umgestellt

Neue interne Route Handler unter app/api/ (dishes inkl. Bulk-Delete,
sections, allergens, additives, menu-entries, dish-suggestions,
settings, admin/branding) ersetzen den direkten Browser→Postgrest-
Zugriff der bisherigen Supabase-Anbindung. Jede Route prüft Autorisierung
selbst (lib/auth/session.ts requireRole) statt sich auf Postgres-RLS zu
verlassen – ersetzt 1:1 die bisherigen Policies (öffentlich lesbar /
staff+admin schreiben / nur admin für Einstellungen & Benutzer).

Rezeptvorschlag-Dublettencheck (SuggestionForm) läuft jetzt zwingend
serverseitig im POST-Handler, da es keinen Browser→Postgres-Pfad mehr
gibt. "Passkey annehmen" (SuggestionReview) läuft transaktional
(Gericht anlegen + Vorschlag löschen in einem Schritt).

Logo/Favicon: neuer lokaler Dateispeicher (lib/branding.ts, Docker-
Volume) statt Supabase-Storage-Bucket, ausgeliefert über
app/branding/[filename]. logoUrl()/faviconUrl() liefern jetzt einfache
relative Pfade statt einer fest verdrahteten Supabase-Storage-URL.

DeepL-Route (api/translate) und Benutzerverwaltung (api/admin/users)
laufen jetzt gegen die users-Tabelle statt Supabase Auth.

Alle "use client"-Manager-Komponenten (DishManager, WeekEditor,
SectionManager, CodeListManager, SuggestionForm/Review, UserManager,
DesignEditor) rufen ihre jeweiligen /api/-Routen per fetch() statt den
Supabase-Browser-Client zu nutzen.
This commit is contained in:
2026-07-18 11:01:52 +02:00
parent 28c9ec15c8
commit a9a9cf7ba4
58 changed files with 1184 additions and 1858 deletions
+33 -27
View File
@@ -1,7 +1,6 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import { dishCodes, formatPrice } from "@/lib/menu";
import { formatMessage } from "@/lib/i18n/shared";
import { localized } from "@/lib/types";
@@ -34,7 +33,6 @@ export default function DishManager({
}: {
translateEnabled: boolean;
}) {
const supabase = useMemo(() => createClient(), []);
const dict = useDict();
const locale = useLocale();
const [dishes, setDishes] = useState<Dish[]>([]);
@@ -58,20 +56,19 @@ export default function DishManager({
const load = useCallback(async () => {
const [dishesRes, allergensRes, additivesRes] = await Promise.all([
supabase.from("dishes").select("*").order("name"),
supabase.from("allergens").select("*").order("sort"),
supabase.from("additives").select("*").order("sort"),
fetch("/api/dishes"),
fetch("/api/allergens"),
fetch("/api/additives"),
]);
const problem = dishesRes.error ?? allergensRes.error ?? additivesRes.error;
if (problem)
setError(formatMessage(dict.dishManager.loadError, { message: problem.message }));
else {
setDishes(dishesRes.data as Dish[]);
setAllergens(allergensRes.data as CodeItem[]);
setAdditives(additivesRes.data as CodeItem[]);
if (!dishesRes.ok || !allergensRes.ok || !additivesRes.ok) {
setError(formatMessage(dict.dishManager.loadError, { message: "load failed" }));
} else {
setDishes(await dishesRes.json());
setAllergens(await allergensRes.json());
setAdditives(await additivesRes.json());
}
setLoading(false);
}, [supabase, dict]);
}, [dict]);
useEffect(() => {
load();
@@ -94,11 +91,19 @@ export default function DishManager({
allergens: form.allergens,
additives: form.additives,
};
const { error } = form.id
? await supabase.from("dishes").update(payload).eq("id", form.id)
: await supabase.from("dishes").insert(payload);
if (error) {
setError(formatMessage(dict.dishManager.saveError, { message: error.message }));
const res = form.id
? await fetch(`/api/dishes/${form.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
: await fetch("/api/dishes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) {
setError(formatMessage(dict.dishManager.saveError, { message: String(res.status) }));
return;
}
setForm(null);
@@ -112,9 +117,9 @@ export default function DishManager({
)
)
return;
const { error } = await supabase.from("dishes").delete().eq("id", dish.id);
if (error)
setError(formatMessage(dict.dishManager.deleteError, { message: error.message }));
const res = await fetch(`/api/dishes/${dish.id}`, { method: "DELETE" });
if (!res.ok)
setError(formatMessage(dict.dishManager.deleteError, { message: String(res.status) }));
setSelected((prev) => {
const next = new Set(prev);
next.delete(dish.id);
@@ -156,12 +161,13 @@ export default function DishManager({
return;
setBulkBusy(true);
setError(null);
const { error } = await supabase
.from("dishes")
.delete()
.in("id", Array.from(selected));
if (error)
setError(formatMessage(dict.dishManager.deleteError, { message: error.message }));
const res = await fetch("/api/dishes/bulk-delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ids: Array.from(selected) }),
});
if (!res.ok)
setError(formatMessage(dict.dishManager.deleteError, { message: String(res.status) }));
setSelected(new Set());
setBulkBusy(false);
await load();