import { NextResponse } from 'next/server' import { db } from '@/lib/db' import { getCurrentUser } from '@/lib/auth/current' import { AccessError } from '@/lib/errors' import { exportDatabase } from '@/lib/export/database' import { createZip } from '@/lib/export/zip' import { absolutePath } from '@/lib/files/paths' import { readFile } from 'node:fs/promises' import { writeAudit } from '@/lib/audit/write' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' /** * Die beiden Wege für Administratoren aus docs/08-suche-und-export.md: * * GET /api/admin/export?kind=database vollständiger JSON-Abzug * GET /api/admin/export?kind=bundle ZIP mit den fertigen Exporten * * Beide nur mit Recht `admin`, beide protokolliert. Der JSON-Abzug ist die * einzige Stelle im System, an der die Klassifizierung nicht greift - deshalb * liegt er hinter einem eigenen Endpunkt und nicht hinter einem Knopf an einer * Akte. */ export async function GET(request: Request): Promise { const user = await getCurrentUser() // Kein 403: wer das Recht nicht hat, für den gibt es den Endpunkt nicht. if (!user?.permissions.isAdmin) return new NextResponse(null, { status: 404 }) const kind = new URL(request.url).searchParams.get('kind') try { if (kind === 'database') { const abzug = await exportDatabase(user.permissions) return datei(abzug, 'application/json', `afis-abzug-${heute()}.json`) } if (kind === 'bundle') { const fertige = await db.exportJob.findMany({ where: { status: 'READY', storageKey: { not: null } }, orderBy: { finishedAt: 'desc' }, take: 100, select: { id: true, storageKey: true, kind: true }, }) const eintraege: { name: string; content: Uint8Array }[] = [] for (const auftrag of fertige) { try { eintraege.push({ name: `${auftrag.kind.toLowerCase()}-${auftrag.id}.pdf`, content: new Uint8Array(await readFile(absolutePath(auftrag.storageKey!))), }) } catch { // Eine fehlende Datei hält den Sammelexport nicht auf. } } await db.$transaction(async (tx) => { await writeAudit(tx, { actorId: user.id, action: 'export.pdf', targetType: 'ExportJob', targetId: 'bundle', clearanceAtTime: user.permissions.clearanceLevel, meta: { files: eintraege.length }, }) }) return datei(createZip(eintraege), 'application/zip', `afis-sammelexport-${heute()}.zip`) } return new NextResponse(null, { status: 404 }) } catch (error) { if (error instanceof AccessError) return new NextResponse(null, { status: 404 }) console.error('[export] Adminexport fehlgeschlagen', error) return NextResponse.json({ error: 'unbekannt' }, { status: 500 }) } } function heute(): string { return new Date().toISOString().slice(0, 10) } function datei(inhalt: Uint8Array, typ: string, name: string): Response { return new NextResponse(inhalt as unknown as BodyInit, { headers: { 'Content-Type': typ, 'Content-Length': String(inhalt.byteLength), 'X-Content-Type-Options': 'nosniff', 'Cache-Control': 'private, no-store', 'Content-Disposition': `attachment; filename="${name}"`, }, }) }