Files
sshkeyvault/src/components/personen/PersonFormular.tsx
T
florian.wildeandClaude Opus 5.5 b5b43f41f6 feat(personen): Personen und Self-Service Meine Schluessel (#013)
Personen lassen sich anlegen, bearbeiten und archivieren; ihr Detail
zeigt ihre Schluessel. Unter "Meine Schluessel" hinterlegt jede Rolle
eigene Public Keys (Freigabe Frage 4), wirksam erst nach Zuweisung
durch einen Operator oder Admin.

Warum so:
- Die eigene Person entsteht beim ersten Aufruf oder wird verknuepft:
  per benutzerId, sonst per E-Mail - aber nur, wenn der OIDC-Anbieter
  die Adresse bestaetigt hat. Dafuer merkt sich Benutzer jetzt
  emailBestaetigt (Migration); sonst koennte jemand mit selbst
  gewaehlter Adresse die Keys einer fremden Person sehen.
- Die Self-Service-Services nehmen nur den angemeldeten Benutzer und
  leiten die Person selbst ab; fremde Schluessel-IDs gelten als nicht
  vorhanden.
- Entfernen nur fuer eigene, selbst hinterlegte Keys ohne Verwendung,
  bedingt geloescht, damit eine gleichzeitige Zuweisung nicht per
  Cascade verschwindet.
- Archivieren statt Loeschen: wer bis wann welchen Key hatte, bleibt
  nachvollziehbar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:38:55 +02:00

152 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { Archive } from "lucide-react";
import { useActionState, useRef } from "react";
import { useFormularAktion } from "@/components/hosts/useFormularAktion";
import { Auswahlliste, Button, Feld, Formularfehler, Karte, Textbereich } from "@/components/ui";
import type { FormularZustand } from "@/server/actions/formular";
import {
personAnlegenAktion,
personArchivierenAktion,
personBearbeitenAktion,
} from "@/server/actions/personen";
export type PersonFormularVorgabe = {
id?: string;
name: string;
email: string;
organisation: string;
notiz: string;
benutzerId: string;
};
export const LEERE_PERSON: PersonFormularVorgabe = {
name: "",
email: "",
organisation: "",
notiz: "",
benutzerId: "",
};
/** Formular fuer neue und bestehende Personen (#013). Regeln prueft der Server. */
export function PersonFormular({
vorgabe,
konten,
abbrechenZiel,
}: {
vorgabe: PersonFormularVorgabe;
konten: { id: string; name: string; email: string | null }[];
abbrechenZiel: string;
}) {
const bearbeiten = Boolean(vorgabe.id);
const { zustand, absenden, laeuft } = useFormularAktion(
bearbeiten ? personBearbeitenAktion : personAnlegenAktion,
);
const f = zustand.feldfehler ?? {};
return (
<form onSubmit={absenden} noValidate className="flex flex-col gap-6">
{vorgabe.id && <input type="hidden" name="id" value={vorgabe.id} />}
<Formularfehler text={zustand.fehler} />
<Karte titel="Person">
<div className="grid gap-4 sm:grid-cols-2">
<Feld
beschriftung="Name"
name="name"
defaultValue={vorgabe.name}
required
maxLength={120}
autoComplete="off"
fehler={f["name"]}
className="sm:col-span-2"
/>
<Feld
beschriftung="E-Mail (optional)"
name="email"
type="email"
defaultValue={vorgabe.email}
maxLength={254}
autoComplete="off"
fehler={f["email"]}
/>
<Feld
beschriftung="Organisation (optional)"
name="organisation"
defaultValue={vorgabe.organisation}
maxLength={120}
placeholder="z. B. Dienstleister"
fehler={f["organisation"]}
/>
<Auswahlliste
beschriftung="Verknüpftes Konto (optional)"
name="benutzerId"
defaultValue={vorgabe.benutzerId}
className="sm:col-span-2"
>
<option value="">keines</option>
{konten.map((k) => (
<option key={k.id} value={k.id}>
{k.email ? `${k.name} (${k.email})` : k.name}
</option>
))}
</Auswahlliste>
{f["benutzerId"] && <p className="text-danger text-[13px] sm:col-span-2">{f["benutzerId"]}</p>}
<Textbereich
beschriftung="Notiz (optional)"
name="notiz"
defaultValue={vorgabe.notiz}
maxLength={1000}
fehler={f["notiz"]}
className="sm:col-span-2"
/>
</div>
</Karte>
<div className="flex flex-wrap gap-2.5">
<Button type="submit" variante="primaer" disabled={laeuft}>
{laeuft ? "Speichere …" : bearbeiten ? "Änderungen speichern" : "Person anlegen"}
</Button>
<Button href={abbrechenZiel}>Abbrechen</Button>
</div>
</form>
);
}
/** Archivieren mit Rueckfrage, nativer `<dialog>` wie bei Hosts (#008). */
export function PersonArchivieren({ personId, name }: { personId: string; name: string }) {
const dialog = useRef<HTMLDialogElement>(null);
const [zustand, aktion, laeuft] = useActionState<FormularZustand, FormData>(personArchivierenAktion, {});
return (
<>
<Button
variante="gefahr"
icon={<Archive className="size-4" />}
onClick={() => dialog.current?.showModal()}
>
Person archivieren
</Button>
<dialog
ref={dialog}
aria-labelledby="person-archivieren-titel"
className="border-border-strong bg-surface text-text rounded-karte m-auto w-[calc(100%-32px)] max-w-md border p-5 backdrop:bg-black/70 sm:p-6"
>
<form action={aktion} className="flex flex-col gap-4">
<input type="hidden" name="id" value={personId} />
<h2 id="person-archivieren-titel" className="text-[17px] font-semibold">
{name} archivieren?
</h2>
<p className="text-muted text-[14.5px]">
Die Person verschwindet aus Listen und Auswahlfeldern, die Verknüpfung zum Konto entfällt. Ihre
Schlüssel bleiben zugeordnet und unverändert – widerrufen ist ein eigener Schritt.
</p>
<Formularfehler text={zustand.fehler} />
<div className="flex flex-wrap justify-end gap-2.5">
<Button onClick={() => dialog.current?.close()}>Abbrechen</Button>
<Button type="submit" variante="gefahr" disabled={laeuft}>
{laeuft ? "Archiviere …" : "Archivieren"}
</Button>
</div>
</form>
</dialog>
</>
);
}