Phase 11: Freigabereife (#16)
Pruefung / pruefung (push) Canceled after 0s
Pruefung / oberflaeche (push) Canceled after 0s

This commit was merged in pull request #16.
This commit is contained in:
2026-09-06 20:05:49 +00:00
parent 91742a0dd7
commit 14cc4de9b8
21 changed files with 2495 additions and 8 deletions
+84
View File
@@ -3,6 +3,9 @@ import { TRPCError } from "@trpc/server";
import { Dienstgrad, Prisma } from "@prisma/client";
import { router, angemeldet, mitRecht } from "@/server/trpc";
import { AuditAktion, protokolliereIn } from "@/lib/audit";
import { auskunftSammeln, auskunftUmfang } from "@/lib/dsgvo/auskunft";
import { auskunftPdf } from "@/lib/dsgvo/bericht";
import { hochladen, objektSchluessel } from "@/lib/storage";
import { darfPersonSehen, KeinZugriffFehler } from "@/lib/authz";
import {
blaettern,
@@ -55,6 +58,87 @@ const personDaten = z.object({
});
export const personRouter = router({
/**
* Auskunft nach Art. 15 DSGVO.
*
* Eine Mutation, obwohl sie im Kern nur liest — und das mit Absicht: das
* Erstellen der Auskunft **ist** die Offenlegung, und die gehört ins
* Protokoll. Als Abfrage würde sie zwischengespeichert, mehrfach ausgeführt
* und wäre entweder nicht oder zwanzigmal vermerkt.
*
* Ein Durchgang liefert alles auf einmal: die Angaben für die Anzeige, das
* PDF für die Akte und die JSON-Fassung nach Art. 20. Wer eine Auskunft
* verlangt, bekommt sie vollständig oder gar nicht — nicht in drei Schritten,
* von denen zwei vergessen werden können.
*/
auskunft: mitRecht("person.export")
.input(idEingabe)
.mutation(async ({ ctx, input }) => {
const auskunft = await ctx.db((db) => auskunftSammeln(db, input.id));
const organisation = await ctx.db((db) =>
db.organisation.findUnique({
where: { id: ctx.sitzung.organisationId },
select: { slug: true },
}),
);
const pdf = await auskunftPdf(auskunft);
const schluessel = objektSchluessel({
organisationSlug: organisation?.slug ?? ctx.sitzung.organisationId,
entitaet: "auskunft",
entitaetId: input.id,
dateiname: `auskunft-${auskunft.erstelltAm.toISOString().slice(0, 10)}.pdf`,
});
const abgelegt = await hochladen(schluessel, pdf, "application/pdf", {
kategorie: "dsgvo-auskunft",
person: input.id,
});
const datei = await ctx.db(async (db) => {
const angelegt = await db.datei.create({
data: {
organisationId: ctx.sitzung.organisationId,
bucket: abgelegt.bucket,
objektSchluessel: abgelegt.objektSchluessel,
dateiname: `Auskunft ${auskunft.person.name}.pdf`,
mimeTyp: "application/pdf",
groesseBytes: BigInt(pdf.byteLength),
pruefsumme: abgelegt.pruefsumme,
hochgeladenVonId: ctx.sitzung.personId,
},
});
// Die Offenlegung selbst — mit Umfang, damit später nachvollziehbar
// ist, was ausgehändigt wurde, ohne die Daten zu wiederholen.
await protokolliereIn(db, {
organisationId: ctx.sitzung.organisationId,
sitzung: ctx.sitzung,
aktion: AuditAktion.EXPORTIERT,
entitaet: "Person",
entitaetId: input.id,
beschreibung:
`Datenauskunft nach Art. 15 DSGVO für ${auskunft.person.name} erstellt `
+ `(${auskunftUmfang(auskunft)} Angaben in ${auskunft.abschnitte.length} Abschnitten)`,
nachher: {
abschnitte: auskunft.abschnitte.length,
angaben: auskunftUmfang(auskunft),
dateiId: angelegt.id,
},
});
return angelegt;
});
return {
auskunft,
umfang: auskunftUmfang(auskunft),
dateiId: datei.id,
};
}),
liste: mitRecht("person.lesen")
.input(
listenEingabe(SORTIERFELDER).extend({