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:
@@ -0,0 +1,195 @@
|
||||
import type { Benutzer } from "@/generated/prisma/client";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { feldfehlerAus } from "@/lib/hosts";
|
||||
import { eigenerKeySchema, type EigenerKeyEingabe } from "@/lib/personen";
|
||||
import { FachFehler } from "@/server/services/fachfehler";
|
||||
import { istEindeutigkeitsFehler } from "@/server/services/hosts";
|
||||
import { protokollieren } from "@/server/services/protokoll";
|
||||
import { publicKeyAnlegen } from "@/server/services/schluessel";
|
||||
|
||||
/**
|
||||
* Self-Service „Meine Schluessel“ (#013, Freigabe Frage 4): jeder angemeldete Benutzer
|
||||
* hinterlegt eigene Public Keys. Wirksam werden sie erst, wenn ein Operator oder Admin sie
|
||||
* einem Host zuweist – hier wird nichts ausgerollt.
|
||||
*
|
||||
* Jede Funktion nimmt den angemeldeten Benutzer und arbeitet nur mit **seiner** Person; eine
|
||||
* Schluessel-ID von aussen wird immer gegen sie geprueft.
|
||||
*/
|
||||
type Konto = Pick<Benutzer, "id" | "name" | "email" | "emailBestaetigt">;
|
||||
|
||||
/**
|
||||
* Liefert die Person des Kontos und legt sie beim ersten Aufruf an. Verknuepft per
|
||||
* `benutzerId`, sonst per E-Mail – aber nur, wenn der Anbieter die Adresse bestaetigt hat und
|
||||
* genau eine freie, nicht archivierte Person dazu passt.
|
||||
*/
|
||||
export async function eigenePersonSicherstellen(konto: Konto): Promise<{ id: string; name: string }> {
|
||||
const vorhanden = await prisma.person.findUnique({
|
||||
where: { benutzerId: konto.id },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
if (vorhanden) return vorhanden;
|
||||
|
||||
const akteur = { id: konto.id, name: konto.name };
|
||||
try {
|
||||
if (konto.email && konto.emailBestaetigt) {
|
||||
const passend = await prisma.person.findMany({
|
||||
where: {
|
||||
email: { equals: konto.email, mode: "insensitive" },
|
||||
benutzerId: null,
|
||||
archiviertAm: null,
|
||||
},
|
||||
select: { id: true },
|
||||
take: 2,
|
||||
});
|
||||
// Bedingt: hat inzwischen jemand anderes die Person verknuepft, aendert sich nichts,
|
||||
// und das Konto bekommt unten eine eigene Person.
|
||||
const verknuepft =
|
||||
passend.length === 1
|
||||
? await prisma.person.updateMany({
|
||||
where: { id: passend[0]!.id, benutzerId: null },
|
||||
data: { benutzerId: konto.id },
|
||||
})
|
||||
: { count: 0 };
|
||||
if (verknuepft.count === 1) {
|
||||
const person = await prisma.person.findUniqueOrThrow({
|
||||
where: { benutzerId: konto.id },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
await protokollieren({
|
||||
akteur,
|
||||
aktion: "person.verknuepft",
|
||||
ergebnis: "ERFOLG",
|
||||
details: { person: person.name, ueber: "email" },
|
||||
});
|
||||
return person;
|
||||
}
|
||||
}
|
||||
const person = await prisma.person.create({
|
||||
data: { name: konto.name, email: konto.email, benutzerId: konto.id },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
await protokollieren({
|
||||
akteur,
|
||||
aktion: "person.angelegt",
|
||||
ergebnis: "ERFOLG",
|
||||
details: { name: konto.name, ueber: "selbstauskunft" },
|
||||
});
|
||||
return person;
|
||||
} catch (fehler) {
|
||||
// Zwei gleichzeitige erste Aufrufe: der zweite findet die Person des ersten.
|
||||
if (istEindeutigkeitsFehler(fehler)) {
|
||||
return prisma.person.findUniqueOrThrow({
|
||||
where: { benutzerId: konto.id },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
}
|
||||
throw fehler;
|
||||
}
|
||||
}
|
||||
|
||||
/** Die eigenen Schluessel – mit genau den Feldern, die die Seite zeigt. */
|
||||
export async function eigeneSchluessel(konto: Konto) {
|
||||
const person = await eigenePersonSicherstellen(konto);
|
||||
const schluessel = await prisma.schluessel.findMany({
|
||||
where: { personId: person.id },
|
||||
orderBy: { angelegtAm: "desc" },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
typ: true,
|
||||
status: true,
|
||||
publicKey: true,
|
||||
fingerprint: true,
|
||||
kommentar: true,
|
||||
selbstHinterlegt: true,
|
||||
angelegtAm: true,
|
||||
_count: {
|
||||
select: {
|
||||
publicVerwendungen: { where: { zustand: { not: "ENTFERNT" } } },
|
||||
privateVerwendungen: { where: { zustand: { not: "ENTFERNT" } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return {
|
||||
person,
|
||||
schluessel: schluessel.map(({ _count, ...rest }) => ({
|
||||
...rest,
|
||||
verwendungen: _count.publicVerwendungen + _count.privateVerwendungen,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export async function eigenenPublicKeyHinterlegen(konto: Konto, eingabe: EigenerKeyEingabe) {
|
||||
const ergebnis = eigenerKeySchema.safeParse(eingabe);
|
||||
if (!ergebnis.success) {
|
||||
throw new FachFehler("Bitte die markierten Felder prüfen.", feldfehlerAus(ergebnis.error));
|
||||
}
|
||||
const person = await eigenePersonSicherstellen(konto);
|
||||
return publicKeyAnlegen(
|
||||
{
|
||||
name: ergebnis.data.name,
|
||||
slug: null,
|
||||
kategorie: "PERSON",
|
||||
publicKey: ergebnis.data.publicKey,
|
||||
kommentar: null,
|
||||
tags: [],
|
||||
ablaufAm: null,
|
||||
personId: person.id,
|
||||
vorlageId: null,
|
||||
selbstHinterlegt: true,
|
||||
},
|
||||
{ id: konto.id, name: konto.name },
|
||||
"schluessel.selbst_hinterlegt",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt einen eigenen, selbst hinterlegten Key, solange er nirgends zugewiesen ist – auch
|
||||
* keine entfernte Verwendung, damit der Verlauf am Key nicht verloren geht. Fremde Keys sind
|
||||
* fuer diese Funktion „nicht vorhanden“, damit sie nichts ueber fremde IDs verraet.
|
||||
*/
|
||||
export async function eigenenSchluesselEntfernen(konto: Konto, schluesselId: string): Promise<void> {
|
||||
const person = await eigenePersonSicherstellen(konto);
|
||||
const schluessel = await prisma.schluessel.findFirst({
|
||||
where: { id: schluesselId, personId: person.id },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
fingerprint: true,
|
||||
selbstHinterlegt: true,
|
||||
_count: { select: { publicVerwendungen: true, privateVerwendungen: true } },
|
||||
},
|
||||
});
|
||||
if (!schluessel) throw new FachFehler("Diesen Schlüssel gibt es unter Ihren Schlüsseln nicht.");
|
||||
if (!schluessel.selbstHinterlegt) {
|
||||
throw new FachFehler(
|
||||
"Diesen Schlüssel hat ein Operator oder Admin angelegt – bitte dort entfernen lassen.",
|
||||
);
|
||||
}
|
||||
if (schluessel._count.publicVerwendungen + schluessel._count.privateVerwendungen > 0) {
|
||||
throw new FachFehler(
|
||||
"Der Schlüssel ist schon einem Host zugewiesen. Bitte einen Operator oder Admin bitten.",
|
||||
);
|
||||
}
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Bedingt geloescht: weist ein Operator den Key gerade zu, darf das Loeschen die neue
|
||||
// Verwendung nicht per Cascade mitnehmen.
|
||||
const geloescht = await tx.schluessel.deleteMany({
|
||||
where: { id: schluessel.id, publicVerwendungen: { none: {} }, privateVerwendungen: { none: {} } },
|
||||
});
|
||||
if (geloescht.count !== 1) {
|
||||
throw new FachFehler("Der Schlüssel wurde gerade zugewiesen und bleibt deshalb bestehen.");
|
||||
}
|
||||
await protokollieren(
|
||||
{
|
||||
akteur: { id: konto.id, name: konto.name },
|
||||
aktion: "schluessel.selbst_entfernt",
|
||||
ergebnis: "ERFOLG",
|
||||
fingerprint: schluessel.fingerprint,
|
||||
details: { name: schluessel.name },
|
||||
},
|
||||
tx,
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user