Files
sshkeyvault/src/components/personen/MeineSchluessel.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

116 lines
4.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 { Plus, Trash2 } from "lucide-react";
import { useActionState, useEffect, useRef } from "react";
import { useFormularAktion } from "@/components/hosts/useFormularAktion";
import { Button, Feld, Formularfehler, Karte, Textbereich } from "@/components/ui";
import type { FormularZustand } from "@/server/actions/formular";
import {
eigenenKeyEntfernenAktion,
eigenenKeyHinterlegenAktion,
type HinterlegenZustand,
} from "@/server/actions/meine-schluessel";
/** Eigenen Public Key hinterlegen (#013). Nach Erfolg leert sich das Formular. */
export function KeyHinterlegen() {
const formular = useRef<HTMLFormElement>(null);
const { zustand, absenden, laeuft } = useFormularAktion(
eigenenKeyHinterlegenAktion as (v: FormularZustand, f: FormData) => Promise<HinterlegenZustand>,
);
const erfolg = (zustand as HinterlegenZustand).erfolg;
useEffect(() => {
if (erfolg) formular.current?.reset();
}, [erfolg]);
const f = zustand.feldfehler ?? {};
return (
<Karte
titel="Public Key hinterlegen"
untertitel="Eine Zeile im OpenSSH-Format, z. B. aus ~/.ssh/id_ed25519.pub"
>
<form ref={formular} onSubmit={absenden} noValidate className="flex flex-col gap-4">
<Formularfehler text={zustand.fehler} />
{erfolg && (
<p role="status" className="text-ok text-[14px] break-all">
{erfolg}
</p>
)}
<Feld
beschriftung="Bezeichnung"
name="name"
required
maxLength={120}
placeholder="Laptop, Workstation …"
autoComplete="off"
fehler={f["name"]}
/>
<Textbereich
beschriftung="Public Key"
name="publicKey"
required
rows={3}
spellCheck={false}
placeholder="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA… name@rechner"
fehler={f["publicKey"]}
className="[&_textarea]:font-mono [&_textarea]:text-[13px]"
/>
<p className="text-muted text-[13.5px]">
Wirksam erst, wenn ein Operator oder Admin den Schlüssel einem Host zuweist. Den Private Key
behalten Sie bei sich – sshkv fragt nie danach.
</p>
<div>
<Button
type="submit"
variante="primaer"
disabled={laeuft}
icon={<Plus className="size-4" strokeWidth={2.4} />}
>
{laeuft ? "Speichere …" : "Hinterlegen"}
</Button>
</div>
</form>
</Karte>
);
}
/** Eigenen, noch nirgends zugewiesenen Key entfernen – mit Rueckfrage. */
export function KeyEntfernen({ schluesselId, name }: { schluesselId: string; name: string }) {
const dialog = useRef<HTMLDialogElement>(null);
const [zustand, aktion, laeuft] = useActionState<FormularZustand, FormData>(eigenenKeyEntfernenAktion, {});
return (
<>
<Button
variante="gefahr"
klein
icon={<Trash2 className="size-4" />}
onClick={() => dialog.current?.showModal()}
aria-label={`${name} entfernen`}
>
Entfernen
</Button>
<dialog
ref={dialog}
aria-labelledby={`entfernen-${schluesselId}`}
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={schluesselId} />
<h2 id={`entfernen-${schluesselId}`} className="text-[17px] font-semibold">
„{name}“ entfernen?
</h2>
<p className="text-muted text-[14.5px]">
Der Public Key verschwindet aus sshkv. Er ist noch keinem Host zugewiesen; auf keinem Server
ändert sich etwas.
</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 ? "Entferne …" : "Entfernen"}
</Button>
</div>
</form>
</dialog>
</>
);
}