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,43 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { fehlerZustand, textfeld, type FormularZustand } from "@/server/actions/formular";
|
||||
import { rechtErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { eigenenPublicKeyHinterlegen, eigenenSchluesselEntfernen } from "@/server/services/meine-schluessel";
|
||||
|
||||
/**
|
||||
* Self-Service (#013). Recht `eigeneSchluesselHinterlegen` hat jede Rolle; der Service
|
||||
* arbeitet ausschliesslich mit der Person des angemeldeten Kontos.
|
||||
*/
|
||||
export type HinterlegenZustand = FormularZustand & { readonly erfolg?: string };
|
||||
|
||||
export async function eigenenKeyHinterlegenAktion(
|
||||
_vorher: HinterlegenZustand,
|
||||
formular: FormData,
|
||||
): Promise<HinterlegenZustand> {
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("eigeneSchluesselHinterlegen");
|
||||
const neu = await eigenenPublicKeyHinterlegen(benutzer, {
|
||||
name: textfeld(formular, "name") ?? "",
|
||||
publicKey: textfeld(formular, "publicKey") ?? "",
|
||||
});
|
||||
revalidatePath("/meine-schluessel");
|
||||
return { erfolg: `Hinterlegt: ${neu.fingerprint}` };
|
||||
} catch (fehler) {
|
||||
return fehlerZustand(fehler);
|
||||
}
|
||||
}
|
||||
|
||||
export async function eigenenKeyEntfernenAktion(
|
||||
_vorher: FormularZustand,
|
||||
formular: FormData,
|
||||
): Promise<FormularZustand> {
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("eigeneSchluesselHinterlegen");
|
||||
await eigenenSchluesselEntfernen(benutzer, textfeld(formular, "id") ?? "");
|
||||
revalidatePath("/meine-schluessel");
|
||||
return {};
|
||||
} catch (fehler) {
|
||||
return fehlerZustand(fehler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import type { PersonEingabe } from "@/lib/personen";
|
||||
import { akteurVon, fehlerZustand, textfeld, type FormularZustand } from "@/server/actions/formular";
|
||||
import { rechtErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { personAnlegen, personArchivieren, personBearbeiten } from "@/server/services/personen";
|
||||
|
||||
/**
|
||||
* Formular-Aktionen fuer Personen (#013). Personen pflegt, wer Schluessel verwaltet (Admin,
|
||||
* Operator) – eine Person ist die Klammer um Personen-Keys.
|
||||
*/
|
||||
|
||||
function personFelder(formular: FormData): PersonEingabe {
|
||||
return {
|
||||
name: textfeld(formular, "name") ?? "",
|
||||
email: textfeld(formular, "email"),
|
||||
organisation: textfeld(formular, "organisation"),
|
||||
notiz: textfeld(formular, "notiz"),
|
||||
benutzerId: textfeld(formular, "benutzerId"),
|
||||
};
|
||||
}
|
||||
|
||||
export async function personAnlegenAktion(
|
||||
_vorher: FormularZustand,
|
||||
formular: FormData,
|
||||
): Promise<FormularZustand> {
|
||||
let id: string;
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("schluesselVerwalten");
|
||||
({ id } = await personAnlegen(personFelder(formular), akteurVon(benutzer)));
|
||||
} catch (fehler) {
|
||||
return fehlerZustand(fehler);
|
||||
}
|
||||
revalidatePath("/personen");
|
||||
redirect(`/personen/${id}`);
|
||||
}
|
||||
|
||||
export async function personBearbeitenAktion(
|
||||
_vorher: FormularZustand,
|
||||
formular: FormData,
|
||||
): Promise<FormularZustand> {
|
||||
const id = textfeld(formular, "id") ?? "";
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("schluesselVerwalten");
|
||||
await personBearbeiten(id, personFelder(formular), akteurVon(benutzer));
|
||||
} catch (fehler) {
|
||||
return fehlerZustand(fehler);
|
||||
}
|
||||
revalidatePath("/personen", "layout");
|
||||
redirect(`/personen/${id}`);
|
||||
}
|
||||
|
||||
export async function personArchivierenAktion(
|
||||
_vorher: FormularZustand,
|
||||
formular: FormData,
|
||||
): Promise<FormularZustand> {
|
||||
const id = textfeld(formular, "id") ?? "";
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("schluesselVerwalten");
|
||||
await personArchivieren(id, akteurVon(benutzer));
|
||||
} catch (fehler) {
|
||||
return fehlerZustand(fehler);
|
||||
}
|
||||
revalidatePath("/personen", "layout");
|
||||
redirect("/personen");
|
||||
}
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
@@ -73,6 +73,8 @@ export async function oidcBenutzerUebernehmen(anspruecke: OidcAnspruecke): Promi
|
||||
const daten = {
|
||||
oidcSub: anspruecke.sub,
|
||||
email: anspruecke.email,
|
||||
// Fuer die Verknuepfung im Self-Service (#013).
|
||||
emailBestaetigt: anspruecke.emailBestaetigt,
|
||||
name: anspruecke.name,
|
||||
rolle,
|
||||
letzteAnmeldungAm: new Date(),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { feldfehlerAus } from "@/lib/hosts";
|
||||
import { personEingabeSchema, type PersonDaten, type PersonEingabe } from "@/lib/personen";
|
||||
import { FachFehler } from "@/server/services/fachfehler";
|
||||
import { istEindeutigkeitsFehler, type Akteur } from "@/server/services/hosts";
|
||||
import { protokollieren } from "@/server/services/protokoll";
|
||||
|
||||
/**
|
||||
* Personen anlegen, bearbeiten, archivieren (#013).
|
||||
*
|
||||
* Archivieren statt Loeschen: an einer Person haengen Schluessel und Protokoll-Bezuege. Die
|
||||
* Schluessel bleiben der archivierten Person zugeordnet – wer bis wann welchen Key hatte, soll
|
||||
* nachvollziehbar bleiben. Widerrufen ist ein eigener Schritt (#024).
|
||||
*/
|
||||
|
||||
function pruefen(eingabe: PersonEingabe): PersonDaten {
|
||||
const ergebnis = personEingabeSchema.safeParse(eingabe);
|
||||
if (!ergebnis.success) {
|
||||
throw new FachFehler("Bitte die markierten Felder prüfen.", feldfehlerAus(ergebnis.error));
|
||||
}
|
||||
return ergebnis.data;
|
||||
}
|
||||
|
||||
function benutzerVergeben(): FachFehler {
|
||||
return new FachFehler("Dieses Konto ist schon mit einer anderen Person verknüpft.", {
|
||||
benutzerId: "Schon mit einer anderen Person verknüpft.",
|
||||
});
|
||||
}
|
||||
|
||||
/** Das gewaehlte Konto muss existieren und frei sein (oder schon zu dieser Person gehoeren). */
|
||||
async function benutzerPruefen(
|
||||
tx: Prisma.TransactionClient,
|
||||
benutzerId: string | null,
|
||||
personId?: string,
|
||||
): Promise<string | null> {
|
||||
if (!benutzerId) return null;
|
||||
const benutzer = await tx.benutzer.findUnique({
|
||||
where: { id: benutzerId },
|
||||
select: { name: true, person: { select: { id: true } } },
|
||||
});
|
||||
if (!benutzer)
|
||||
throw new FachFehler("Das gewählte Konto gibt es nicht.", { benutzerId: "Bitte neu wählen." });
|
||||
if (benutzer.person && benutzer.person.id !== personId) throw benutzerVergeben();
|
||||
return benutzer.name;
|
||||
}
|
||||
|
||||
export async function personAnlegen(eingabe: PersonEingabe, akteur: Akteur) {
|
||||
const daten = pruefen(eingabe);
|
||||
try {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const konto = await benutzerPruefen(tx, daten.benutzerId);
|
||||
const person = await tx.person.create({ data: daten, select: { id: true, name: true } });
|
||||
await protokollieren(
|
||||
{
|
||||
akteur,
|
||||
aktion: "person.angelegt",
|
||||
ergebnis: "ERFOLG",
|
||||
details: { name: daten.name, email: daten.email, organisation: daten.organisation, konto },
|
||||
},
|
||||
tx,
|
||||
);
|
||||
return person;
|
||||
});
|
||||
} catch (fehler) {
|
||||
if (istEindeutigkeitsFehler(fehler)) throw benutzerVergeben();
|
||||
throw fehler;
|
||||
}
|
||||
}
|
||||
|
||||
export async function personBearbeiten(id: string, eingabe: PersonEingabe, akteur: Akteur) {
|
||||
const daten = pruefen(eingabe);
|
||||
const alt = await prisma.person.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
organisation: true,
|
||||
notiz: true,
|
||||
benutzerId: true,
|
||||
archiviertAm: true,
|
||||
},
|
||||
});
|
||||
if (!alt) throw new FachFehler("Diese Person gibt es nicht.");
|
||||
if (alt.archiviertAm) throw new FachFehler("Archivierte Personen lassen sich nicht bearbeiten.");
|
||||
|
||||
const geaendert = Object.fromEntries(
|
||||
(Object.keys(daten) as (keyof PersonDaten)[])
|
||||
.filter((feld) => daten[feld] !== alt[feld])
|
||||
// Die Notiz kann Persoenliches enthalten – im Protokoll steht nur, dass sie sich aenderte.
|
||||
.map((feld) => [feld, feld === "notiz" ? "geändert" : { vorher: alt[feld], nachher: daten[feld] }]),
|
||||
);
|
||||
try {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
await benutzerPruefen(tx, daten.benutzerId, id);
|
||||
const person = await tx.person.update({ where: { id }, data: daten, select: { id: true, name: true } });
|
||||
if (Object.keys(geaendert).length > 0) {
|
||||
await protokollieren(
|
||||
{ akteur, aktion: "person.geaendert", ergebnis: "ERFOLG", details: geaendert },
|
||||
tx,
|
||||
);
|
||||
}
|
||||
return person;
|
||||
});
|
||||
} catch (fehler) {
|
||||
if (istEindeutigkeitsFehler(fehler)) throw benutzerVergeben();
|
||||
throw fehler;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Archiviert eine Person. Die Verknuepfung zum Konto faellt weg, damit der Self-Service beim
|
||||
* naechsten Aufruf nicht wieder bei einer archivierten Person landet.
|
||||
*/
|
||||
export async function personArchivieren(id: string, akteur: Akteur) {
|
||||
const person = await prisma.person.findUnique({
|
||||
where: { id },
|
||||
select: { name: true, archiviertAm: true, _count: { select: { schluessel: true } } },
|
||||
});
|
||||
if (!person) throw new FachFehler("Diese Person gibt es nicht.");
|
||||
if (person.archiviertAm) throw new FachFehler("Diese Person ist bereits archiviert.");
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const archiviert = await tx.person.update({
|
||||
where: { id },
|
||||
data: { archiviertAm: new Date(), benutzerId: null },
|
||||
select: { id: true },
|
||||
});
|
||||
await protokollieren(
|
||||
{
|
||||
akteur,
|
||||
aktion: "person.archiviert",
|
||||
ergebnis: "ERFOLG",
|
||||
details: { name: person.name, schluessel: person._count.schluessel },
|
||||
},
|
||||
tx,
|
||||
);
|
||||
return archiviert;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user