From 17c1b6587a9def23f96440a65ec366d00aaf40ed Mon Sep 17 00:00:00 2001 From: "CTO (LegalAI)" Date: Fri, 10 Apr 2026 20:28:18 +0000 Subject: [PATCH] feat: add document deletion endpoint and UI button (AIIA-70) Add DELETE /api/documents/:id endpoint that removes the DB record, cleans up the stored file from disk, and logs an audit event. Add a "Loeschen" button to the DokumentUpload component with confirmation dialog. Co-Authored-By: Paperclip --- src/app/api/documents/[id]/route.ts | 34 ++++++++++++++++++-- src/components/documents/dokument-upload.tsx | 32 ++++++++++++++++++ src/lib/documents/index.ts | 23 +++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/app/api/documents/[id]/route.ts b/src/app/api/documents/[id]/route.ts index 5108e92..bbd0aa5 100644 --- a/src/app/api/documents/[id]/route.ts +++ b/src/app/api/documents/[id]/route.ts @@ -1,7 +1,9 @@ -// GET /api/documents/:id — get document status and metadata (used for polling) +// GET /api/documents/:id — get document status and metadata (used for polling) +// DELETE /api/documents/:id — delete a document and its stored file import { type NextRequest } from 'next/server'; -import { getDocument } from '@/lib/documents'; +import { getDocument, deleteDocument } from '@/lib/documents'; +import { logAuditEvent } from '@/lib/auth/audit'; import { requirePermission } from '@/lib/auth/rbac'; export async function GET( @@ -32,3 +34,31 @@ export async function GET( updatedAt: doc.updatedAt, }); } + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + const auth = await requirePermission('cases:edit'); + if ('response' in auth) return auth.response; + const { ctx } = auth; + + const { id } = await params; + + const deleted = await deleteDocument(ctx.tenantId, id); + + if (!deleted) { + return Response.json( + { error: 'Dokument nicht gefunden.' }, + { status: 404 }, + ); + } + + const ip = + request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? + request.headers.get('x-real-ip') ?? + undefined; + await logAuditEvent(ctx, 'delete', 'document', id, { filename: deleted.filename }, ip); + + return Response.json({ deleted: true }); +} diff --git a/src/components/documents/dokument-upload.tsx b/src/components/documents/dokument-upload.tsx index e304620..54be5e9 100644 --- a/src/components/documents/dokument-upload.tsx +++ b/src/components/documents/dokument-upload.tsx @@ -121,6 +121,7 @@ export default function DokumentUpload({ label = 'Dokument hochladen', }: DokumentUploadProps) { const [uploading, setUploading] = useState(false); + const [deleting, setDeleting] = useState(null); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [documents, setDocuments] = useState([]); @@ -204,6 +205,28 @@ export default function DokumentUpload({ } } + async function handleDelete(docId: string, filename: string) { + if (!confirm(`"${filename}" wirklich loeschen?`)) return; + + setError(''); + setSuccess(''); + setDeleting(docId); + + try { + const res = await fetch(`/api/documents/${docId}`, { method: 'DELETE' }); + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || 'Loeschen fehlgeschlagen'); + } + setSuccess(`"${filename}" wurde geloescht.`); + fetchDocuments(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten'); + } finally { + setDeleting(null); + } + } + function handleSubmit(e: React.FormEvent) { e.preventDefault(); const file = fileRef.current?.files?.[0]; @@ -305,6 +328,15 @@ export default function DokumentUpload({ > {STATUS_LABELS[doc.status] ?? doc.status} + {/* Show progress for non-extracted documents or if debug is on */} diff --git a/src/lib/documents/index.ts b/src/lib/documents/index.ts index 96bc0e3..7a494f5 100644 --- a/src/lib/documents/index.ts +++ b/src/lib/documents/index.ts @@ -258,3 +258,26 @@ export async function getDocument(tenantId: string, documentId: string) { return doc ?? null; }); } + +/** + * Delete a document by ID. Removes the DB record and the stored file from disk. + * Returns the deleted document row, or null if not found. + */ +export async function deleteDocument(tenantId: string, documentId: string) { + const deleted = await withTenantDb(tenantId, async (tdb) => { + const [row] = await tdb + .delete(documents) + .where(eq(documents.id, documentId)) + .returning(); + return row ?? null; + }); + + if (deleted?.storagePath) { + const fs = await import('node:fs/promises'); + await fs.unlink(deleted.storagePath).catch(() => { + // File may already be removed — ignore cleanup errors + }); + } + + return deleted; +}