import { NextResponse } from "next/server"; import { CSV_BOM, csvZeile } from "@/lib/csv"; import { PROTOKOLL_CSV_KOPF, protokollCsvWerte, protokollFilterAus } from "@/lib/protokoll-filter"; import { RechteFehler, rechtErforderlich } from "@/server/auth/sitzungspflicht"; import { protokollStapelweise } from "@/server/queries/protokoll"; import { protokollieren } from "@/server/services/protokoll"; /** * Protokoll als CSV (#027) mit denselben Filtern wie die Seite. Jede Rolle darf lesen; der * Handler prueft das selbst. * * Gestreamt: die Datenbank liefert Stapel zu 500 Zeilen, jeder Stapel geht sofort hinaus. * Ein Protokoll mit Hunderttausenden Eintraegen muss so nie ganz in den Speicher. */ export async function GET(anfrage: Request) { let benutzer; try { ({ benutzer } = await rechtErforderlich("lesen")); } catch (fehler) { if (fehler instanceof RechteFehler) return NextResponse.json({ fehler: fehler.message }, { status: 403 }); throw fehler; } const filter = protokollFilterAus(Object.fromEntries(new URL(anfrage.url).searchParams)); // Vor dem ersten Byte: wer das Protokoll mitnimmt, steht selbst darin – und scheitert das // Schreiben, gibt es auch keinen Export. await protokollieren({ akteur: { id: benutzer.id, name: benutzer.name }, aktion: "protokoll.exportiert", ergebnis: "ERFOLG", details: { filter: Object.fromEntries(Object.entries(filter).filter(([, wert]) => wert)) }, }); const kodierer = new TextEncoder(); const stapel = protokollStapelweise(filter); const strom = new ReadableStream({ start(steuerung) { steuerung.enqueue(kodierer.encode(CSV_BOM + csvZeile(PROTOKOLL_CSV_KOPF) + "\r\n")); }, // pull statt Schleife in start: der naechste Stapel wird erst gelesen, wenn der Client // den vorigen abgenommen hat (Gegendruck). async pull(steuerung) { try { const { value, done } = await stapel.next(); if (done) { steuerung.close(); return; } steuerung.enqueue( kodierer.encode(value.map((z) => csvZeile(protokollCsvWerte(z)) + "\r\n").join("")), ); } catch (fehler) { steuerung.error(fehler); } }, async cancel() { await stapel.return(undefined); }, }); const tag = new Date().toISOString().slice(0, 10); return new NextResponse(strom, { headers: { "Content-Type": "text/csv; charset=utf-8", "Content-Disposition": `attachment; filename="sshkv-protokoll-${tag}.csv"`, "Cache-Control": "no-store", }, }); }