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>
196 lines
6.9 KiB
TypeScript
196 lines
6.9 KiB
TypeScript
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,
|
||
);
|
||
});
|
||
}
|