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; /** * 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 { 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, ); }); }