Files
sshkeyvault/src/server/services/meine-schluessel.ts
T
florian.wildeandClaude Opus 5.5 b5b43f41f6 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>
2026-10-12 10:38:55 +02:00

196 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
);
});
}