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>
This commit is contained in:
2026-10-12 10:38:55 +02:00
co-authored by Claude Opus 5.5
parent 263143134f
commit b5b43f41f6
22 changed files with 1569 additions and 10 deletions
@@ -0,0 +1,44 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { PersonFormular } from "@/components/personen/PersonFormular";
import { Breadcrumb } from "@/components/ui/Breadcrumb";
import { Seitenkopf } from "@/components/ui/Seitenkopf";
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
import { kontenAuswahl, personDetail } from "@/server/queries/personen";
export const metadata: Metadata = { title: "Person bearbeiten" };
export default async function PersonBearbeiten({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
await sitzungErforderlich("schluesselVerwalten", `/personen/${id}/bearbeiten`);
const [person, konten] = await Promise.all([personDetail(id), kontenAuswahl(id)]);
if (!person || person.archiviertAm) notFound();
return (
<div className="flex max-w-3xl flex-col gap-6 sm:gap-7">
<Seitenkopf
oben={
<Breadcrumb
pfad={[
{ titel: "Personen", href: "/personen" },
{ titel: person.name, href: `/personen/${person.id}` },
{ titel: "Bearbeiten" },
]}
/>
}
titel="Person bearbeiten"
/>
<PersonFormular
vorgabe={{
id: person.id,
name: person.name,
email: person.email ?? "",
organisation: person.organisation ?? "",
notiz: person.notiz ?? "",
benutzerId: person.benutzerId ?? "",
}}
konten={konten}
abbrechenZiel={`/personen/${person.id}`}
/>
</div>
);
}