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>
154 lines
6.1 KiB
TypeScript
154 lines
6.1 KiB
TypeScript
import { KeyRound, Pencil, Plus } from "lucide-react";
|
||
import type { Metadata } from "next";
|
||
import Link from "next/link";
|
||
import { notFound } from "next/navigation";
|
||
import type { ReactNode } from "react";
|
||
import { PersonArchivieren } from "@/components/personen/PersonFormular";
|
||
import { AblaufBadge, KategorieBadge, StatusBadge, TypBadge } from "@/components/schluessel/Anzeige";
|
||
import { Badge } from "@/components/ui/Badge";
|
||
import { Breadcrumb } from "@/components/ui/Breadcrumb";
|
||
import { Button } from "@/components/ui/Button";
|
||
import { Karte } from "@/components/ui/Karte";
|
||
import { Leerzustand } from "@/components/ui/Leerzustand";
|
||
import { Seitenkopf } from "@/components/ui/Seitenkopf";
|
||
import { datum } from "@/lib/format";
|
||
import { fingerprintKurz } from "@/lib/schluessel";
|
||
import { darf } from "@/server/auth/rechte";
|
||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||
import { personDetail } from "@/server/queries/personen";
|
||
|
||
export const metadata: Metadata = { title: "Person" };
|
||
|
||
function Info({ titel, children }: { titel: string; children: ReactNode }) {
|
||
return (
|
||
<div className="min-w-0">
|
||
<dt className="text-muted text-[13px] font-medium">{titel}</dt>
|
||
<dd className="text-text mt-1 text-[14.5px] break-words">{children}</dd>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** Persondetail (#013) mit ihren Schlüsseln. */
|
||
export default async function PersonSeite({ params }: { params: Promise<{ id: string }> }) {
|
||
const { id } = await params;
|
||
const { benutzer } = await sitzungErforderlich("lesen", `/personen/${id}`);
|
||
const person = await personDetail(id);
|
||
if (!person) notFound();
|
||
const archiviert = person.archiviertAm !== null;
|
||
const verwalten = darf(benutzer.rolle, "schluesselVerwalten") && !archiviert;
|
||
const jetzt = new Date();
|
||
|
||
return (
|
||
<div className="flex flex-col gap-6 sm:gap-7">
|
||
<Seitenkopf
|
||
oben={<Breadcrumb pfad={[{ titel: "Personen", href: "/personen" }, { titel: person.name }]} />}
|
||
titel={person.name}
|
||
badges={
|
||
<>
|
||
{archiviert && person.archiviertAm && (
|
||
<Badge variante="gefahr">archiviert am {datum(person.archiviertAm)}</Badge>
|
||
)}
|
||
{person.organisation && <Badge>{person.organisation}</Badge>}
|
||
{person.benutzer && <Badge variante="ok">Konto verknüpft</Badge>}
|
||
</>
|
||
}
|
||
aktionen={
|
||
verwalten && (
|
||
<Button href={`/personen/${person.id}/bearbeiten`} icon={<Pencil className="size-4" />}>
|
||
Bearbeiten
|
||
</Button>
|
||
)
|
||
}
|
||
/>
|
||
|
||
<Karte as="div">
|
||
<dl className="grid grid-cols-1 gap-x-6 gap-y-4 min-[480px]:grid-cols-2 xl:grid-cols-3">
|
||
<Info titel="E-Mail">{person.email ?? <span className="text-muted">–</span>}</Info>
|
||
<Info titel="Konto">
|
||
{person.benutzer ? (
|
||
<>
|
||
{person.benutzer.name}
|
||
{person.benutzer.email && (
|
||
<span className="text-muted block text-[13px]">{person.benutzer.email}</span>
|
||
)}
|
||
</>
|
||
) : (
|
||
<span className="text-muted">nicht verknüpft</span>
|
||
)}
|
||
</Info>
|
||
<Info titel="Angelegt">{datum(person.angelegtAm)}</Info>
|
||
</dl>
|
||
{person.notiz && (
|
||
<p className="border-border text-muted mt-5 border-t pt-4 text-[14px] whitespace-pre-line">
|
||
{person.notiz}
|
||
</p>
|
||
)}
|
||
</Karte>
|
||
|
||
<Karte
|
||
titel="Schlüssel"
|
||
untertitel={`${person.schluessel.length} Schlüssel dieser Person`}
|
||
aktionen={
|
||
verwalten && (
|
||
<Button
|
||
href={`/schluessel/neu?person=${person.id}`}
|
||
klein
|
||
icon={<Plus className="size-4" strokeWidth={2.4} />}
|
||
>
|
||
Public Key hinterlegen
|
||
</Button>
|
||
)
|
||
}
|
||
>
|
||
{person.schluessel.length === 0 ? (
|
||
<Leerzustand
|
||
icon={<KeyRound className="size-5" />}
|
||
titel="Noch keine Schlüssel"
|
||
text="Die Person kann ihre Public Keys auch selbst unter „Meine Schlüssel“ hinterlegen, sobald ihr Konto verknüpft ist."
|
||
/>
|
||
) : (
|
||
<ul className="flex flex-col">
|
||
{person.schluessel.map((s) => (
|
||
<li key={s.id} className="border-border border-b py-3 first:pt-0 last:border-b-0 last:pb-0">
|
||
<Link
|
||
href={`/schluessel/${s.id}`}
|
||
className="text-text hover:text-accent text-[14.5px] font-medium transition-colors"
|
||
>
|
||
{s.name}
|
||
</Link>
|
||
<span className="text-muted block font-mono text-[12.5px]">
|
||
{fingerprintKurz(s.fingerprint)}
|
||
</span>
|
||
<span className="mt-1.5 flex flex-wrap items-center gap-1.5">
|
||
<TypBadge typ={s.typ} publicKey={s.publicKey} />
|
||
<KategorieBadge kategorie={s.kategorie} />
|
||
<StatusBadge status={s.status} />
|
||
{s.selbstHinterlegt && <Badge>selbst hinterlegt</Badge>}
|
||
<AblaufBadge ablaufAm={s.ablaufAm} jetzt={jetzt} />
|
||
<span className="text-muted text-[13px]">
|
||
{s.verwendungen === 0
|
||
? "· noch keinem Host zugewiesen"
|
||
: `· ${s.verwendungen} ${s.verwendungen === 1 ? "Verwendung" : "Verwendungen"}`}
|
||
</span>
|
||
</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</Karte>
|
||
|
||
{verwalten && (
|
||
<Karte titel="Archivieren" untertitel="Person stilllegen, ohne die Historie zu verlieren">
|
||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||
<p className="text-muted max-w-xl text-[14px]">
|
||
Archivierte Personen verschwinden aus Listen. Ihre Schlüssel bleiben bestehen und lassen sich
|
||
weiterhin widerrufen.
|
||
</p>
|
||
<PersonArchivieren personId={person.id} name={person.name} />
|
||
</div>
|
||
</Karte>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|