"use client"; import { useCallback, useEffect, useState } from "react"; import { DomainBericht } from "@/components/domain-bericht"; import { Icon } from "@/components/icons"; import { Modal } from "@/components/modal"; import { Badge, Button, Card, Checkbox, Field, Notice, TextInput, } from "@/components/ui"; import { ApiError, apiJson } from "@/lib/api-client"; import type { Pruefbericht } from "@/lib/domain-check"; type Grant = { groupId: string; groupName: string; isDefault: boolean }; type AdminDomain = { id: string; hostname: string; label: string | null; isActive: boolean; codeCount: number; grants: Grant[]; }; type GroupOption = { id: string; name: string }; export function DomainAdmin({ baseUrlHostname }: { baseUrlHostname: string }) { const [domains, setDomains] = useState(null); const [groups, setGroups] = useState([]); const [error, setError] = useState(null); const [busy, setBusy] = useState(null); const [creating, setCreating] = useState(false); const [form, setForm] = useState({ hostname: "", label: "" }); const [formError, setFormError] = useState(null); const [editing, setEditing] = useState(null); const [grants, setGrants] = useState([]); const [grantError, setGrantError] = useState(null); const [deleting, setDeleting] = useState(null); /** Prüfberichte je Domain — bleiben stehen, bis erneut geprüft wird. */ const [reports, setReports] = useState>({}); const [checking, setChecking] = useState(null); const load = useCallback(async () => { try { const result = await apiJson<{ domains: AdminDomain[]; groups: GroupOption[]; }>("/api/admin/domains"); setDomains(result.domains); setGroups(result.groups); setError(null); } catch (caught) { setDomains([]); setError( caught instanceof ApiError ? caught.message : "Die Domains konnten nicht geladen werden.", ); } }, []); useEffect(() => { // setState folgt erst nach dem await der Netzwerkantwort. // eslint-disable-next-line react-hooks/set-state-in-effect void load(); }, [load]); const create = async () => { setFormError(null); try { await apiJson("/api/admin/domains", { method: "POST", json: form }); setCreating(false); setForm({ hostname: "", label: "" }); await load(); } catch (caught) { setFormError( caught instanceof ApiError ? caught.message : "Die Domain konnte nicht angelegt werden.", ); } }; const patch = async (id: string, body: unknown, onDone?: () => void) => { setBusy(id); setError(null); try { await apiJson(`/api/admin/domains/${id}`, { method: "PATCH", json: body }); await load(); onDone?.(); return true; } catch (caught) { const message = caught instanceof ApiError ? caught.message : "Die Änderung konnte nicht gespeichert werden."; if (onDone) setGrantError(message); else setError(message); return false; } finally { setBusy(null); } }; const remove = async () => { if (!deleting) return; try { await apiJson(`/api/admin/domains/${deleting.id}`, { method: "DELETE" }); setDeleting(null); await load(); } catch (caught) { setError( caught instanceof ApiError ? caught.message : "Die Domain konnte nicht gelöscht werden.", ); setDeleting(null); } }; const check = async (domain: AdminDomain) => { setChecking(domain.id); setError(null); try { const { report } = await apiJson<{ report: Pruefbericht }>( `/api/admin/domains/${domain.id}/check`, { method: "POST" }, ); setReports((bisher) => ({ ...bisher, [domain.id]: report })); } catch (caught) { setError( caught instanceof ApiError ? caught.message : "Die Prüfung ließ sich nicht ausführen.", ); } finally { setChecking(null); } }; if (domains === null) { return

Domains werden geladen …

; } return (
{error ? {error} : null} Eine Domain wirkt erst, wenn dein Reverse Proxy sie auf diese Anwendung weiterleitet. Die Weiterleitung erkennt sie am{" "} Host-Header und prüft ihn gegen diese Liste — ein unbekannter Hostname endet mit 404.
{domains.map((domain) => (

{domain.hostname}

{domain.hostname === baseUrlHostname ? ( aus BASE_URL ) : null} {!domain.isActive ? ( abgeschaltet ) : null} {domain.codeCount} {domain.codeCount === 1 ? "Code" : "Codes"}
{domain.label ? (

{domain.label}

) : null}

{domain.grants.length === 0 ? "Keiner Gruppe zugeteilt — niemand kann hier Codes anlegen." : domain.grants .map( (grant) => `${grant.groupName}${grant.isDefault ? " (Voreinstellung)" : ""}`, ) .join(" · ")}

{reports[domain.id] ? ( ) : null}
))} {/* --------------------------------------------------- Registrieren */} setCreating(false)} footer={ <> } >
{formError ? {formError} : null} setForm({ ...form, hostname: event.target.value }) } /> setForm({ ...form, label: event.target.value })} />
{/* ------------------------------------------------------- Zuteilung */} setEditing(null)} footer={ <> } >
{grantError ? {grantError} : null}

Mehrere Gruppen dürfen dieselbe Domain nutzen. Die Voreinstellung bestimmt, welche Domain beim Anlegen eines Codes vorausgewählt ist.

    {groups.map((group) => { const grant = grants.find((entry) => entry.groupId === group.id); return (
  • setGrants((current) => event.target.checked ? [ ...current, { groupId: group.id, groupName: group.name, isDefault: false, }, ] : current.filter((entry) => entry.groupId !== group.id), ) } /> {grant ? ( ) : null}
  • ); })}
{/* -------------------------------------------------------- Löschen */} setDeleting(null)} footer={ <> } > {deleting ? (

{deleting.hostname} {" "} wird entfernt. {deleting.codeCount > 0 ? ` Unter dieser Domain liegen noch ${deleting.codeCount} ${deleting.codeCount === 1 ? "Code" : "Codes"} — das Löschen wird deshalb abgelehnt. Schalte sie stattdessen ab.` : " Es liegen keine Codes darunter."}

) : null}
); }