1 Commits

Author SHA1 Message Date
CTO (LegalAI)
5ff2347aac 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 <noreply@paperclip.ing>
2026-04-10 20:28:18 +00:00
3 changed files with 89 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
// DELETE /api/documents/:id — delete a document and its stored file
import { type NextRequest } from 'next/server';
import { deleteDocument } from '@/lib/documents';
import { logAuditEvent } from '@/lib/auth/audit';
import { requirePermission } from '@/lib/auth/rbac';
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

@@ -49,6 +49,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[]>([]);
@@ -109,6 +110,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];
@@ -196,6 +219,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>
))}
</div>

View File

@@ -222,3 +222,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;
}