feat: add document deletion endpoint and UI button (AIIA-70)
All checks were successful
Deploy to VPS / deploy (push) Successful in 34s

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 <noreply@paperclip.ing>
This commit is contained in:
CTO (LegalAI)
2026-04-10 20:28:18 +00:00
parent 4e74e4b5c9
commit 17c1b6587a
3 changed files with 87 additions and 2 deletions

View File

@@ -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 });
}

View File

@@ -121,6 +121,7 @@ export default function DokumentUpload({
label = 'Dokument hochladen',
}: DokumentUploadProps) {
const [uploading, setUploading] = useState(false);
const [deleting, setDeleting] = useState<string | null>(null);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [documents, setDocuments] = useState<DocumentItem[]>([]);
@@ -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}
</span>
<button
type="button"
onClick={() => handleDelete(doc.id, doc.filename)}
disabled={deleting === doc.id}
className="ml-2 text-xs text-danger hover:text-danger/80 transition-colors disabled:opacity-50 shrink-0"
title="Dokument loeschen"
>
{deleting === doc.id ? '...' : 'Loeschen'}
</button>
</div>
{/* Show progress for non-extracted documents or if debug is on */}

View File

@@ -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;
}