Hygiene R140: Datei-Loesch-Helfer konsolidieren + DB-vor-Datei

Pentester-Hygiene zu a6b1dac:

1) Konsolidierung: neuer utils/fileCleanup.ts mit deleteFileAbsolute
   (absoluter Pfad, z.B. Multer-Temp) + deleteUploadByRelativePath
   (in DB gespeicherter /uploads/-Pfad). Ersetzt die 3x kopierten
   deleteFileIfExists/cleanupFile in creditNote-, upload- und
   customer-Service.

2) Reihenfolge: In deleteCreditNote/updateCreditNote erst die DB-
   Operation, DANN die Datei loeschen. Schlaegt der DB-Schritt fehl,
   bleibt die Datei erhalten (kein ins-Leere-zeigender Zustand).

Verifiziert: Update -> pdfPath null + alte Datei weg; Delete -> gibt
geloeschte Row zurueck (Audit) + Datei weg. Kein Regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-08 22:57:40 +02:00
co-authored by Claude Opus 4.8
parent c29ffd7bea
commit 3e2d9395a7
5 changed files with 75 additions and 69 deletions
+13 -24
View File
@@ -2,26 +2,12 @@
// CRUD für Vertrags-Gutschriften (Subventionen: Geld/Sachwert) inkl.
// USt-Berechnung (pro Gutschrift wählbar: vatRelevant + Basis Netto/Brutto).
import fs from 'fs';
import path from 'path';
import prisma from '../lib/prisma.js';
import { ApiError } from '../utils/apiError.js';
import { assignNextNumber } from './creditNoteNumberRange.service.js';
import { deleteUploadByRelativePath } from '../utils/fileCleanup.js';
import { CreditNoteType, CreditNoteCustomerType, CreditNoteAmountBasis } from '@prisma/client';
// Löscht eine hochgeladene/erzeugte Datei von der Platte (best effort).
function deleteFileIfExists(filePath: string | null) {
if (!filePath) return;
const absolute = path.join(process.cwd(), filePath);
if (fs.existsSync(absolute)) {
try {
fs.unlinkSync(absolute);
} catch (error) {
console.error('Fehler beim Löschen der Gutschrift-Datei:', absolute, error);
}
}
}
const round2 = (n: number) => Math.round((n + Number.EPSILON) * 100) / 100;
export interface AmountResult {
@@ -245,10 +231,11 @@ export async function updateCreditNote(id: number, input: CreateCreditNoteInput)
}
// Nummer bleibt unverändert (einmal vergeben = fix). Ein evtl. schon
// erzeugtes PDF ist nach inhaltlicher Änderung veraltet → Pfad leeren
// (der User erzeugt es bei Bedarf neu) und die alte Datei entfernen,
// damit sie nicht verwaist liegen bleibt.
if (existing.pdfPath) deleteFileIfExists(existing.pdfPath);
return prisma.creditNote.update({ where: { id }, data: { ...normalized, pdfPath: null } });
// (der User erzeugt es bei Bedarf neu). Reihenfolge (R140): erst DB-Update,
// DANN die alte Datei löschen schlägt das Update fehl, bleibt die Datei.
const updated = await prisma.creditNote.update({ where: { id }, data: { ...normalized, pdfPath: null } });
deleteUploadByRelativePath(existing.pdfPath);
return updated;
}
export async function deleteCreditNote(id: number) {
@@ -256,11 +243,13 @@ export async function deleteCreditNote(id: number) {
if (!existing) {
throw new ApiError(404, 'Gutschrift nicht gefunden');
}
// Verwaiste Dateien vermeiden: generiertes PDF + Überweisungsbeleg von der
// Platte entfernen (Pentest R138-Hinweis).
deleteFileIfExists(existing.pdfPath);
deleteFileIfExists(existing.receiptPath);
return prisma.creditNote.delete({ where: { id } });
// Reihenfolge (R140): erst den DB-Datensatz löschen, DANN die Dateien
// schlägt das DB-Delete fehl, bleiben PDF + Beleg erhalten (kein
// ins-Leere-zeigender Eintrag).
const deleted = await prisma.creditNote.delete({ where: { id } });
deleteUploadByRelativePath(existing.pdfPath);
deleteUploadByRelativePath(existing.receiptPath);
return deleted;
}
// Setzt/aktualisiert den Pfad des hochgeladenen Überweisungsbelegs.
+8 -22
View File
@@ -2,21 +2,7 @@ import { CustomerType, ContractStatus } from '@prisma/client';
import prisma from '../lib/prisma.js';
import { generateCustomerNumber, paginate, buildPaginationResponse } from '../utils/helpers.js';
import { ApiError } from '../utils/apiError.js';
import fs from 'fs';
import path from 'path';
// Helper zum Löschen von Dateien
function deleteFileIfExists(filePath: string | null) {
if (!filePath) return;
const absolutePath = path.join(process.cwd(), filePath);
if (fs.existsSync(absolutePath)) {
try {
fs.unlinkSync(absolutePath);
} catch (error) {
console.error('Fehler beim Löschen der Datei:', absolutePath, error);
}
}
}
import { deleteUploadByRelativePath } from '../utils/fileCleanup.js';
export interface CustomerFilters {
search?: string;
@@ -205,17 +191,17 @@ export async function deleteCustomer(id: number) {
// Kundendokumente löschen
if (customer) {
deleteFileIfExists(customer.businessRegistrationPath);
deleteFileIfExists(customer.commercialRegisterPath);
deleteFileIfExists(customer.privacyPolicyPath);
deleteUploadByRelativePath(customer.businessRegistrationPath);
deleteUploadByRelativePath(customer.commercialRegisterPath);
deleteUploadByRelativePath(customer.privacyPolicyPath);
}
// Bankkarten- und Ausweisdokumente löschen
for (const card of bankCards) {
deleteFileIfExists(card.documentPath);
deleteUploadByRelativePath(card.documentPath);
}
for (const doc of identityDocs) {
deleteFileIfExists(doc.documentPath);
deleteUploadByRelativePath(doc.documentPath);
}
// Jetzt DB-Eintrag löschen (Cascade löscht die verknüpften Einträge)
@@ -353,7 +339,7 @@ export async function deleteBankCard(id: number) {
// Erst Datei-Pfad holen, dann Datei löschen, dann DB-Eintrag löschen
const bankCard = await prisma.bankCard.findUnique({ where: { id } });
if (bankCard?.documentPath) {
deleteFileIfExists(bankCard.documentPath);
deleteUploadByRelativePath(bankCard.documentPath);
}
return prisma.bankCard.delete({ where: { id } });
}
@@ -417,7 +403,7 @@ export async function deleteDocument(id: number) {
// Erst Datei-Pfad holen, dann Datei löschen, dann DB-Eintrag löschen
const document = await prisma.identityDocument.findUnique({ where: { id } });
if (document?.documentPath) {
deleteFileIfExists(document.documentPath);
deleteUploadByRelativePath(document.documentPath);
}
return prisma.identityDocument.delete({ where: { id } });
}