Files
AfiS-Aktensystem/web/app/api/admin/export/route.ts
T
Harlekin e0b03ea336
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Gedankenstriche durch Bindestriche ersetzt
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +02:00

95 lines
3.3 KiB
TypeScript

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<Response> {
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}"`,
},
})
}