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
+103
View File
@@ -0,0 +1,103 @@
import type { Prisma } from "@/generated/prisma/client";
import { prisma } from "@/lib/db";
/**
* Lesemodelle fuer Personen (#013). Jede angemeldete Rolle darf sie sehen; Schluessel nur mit
* festem `select` ohne Chiffrat-Felder.
*/
export type PersonFilter = { readonly q?: string; readonly archivierte?: boolean };
export async function personenListe(filter: PersonFilter = {}) {
const q = filter.q?.trim().slice(0, 100);
const where: Prisma.PersonWhereInput = {
...(filter.archivierte ? {} : { archiviertAm: null }),
...(q
? {
OR: [
{ name: { contains: q, mode: "insensitive" } },
{ email: { contains: q, mode: "insensitive" } },
{ organisation: { contains: q, mode: "insensitive" } },
],
}
: {}),
};
const personen = await prisma.person.findMany({
where,
orderBy: { name: "asc" },
select: {
id: true,
name: true,
email: true,
organisation: true,
archiviertAm: true,
benutzer: { select: { id: true, name: true } },
_count: { select: { schluessel: { where: { status: "AKTIV" } } } },
},
});
return personen.map(({ _count, ...rest }) => ({ ...rest, aktiveSchluessel: _count.schluessel }));
}
export type PersonListenEintrag = Awaited<ReturnType<typeof personenListe>>[number];
export async function personDetail(id: string) {
const person = await prisma.person.findUnique({
where: { id },
select: {
id: true,
name: true,
email: true,
organisation: true,
notiz: true,
archiviertAm: true,
angelegtAm: true,
benutzerId: true,
benutzer: { select: { id: true, name: true, email: true, rolle: true } },
schluessel: {
orderBy: [{ status: "asc" }, { name: "asc" }],
select: {
id: true,
name: true,
typ: true,
kategorie: true,
status: true,
publicKey: true,
fingerprint: true,
ablaufAm: true,
selbstHinterlegt: true,
_count: {
select: {
publicVerwendungen: { where: { zustand: { not: "ENTFERNT" } } },
privateVerwendungen: { where: { zustand: { not: "ENTFERNT" } } },
},
},
},
},
},
});
if (!person) return null;
return {
...person,
schluessel: person.schluessel.map(({ _count, ...rest }) => ({
...rest,
verwendungen: _count.publicVerwendungen + _count.privateVerwendungen,
})),
};
}
export type PersonDetail = NonNullable<Awaited<ReturnType<typeof personDetail>>>;
/**
* Konten, die sich mit einer Person verknuepfen lassen: noch keiner Person zugeordnet, oder
* der Person `personId` selbst. Der Notfall-Admin ist kein Mensch mit eigenen Keys.
*/
export async function kontenAuswahl(personId?: string) {
return prisma.benutzer.findMany({
where: {
istNotfall: false,
OR: [{ person: null }, ...(personId ? [{ person: { id: personId } }] : [])],
},
orderBy: { name: "asc" },
select: { id: true, name: true, email: true },
});
}