284 lines
12 KiB
TypeScript
284 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { useActionState, useState } from "react";
|
|
import { KeyRound, Save, Server, ShieldAlert, ShieldCheck } from "lucide-react";
|
|
import { FELD, KAESTCHEN } from "@/lib/formular";
|
|
import {
|
|
AD_ZUORDNUNG,
|
|
INTERVALL_HOECHSTENS,
|
|
INTERVALL_MINDESTENS,
|
|
ZUORDNUNGS_BESCHRIFTUNG,
|
|
ZUORDNUNGS_FELDER,
|
|
} from "@/lib/verzeichnis";
|
|
import {
|
|
quelleAnlegenAktion,
|
|
quelleBearbeitenAktion,
|
|
type AktionsErgebnis,
|
|
} from "@/server/actions/verzeichnis";
|
|
import type { QuellenSicht } from "@/server/services/verzeichnis";
|
|
|
|
/**
|
|
* Anlegen und Bearbeiten einer Verzeichnisquelle (#016).
|
|
*
|
|
* Ein Formular für beides, weil es dieselben Felder sind — mit einem
|
|
* Unterschied, der zählt: **beim Bearbeiten bleibt das Passwortfeld leer, und
|
|
* leer heißt „unverändert“.** Die Anwendung gibt das gespeicherte Passwort
|
|
* nirgends heraus; würde ein leeres Feld als Löschung verstanden, nähme jede
|
|
* Namensänderung der Quelle ihre Anmeldung.
|
|
*/
|
|
export function QuelleFormular({ quelle }: { quelle?: QuellenSicht }) {
|
|
const bearbeiten = quelle !== undefined;
|
|
const [ergebnis, aktion, laeuft] = useActionState<AktionsErgebnis, FormData>(
|
|
bearbeiten ? quelleBearbeitenAktion : quelleAnlegenAktion,
|
|
{},
|
|
);
|
|
const [zertifikatOffen, setzeZertifikatOffen] = useState(false);
|
|
const [zuordnungOffen, setzeZuordnungOffen] = useState(false);
|
|
const [adresse, setzeAdresse] = useState(quelle?.url ?? "");
|
|
const klartext = adresse.trim().startsWith("ldap://");
|
|
|
|
return (
|
|
<form action={aktion} className="flex flex-col gap-4">
|
|
{bearbeiten ? <input type="hidden" name="id" value={quelle.id} /> : null}
|
|
|
|
<label className="flex flex-col gap-1.5 text-sm">
|
|
<span className="text-nebentext">Name</span>
|
|
<input
|
|
name="name"
|
|
defaultValue={quelle?.name ?? ""}
|
|
required
|
|
placeholder="Firmen-AD"
|
|
className={FELD}
|
|
/>
|
|
</label>
|
|
|
|
{/*
|
|
* Die Erklärung steht **außerhalb** des `label` und hängt über
|
|
* `aria-describedby` am Feld. Im `label` wäre sie Teil des Feldnamens:
|
|
* ein Screenreader läse „Adresse Drei Formen: ldaps:// …“ als Bezeichnung
|
|
* des Eingabefelds, und kein Werkzeug fände das Feld noch über seinen
|
|
* Namen.
|
|
*/}
|
|
<div className="flex flex-col gap-1.5 text-sm">
|
|
<label htmlFor="quelle-url" className="text-nebentext">
|
|
Adresse
|
|
</label>
|
|
<input
|
|
id="quelle-url"
|
|
name="url"
|
|
value={adresse}
|
|
onChange={(ereignis) => setzeAdresse(ereignis.target.value)}
|
|
required
|
|
aria-describedby="quelle-url-hinweis"
|
|
placeholder="ldaps://dc01.firma.local:636"
|
|
className={`${FELD} mono`}
|
|
/>
|
|
<span id="quelle-url-hinweis" className="text-xs text-nebentext">
|
|
Drei Formen: <span className="mono">ldaps://</span> (verschlüsselt ab dem ersten Byte, die richtige
|
|
Wahl), <span className="mono">ldap+starttls://</span> (unverschlüsselt verbunden und dann
|
|
hochgestuft) und <span className="mono">ldap://</span>.
|
|
</span>
|
|
{klartext ? (
|
|
/*
|
|
* Die Warnung erscheint erst, wenn sie zutrifft — eine, die immer
|
|
* dasteht, liest nach dem dritten Mal niemand mehr.
|
|
*/
|
|
<span role="status" className="flex items-start gap-2 text-xs text-faellig">
|
|
<ShieldAlert className="mt-0.5 size-3.5 shrink-0" strokeWidth={2} aria-hidden="true" />
|
|
<span>
|
|
Mit <span className="mono">ldap://</span> geht auch das Bind-Passwort im Klartext über das Netz.
|
|
Für ein Dienstkonto im Active Directory ist das keine gute Idee.
|
|
</span>
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<label className="flex flex-col gap-1.5 text-sm">
|
|
<span className="text-nebentext">Anmelde-DN</span>
|
|
<input
|
|
name="bindDn"
|
|
defaultValue={quelle?.bindDn ?? ""}
|
|
required
|
|
placeholder="CN=laufzettel,OU=Dienstkonten,DC=firma,DC=local"
|
|
className={`${FELD} mono`}
|
|
/>
|
|
</label>
|
|
<div className="flex flex-col gap-1.5 text-sm">
|
|
<label htmlFor="quelle-passwort" className="flex items-center gap-2 text-nebentext">
|
|
<KeyRound className="size-4" strokeWidth={1.75} aria-hidden="true" />
|
|
Passwort
|
|
</label>
|
|
<input
|
|
id="quelle-passwort"
|
|
name="bindPasswort"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
aria-describedby="quelle-passwort-hinweis"
|
|
placeholder={bearbeiten ? "unverändert lassen" : ""}
|
|
required={!bearbeiten}
|
|
className={FELD}
|
|
/>
|
|
<span id="quelle-passwort-hinweis" className="text-xs text-nebentext">
|
|
{bearbeiten
|
|
? "Leer lassen behält das gespeicherte Passwort."
|
|
: "Wird verschlüsselt gespeichert und nie wieder angezeigt."}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<label className="flex flex-col gap-1.5 text-sm">
|
|
<span className="text-nebentext">Basis-DN</span>
|
|
<input
|
|
name="basisDn"
|
|
defaultValue={quelle?.basisDn ?? ""}
|
|
required
|
|
placeholder="OU=Benutzer,DC=firma,DC=local"
|
|
className={`${FELD} mono`}
|
|
/>
|
|
</label>
|
|
|
|
<div className="flex flex-col gap-1.5 text-sm">
|
|
<label htmlFor="quelle-filter" className="text-nebentext">
|
|
Filter (optional)
|
|
</label>
|
|
<input
|
|
id="quelle-filter"
|
|
name="filter"
|
|
defaultValue={quelle?.filter ?? ""}
|
|
aria-describedby="quelle-filter-hinweis"
|
|
className={`${FELD} mono`}
|
|
/>
|
|
<span id="quelle-filter-hinweis" className="text-xs text-nebentext">
|
|
Leer lassen nimmt den Standard: aktive Personenkonten. Er schließt deaktivierte Konten aus — wer aus
|
|
dem Ergebnis verschwindet, wird hier deaktiviert und nie gelöscht.
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<label className="flex flex-col gap-1.5 text-sm">
|
|
<span className="text-nebentext">Abgleich alle … Minuten</span>
|
|
<input
|
|
name="abgleichIntervallMinuten"
|
|
type="number"
|
|
min={INTERVALL_MINDESTENS}
|
|
max={INTERVALL_HOECHSTENS}
|
|
defaultValue={quelle?.abgleichIntervallMinuten ?? 60}
|
|
className={FELD}
|
|
/>
|
|
</label>
|
|
<label className="flex min-h-11 items-center gap-2 self-end text-sm text-nebentext md:min-h-10">
|
|
<input
|
|
type="checkbox"
|
|
name="aktiv"
|
|
value="1"
|
|
defaultChecked={quelle?.aktiv ?? false}
|
|
className={KAESTCHEN}
|
|
/>
|
|
Nach Zeitplan abgleichen
|
|
</label>
|
|
</div>
|
|
|
|
{/*
|
|
* Die Attributzuordnung ist einklappbar, aber vollständig da. Vorher stand
|
|
* sie nur in der Datenbank: der Kommentar im Code versprach „wer ein
|
|
* OpenLDAP anbindet, ändert einzelne Zeilen“, und genau das ging über die
|
|
* Oberfläche nicht (Reviewbefund zu #016).
|
|
*/}
|
|
<details open={zuordnungOffen} onToggle={(e) => setzeZuordnungOffen(e.currentTarget.open)}>
|
|
<summary className="flex min-h-11 cursor-pointer list-none items-center text-sm text-nebentext hover:text-text md:min-h-10">
|
|
Attributzuordnung anpassen
|
|
</summary>
|
|
<p className="mt-2 max-w-prose text-xs text-nebentext">
|
|
Die Vorgaben sind die Attributnamen eines Active Directory. Ein OpenLDAP heißt an einigen Stellen
|
|
anders — dort ist die dauerhafte Kennung etwa <span className="mono">entryUUID</span>.
|
|
</p>
|
|
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
|
{ZUORDNUNGS_FELDER.map((feld) => (
|
|
<label key={feld} className="flex flex-col gap-1.5 text-sm">
|
|
<span className="text-nebentext">{ZUORDNUNGS_BESCHRIFTUNG[feld]}</span>
|
|
<input
|
|
name={`attr.${feld}`}
|
|
defaultValue={quelle?.attributZuordnung[feld] ?? AD_ZUORDNUNG[feld]}
|
|
placeholder={AD_ZUORDNUNG[feld]}
|
|
className={`${FELD} mono`}
|
|
/>
|
|
</label>
|
|
))}
|
|
</div>
|
|
<p className="mt-2 max-w-prose text-xs text-nebentext">
|
|
Die Kennung ist das eine Feld, das später nicht mehr geändert werden sollte: an ihr erkennt der
|
|
Abgleich eine Person wieder. Ein anderer Wert legt beim nächsten Lauf alle neu an.
|
|
</p>
|
|
</details>
|
|
|
|
<details open={zertifikatOffen} onToggle={(e) => setzeZertifikatOffen(e.currentTarget.open)}>
|
|
<summary className="flex min-h-11 cursor-pointer list-none items-center text-sm text-nebentext hover:text-text md:min-h-10">
|
|
Eigenes CA-Zertifikat hinterlegen
|
|
</summary>
|
|
{/*
|
|
* Der Zertifikatstext selbst kommt nicht zurück in die Oberfläche — nur
|
|
* die Auskunft, dass eines hinterlegt ist. Ohne diese Zeile sah das
|
|
* leere Feld beim Bearbeiten aus wie „kein Zertifikat“, und der Hinweis
|
|
* darunter behauptete es auch (Reviewbefund zu #016).
|
|
*/}
|
|
{quelle?.caZertifikatVorhanden ? (
|
|
<p className="mt-2 flex items-start gap-2 text-xs text-erledigt">
|
|
<ShieldCheck className="mt-0.5 size-3.5 shrink-0" strokeWidth={2} aria-hidden="true" />
|
|
<span>
|
|
Ein CA-Zertifikat ist hinterlegt. Das Feld unten bleibt leer — Text eintragen ersetzt es, leer
|
|
lassen behält es.
|
|
</span>
|
|
</p>
|
|
) : null}
|
|
<label className="mt-2 flex flex-col gap-1.5 text-sm">
|
|
<span className="text-nebentext">PEM</span>
|
|
<textarea
|
|
name="caZertifikatPem"
|
|
rows={4}
|
|
defaultValue=""
|
|
placeholder="-----BEGIN CERTIFICATE-----"
|
|
className={`${FELD} mono py-2`}
|
|
/>
|
|
<span className="text-xs text-nebentext">
|
|
Nur nötig, wenn der Domänencontroller ein Zertifikat aus einer eigenen CA trägt.
|
|
{quelle?.caZertifikatVorhanden
|
|
? " Ohne hinterlegtes Zertifikat gelten die Wurzelzertifikate des Systems."
|
|
: " Ohne Eintrag gelten die Wurzelzertifikate des Systems."}
|
|
</span>
|
|
</label>
|
|
{quelle?.caZertifikatVorhanden ? (
|
|
<label className="mt-2 flex min-h-11 items-center gap-2 text-sm text-nebentext md:min-h-10">
|
|
<input type="checkbox" name="caZertifikatEntfernen" value="1" className={KAESTCHEN} />
|
|
Hinterlegtes Zertifikat entfernen
|
|
</label>
|
|
) : null}
|
|
</details>
|
|
|
|
{ergebnis.fehler ? (
|
|
<p role="alert" className="max-w-prose text-sm text-fehler">
|
|
{ergebnis.fehler}
|
|
</p>
|
|
) : null}
|
|
{ergebnis.erfolg ? (
|
|
<p role="status" className="max-w-prose text-sm text-erledigt">
|
|
{ergebnis.erfolg}
|
|
</p>
|
|
) : null}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={laeuft}
|
|
className="flex min-h-11 w-fit items-center gap-2 rounded-pille bg-akzent px-5 text-sm font-semibold text-auf-akzent disabled:pointer-events-none disabled:opacity-40 md:min-h-10"
|
|
>
|
|
{bearbeiten ? (
|
|
<Save className="size-[18px]" strokeWidth={2} aria-hidden="true" />
|
|
) : (
|
|
<Server className="size-[18px]" strokeWidth={2} aria-hidden="true" />
|
|
)}
|
|
{laeuft ? "Wird gespeichert …" : bearbeiten ? "Speichern" : "Quelle anlegen"}
|
|
</button>
|
|
</form>
|
|
);
|
|
}
|