feat(kunden): Kundenverwaltung, Hostzuweisung und Token-Verwaltung (#006)

Kunden und Standorte pflegen, nicht zugeordnete Hosts in einem Schritt zuweisen,
Enrollment- und Host-Tokens verwalten, Audit-Log. Dashboard, Kundenseiten und
globale Suche laufen jetzt auf der Datenbank. 45 Tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 21:02:44 +02:00
co-authored by Claude Opus 5
parent f591b45aac
commit f9a5ad2daa
40 changed files with 2060 additions and 415 deletions
+101
View File
@@ -0,0 +1,101 @@
"use client";
import { useActionState } from "react";
import { CopyableCode } from "@/components/ui/CopyableCode";
import { enrollmentTokenErzeugen, type TokenErgebnis } from "@/server/actions/tokens";
const LEER: TokenErgebnis = {};
/** Erzeugt Enrollment-Tokens und zeigt den Klartext genau einmal an. */
export function TokenFormular({
kunden,
dashboardUrl,
}: {
kunden: { id: string; name: string }[];
dashboardUrl: string;
}) {
const [ergebnis, aktion, laeuft] = useActionState(enrollmentTokenErzeugen, LEER);
return (
<>
{ergebnis.fehler ? <div className="alert alert-danger">{ergebnis.fehler}</div> : null}
{ergebnis.klartext ? (
<div className="alert alert-success">
<p className="fw-semibold mb-2">
<i className="bi bi-check-circle me-1" aria-hidden="true" />
Token erzeugt – er wird nur dieses eine Mal angezeigt.
</p>
<div className="mb-2">
<CopyableCode wert={ergebnis.klartext} />
</div>
<p className="mb-1 small">Auf dem Proxmox-Host als root ausführen:</p>
<pre className="bg-body p-2 rounded mb-0">
<code className="pl-mono">
curl -fsSL {dashboardUrl}/install.sh | bash -s -- --token {ergebnis.klartext}
</code>
</pre>
</div>
) : null}
<form action={aktion} className="row g-3">
<div className="col-md-4">
<label className="form-label" htmlFor="bezeichnung">
Bezeichnung
</label>
<input
id="bezeichnung"
name="bezeichnung"
className="form-control"
placeholder="z. B. Rollout Brandl"
/>
</div>
<div className="col-md-4">
<label className="form-label" htmlFor="token-kundeId">
Hosts direkt zuordnen zu
</label>
<select id="token-kundeId" name="kundeId" className="form-select">
<option value="">– keinem Kunden –</option>
{kunden.map((kunde) => (
<option key={kunde.id} value={kunde.id}>
{kunde.name}
</option>
))}
</select>
</div>
<div className="col-md-2">
<label className="form-label" htmlFor="gueltigTage">
Gültig (Tage)
</label>
<input
id="gueltigTage"
name="gueltigTage"
type="number"
min={1}
max={365}
defaultValue={30}
className="form-control"
/>
</div>
<div className="col-md-2">
<label className="form-label" htmlFor="maxNutzungen">
Max. Nutzungen
</label>
<input
id="maxNutzungen"
name="maxNutzungen"
type="number"
min={1}
placeholder="unbegrenzt"
className="form-control"
/>
</div>
<div className="col-12">
<button type="submit" className="btn btn-primary pl-touch-target" disabled={laeuft}>
{laeuft ? "Erzeugt …" : "Token erzeugen"}
</button>
</div>
</form>
</>
);
}