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
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
95 lines
3.3 KiB
TypeScript
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}"`,
|
|
},
|
|
})
|
|
}
|