import { Router, Response } from 'express'; import multer from 'multer'; import path from 'path'; import fs from 'fs'; import prisma from '../lib/prisma.js'; import { authenticate, requirePermission } from '../middleware/auth.js'; import { AuthRequest } from '../types/index.js'; import { logChange } from '../services/audit.service.js'; import { canAccessContract, canAccessCustomer, canAccessBankCard, canAccessIdentityDocument, } from '../utils/accessControl.js'; import { validateOptionalIsoDate } from '../utils/sanitize.js'; import { validateUploadedFile } from '../middleware/uploadFileTypeValidator.js'; // Pentest 56.1 (HIGH, 2026-06-01): Upload-Endpoints prüften nur die // Permission, nicht ob die Ziel-Resource zum Caller passt. Helper-Funktion // für saubere 404-Antwort + Datei-Cleanup, wenn die Resource nicht // existiert. Anschließend laufen die `canAccess*`-Checks (Portal-User // werden dort auf ihre eigenen Kunden eingeschränkt; Staff bekommen // volle Sicht – konsistent mit der bestehenden Access-Control-Logik). async function resolveInvoiceContractId(invoiceId: number): Promise { const invoice = await prisma.invoice.findUnique({ where: { id: invoiceId }, select: { contractId: true, energyContractDetails: { select: { contractId: true } } }, }); return invoice?.contractId ?? invoice?.energyContractDetails?.contractId ?? null; } function cleanupFile(filePath?: string) { if (!filePath) return; try { fs.unlinkSync(filePath); } catch { /* ignore */ } } const router = Router(); // Uploads-Verzeichnis erstellen falls nicht vorhanden const uploadsDir = path.join(process.cwd(), 'uploads'); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } // Multer-Konfiguration const storage = multer.diskStorage({ destination: (req, file, cb) => { const subDir = (req as any).uploadSubDir || 'misc'; const targetDir = path.join(uploadsDir, subDir); if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }); } cb(null, targetDir); }, filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const ext = path.extname(file.originalname); cb(null, `${uniqueSuffix}${ext}`); }, }); const fileFilter = ( req: Express.Request, file: Express.Multer.File, cb: multer.FileFilterCallback ) => { // PDFs + gängige Web-Bildformate. WebP + GIF nachgezogen 2026-05-30 // (Pentest INFO: WebP/GIF lieferten 500 statt sauberem 4xx, weil // erlaubter MIME-Type fehlte und der fileFilter dann throwte). const allowedTypes = [ 'application/pdf', 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', ]; if (allowedTypes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('Nur PDF, JPG, PNG, GIF und WebP-Dateien sind erlaubt')); } }; const upload = multer({ storage, fileFilter, limits: { // 25 MB – passt für Ausweis-Scans, Handy-Photos im JpgToPdf-Flow, // mehrseitige PDFs aus dem Modal (bis ~5-7 Seiten je nach Auflösung). // Vorher 10 MB → Multer brach bei zwei Smartphone-Fotos ab. fileSize: 25 * 1024 * 1024, }, }); // Middleware um Subdirectory zu setzen function setUploadDir(subDir: string) { return (req: AuthRequest, res: Response, next: Function) => { (req as any).uploadSubDir = subDir; next(); }; } // Magic-Byte-Whitelist + canonical Rename + PDF-Scan: siehe // middleware/uploadFileTypeValidator.ts (zentralisiert, damit auch // contract.routes.ts denselben Check fahren kann – Pentest 69.3). // Upload für Bankkarten-Dokumente router.post( '/bank-cards/:id', authenticate, requirePermission('customers:update'), setUploadDir('bank-cards'), upload.single('document'), validateUploadedFile, async (req: AuthRequest, res: Response) => { try { if (!req.file) { res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' }); return; } const bankCardId = parseInt(req.params.id); // Pentest 56.1: Existenz- und Ownership-Check VOR DB-Update. const card = await prisma.bankCard.findUnique({ where: { id: bankCardId } }); if (!card) { cleanupFile(req.file.path); res.status(404).json({ success: false, error: 'Bankkarte nicht gefunden' }); return; } if (!(await canAccessBankCard(req, res, bankCardId))) { cleanupFile(req.file.path); return; } const relativePath = `/uploads/bank-cards/${req.file.filename}`; await prisma.bankCard.update({ where: { id: bankCardId }, data: { documentPath: relativePath }, }); res.json({ success: true, data: { path: relativePath, filename: req.file.filename, originalName: req.file.originalname, size: req.file.size, }, }); } catch (error) { console.error('Upload error:', error); cleanupFile(req.file?.path); res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' }); } } ); // Upload für Ausweis-Dokumente router.post( '/documents/:id', authenticate, requirePermission('customers:update'), setUploadDir('documents'), upload.single('document'), validateUploadedFile, async (req: AuthRequest, res: Response) => { try { if (!req.file) { res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' }); return; } const documentId = parseInt(req.params.id); // Pentest 56.1: Existenz- und Ownership-Check VOR DB-Update. const doc = await prisma.identityDocument.findUnique({ where: { id: documentId } }); if (!doc) { cleanupFile(req.file.path); res.status(404).json({ success: false, error: 'Ausweis nicht gefunden' }); return; } if (!(await canAccessIdentityDocument(req, res, documentId))) { cleanupFile(req.file.path); return; } const relativePath = `/uploads/documents/${req.file.filename}`; await prisma.identityDocument.update({ where: { id: documentId }, data: { documentPath: relativePath }, }); res.json({ success: true, data: { path: relativePath, filename: req.file.filename, originalName: req.file.originalname, size: req.file.size, }, }); } catch (error) { console.error('Upload error:', error); cleanupFile(req.file?.path); res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' }); } } ); // Löschen von Bankkarten-Dokumenten router.delete( '/bank-cards/:id', authenticate, requirePermission('customers:update'), async (req: AuthRequest, res: Response) => { try { const bankCardId = parseInt(req.params.id); // Bankkarte aus DB holen um Dateipfad zu bekommen const bankCard = await prisma.bankCard.findUnique({ where: { id: bankCardId }, }); if (!bankCard) { res.status(404).json({ success: false, error: 'Bankkarte nicht gefunden' }); return; } // Pentest 56.1: Ownership-Check. if (!(await canAccessBankCard(req, res, bankCardId))) return; if (!bankCard.documentPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' }); return; } // Datei von Festplatte löschen const filePath = path.join(process.cwd(), bankCard.documentPath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } // documentPath in DB auf null setzen await prisma.bankCard.update({ where: { id: bankCardId }, data: { documentPath: null }, }); res.json({ success: true }); } catch (error) { console.error('Delete error:', error); res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' }); } } ); // Löschen von Ausweis-Dokumenten router.delete( '/documents/:id', authenticate, requirePermission('customers:update'), async (req: AuthRequest, res: Response) => { try { const documentId = parseInt(req.params.id); // Ausweis aus DB holen um Dateipfad zu bekommen const document = await prisma.identityDocument.findUnique({ where: { id: documentId }, }); if (!document) { res.status(404).json({ success: false, error: 'Ausweis nicht gefunden' }); return; } // Pentest 56.1: Ownership-Check. if (!(await canAccessIdentityDocument(req, res, documentId))) return; if (!document.documentPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' }); return; } // Datei von Festplatte löschen const filePath = path.join(process.cwd(), document.documentPath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } // documentPath in DB auf null setzen await prisma.identityDocument.update({ where: { id: documentId }, data: { documentPath: null }, }); res.json({ success: true }); } catch (error) { console.error('Delete error:', error); res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' }); } } ); // ==================== FIRMEN-DOKUMENTE ==================== // Upload für Gewerbeanmeldung router.post( '/customers/:id/business-registration', authenticate, requirePermission('customers:update'), setUploadDir('business-registrations'), upload.single('document'), validateUploadedFile, async (req: AuthRequest, res: Response) => { try { if (!req.file) { res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' }); return; } const customerId = parseInt(req.params.id); // Pentest 56.1: Ownership-Check. if (!(await canAccessCustomer(req, res, customerId))) { cleanupFile(req.file.path); return; } const relativePath = `/uploads/business-registrations/${req.file.filename}`; // Alte Datei löschen falls vorhanden const customer = await prisma.customer.findUnique({ where: { id: customerId } }); if (customer?.businessRegistrationPath) { const oldPath = path.join(process.cwd(), customer.businessRegistrationPath); if (fs.existsSync(oldPath)) { fs.unlinkSync(oldPath); } } // Kunde in der DB aktualisieren await prisma.customer.update({ where: { id: customerId }, data: { businessRegistrationPath: relativePath }, }); res.json({ success: true, data: { path: relativePath, filename: req.file.filename, originalName: req.file.originalname, size: req.file.size, }, }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' }); } } ); // Upload für Handelsregisterauszug router.post( '/customers/:id/commercial-register', authenticate, requirePermission('customers:update'), setUploadDir('commercial-registers'), upload.single('document'), validateUploadedFile, async (req: AuthRequest, res: Response) => { try { if (!req.file) { res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' }); return; } const customerId = parseInt(req.params.id); // Pentest 56.1: Ownership-Check. if (!(await canAccessCustomer(req, res, customerId))) { cleanupFile(req.file.path); return; } const relativePath = `/uploads/commercial-registers/${req.file.filename}`; // Alte Datei löschen falls vorhanden const customer = await prisma.customer.findUnique({ where: { id: customerId } }); if (customer?.commercialRegisterPath) { const oldPath = path.join(process.cwd(), customer.commercialRegisterPath); if (fs.existsSync(oldPath)) { fs.unlinkSync(oldPath); } } // Kunde in der DB aktualisieren await prisma.customer.update({ where: { id: customerId }, data: { commercialRegisterPath: relativePath }, }); res.json({ success: true, data: { path: relativePath, filename: req.file.filename, originalName: req.file.originalname, size: req.file.size, }, }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' }); } } ); // Löschen der Gewerbeanmeldung router.delete( '/customers/:id/business-registration', authenticate, requirePermission('customers:update'), async (req: AuthRequest, res: Response) => { try { const customerId = parseInt(req.params.id); // Pentest 56.1: Ownership-Check. if (!(await canAccessCustomer(req, res, customerId))) return; const customer = await prisma.customer.findUnique({ where: { id: customerId } }); if (!customer) { res.status(404).json({ success: false, error: 'Kunde nicht gefunden' }); return; } if (!customer.businessRegistrationPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' }); return; } // Datei löschen const filePath = path.join(process.cwd(), customer.businessRegistrationPath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } // Pfad in DB auf null setzen await prisma.customer.update({ where: { id: customerId }, data: { businessRegistrationPath: null }, }); res.json({ success: true }); } catch (error) { console.error('Delete error:', error); res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' }); } } ); // Löschen des Handelsregisterauszugs router.delete( '/customers/:id/commercial-register', authenticate, requirePermission('customers:update'), async (req: AuthRequest, res: Response) => { try { const customerId = parseInt(req.params.id); // Pentest 56.1: Ownership-Check. if (!(await canAccessCustomer(req, res, customerId))) return; const customer = await prisma.customer.findUnique({ where: { id: customerId } }); if (!customer) { res.status(404).json({ success: false, error: 'Kunde nicht gefunden' }); return; } if (!customer.commercialRegisterPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' }); return; } // Datei löschen const filePath = path.join(process.cwd(), customer.commercialRegisterPath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } // Pfad in DB auf null setzen await prisma.customer.update({ where: { id: customerId }, data: { commercialRegisterPath: null }, }); res.json({ success: true }); } catch (error) { console.error('Delete error:', error); res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' }); } } ); // ==================== DATENSCHUTZERKLÄRUNG (für alle Kunden) ==================== // Upload für Datenschutzerklärung router.post( '/customers/:id/privacy-policy', authenticate, requirePermission('customers:update'), setUploadDir('privacy-policies'), upload.single('document'), validateUploadedFile, async (req: AuthRequest, res: Response) => { try { if (!req.file) { res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' }); return; } const customerId = parseInt(req.params.id); // Pentest 56.2 (HIGH): Ownership-Check VOR Consent-Massen-Update. // Ohne diese Prüfung konnte jeder Caller mit customers:update für // jede beliebige customerId ALLE Einwilligungen auf GRANTED setzen // (DSGVO-Eskalation). if (!(await canAccessCustomer(req, res, customerId))) { cleanupFile(req.file.path); return; } const relativePath = `/uploads/privacy-policies/${req.file.filename}`; // Alte Datei löschen falls vorhanden const customer = await prisma.customer.findUnique({ where: { id: customerId } }); if (customer?.privacyPolicyPath) { const oldPath = path.join(process.cwd(), customer.privacyPolicyPath); if (fs.existsSync(oldPath)) { fs.unlinkSync(oldPath); } } // Kunde in der DB aktualisieren await prisma.customer.update({ where: { id: customerId }, data: { privacyPolicyPath: relativePath }, }); // Alle Consents auf GRANTED setzen (PDF = vollständige Einwilligung) const consentTypes = ['DATA_PROCESSING', 'MARKETING_EMAIL', 'MARKETING_PHONE', 'DATA_SHARING_PARTNER'] as const; for (const consentType of consentTypes) { await prisma.customerConsent.upsert({ where: { customerId_consentType: { customerId, consentType } }, update: { status: 'GRANTED', grantedAt: new Date(), source: 'papier' }, create: { customerId, consentType, status: 'GRANTED', grantedAt: new Date(), source: 'papier', createdBy: (req as any).user?.email || 'admin' }, }); } // Audit const cust = await prisma.customer.findUnique({ where: { id: customerId }, select: { firstName: true, lastName: true } }); await logChange({ req, action: 'CREATE', resourceType: 'CustomerConsent', label: `Datenschutzerklärung-PDF hochgeladen für ${cust?.firstName} ${cust?.lastName} – alle Einwilligungen erteilt`, details: { aktion: 'PDF hochgeladen', einwilligungen: 'alle erteilt', quelle: 'papier' }, customerId, }); res.json({ success: true, data: { path: relativePath, filename: req.file.filename, originalName: req.file.originalname, size: req.file.size, }, }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' }); } } ); // Löschen der Datenschutzerklärung router.delete( '/customers/:id/privacy-policy', authenticate, requirePermission('customers:update'), async (req: AuthRequest, res: Response) => { try { const customerId = parseInt(req.params.id); // Pentest 56.1: Ownership-Check. if (!(await canAccessCustomer(req, res, customerId))) return; const customer = await prisma.customer.findUnique({ where: { id: customerId } }); if (!customer) { res.status(404).json({ success: false, error: 'Kunde nicht gefunden' }); return; } if (!customer.privacyPolicyPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' }); return; } // Datei löschen const filePath = path.join(process.cwd(), customer.privacyPolicyPath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } // Pfad in DB auf null setzen await prisma.customer.update({ where: { id: customerId }, data: { privacyPolicyPath: null }, }); // Nur Consents widerrufen die per Papier erteilt wurden await prisma.customerConsent.updateMany({ where: { customerId, status: 'GRANTED', source: 'papier' }, data: { status: 'WITHDRAWN', withdrawnAt: new Date() }, }); // Audit const cust = await prisma.customer.findUnique({ where: { id: customerId }, select: { firstName: true, lastName: true } }); await logChange({ req, action: 'DELETE', resourceType: 'CustomerConsent', label: `Datenschutzerklärung-PDF gelöscht für ${cust?.firstName} ${cust?.lastName} – Papier-Einwilligungen widerrufen`, details: { aktion: 'PDF gelöscht', einwilligungen: 'papier-basierte widerrufen' }, customerId, }); res.json({ success: true }); } catch (error) { console.error('Delete error:', error); res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' }); } } ); // ==================== VERTRAGS-DOKUMENTE ==================== // Generische Funktion für Vertrags-Dokument Upload async function handleContractDocumentUpload( req: AuthRequest, res: Response, fieldName: 'cancellationLetterPath' | 'cancellationConfirmationPath' | 'cancellationLetterOptionsPath' | 'cancellationConfirmationOptionsPath', subDir: string ) { try { if (!req.file) { res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' }); return; } const contractId = parseInt(req.params.id); if (!(await canAccessContract(req, res, contractId))) return; const relativePath = `/uploads/${subDir}/${req.file.filename}`; // Alte Datei löschen falls vorhanden const contract = await prisma.contract.findUnique({ where: { id: contractId } }); if (!contract) { res.status(404).json({ success: false, error: 'Vertrag nicht gefunden' }); return; } const oldPath = contract[fieldName]; if (oldPath) { const fullPath = path.join(process.cwd(), oldPath); if (fs.existsSync(fullPath)) { fs.unlinkSync(fullPath); } } // Bei Kündigungsbestätigung(s-Optionen): optionales Datum aus multipart // übernehmen. Ohne Angabe: falls Feld noch leer → heute, sonst nicht anfassen. const updateData: Record = { [fieldName]: relativePath }; if (fieldName === 'cancellationConfirmationPath' || fieldName === 'cancellationConfirmationOptionsPath') { const dateField = fieldName === 'cancellationConfirmationPath' ? 'cancellationConfirmationDate' : 'cancellationConfirmationOptionsDate'; // Pentest 62.7: confirmationDate gegen ISO-8601 validieren. let provided: string | null; try { provided = validateOptionalIsoDate(req.body?.confirmationDate, 'confirmationDate'); } catch (err) { cleanupFile(req.file?.path); res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültiges Bestätigungsdatum' }); return; } let target: Date | null = null; if (provided) { target = new Date(provided); } if (target) { updateData[dateField] = target; } else if (!contract[dateField]) { updateData[dateField] = new Date(); } } // Vertrag in der DB aktualisieren await prisma.contract.update({ where: { id: contractId }, data: updateData, }); // Wenn eine Kündigungsbestätigung (nicht "Optionen") hochgeladen wurde und // der Vertrag noch ACTIVE ist → auf CANCELLED umstellen + Audit-Log. // "Optionen" ist für Vertrags-Änderungen gedacht, nicht für echte Kündigungen. if (fieldName === 'cancellationConfirmationPath' && contract.status === 'ACTIVE') { await prisma.contract.update({ where: { id: contractId }, data: { status: 'CANCELLED' }, }); await logChange({ req, action: 'UPDATE', resourceType: 'Contract', resourceId: contractId.toString(), label: `Vertrag ${contract.contractNumber} automatisch auf CANCELLED gesetzt (Kündigungsbestätigung hochgeladen)`, details: { vorher: 'ACTIVE', nachher: 'CANCELLED', trigger: 'cancellationConfirmation-Upload' }, customerId: contract.customerId, }); } res.json({ success: true, data: { path: relativePath, filename: req.file.filename, originalName: req.file.originalname, size: req.file.size, }, }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' }); } } // Generische Funktion für Vertrags-Dokument Löschen async function handleContractDocumentDelete( req: AuthRequest, res: Response, fieldName: 'cancellationLetterPath' | 'cancellationConfirmationPath' | 'cancellationLetterOptionsPath' | 'cancellationConfirmationOptionsPath' ) { try { const contractId = parseInt(req.params.id); if (!(await canAccessContract(req, res, contractId))) return; const contract = await prisma.contract.findUnique({ where: { id: contractId } }); if (!contract) { res.status(404).json({ success: false, error: 'Vertrag nicht gefunden' }); return; } const documentPath = contract[fieldName]; if (!documentPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' }); return; } // Datei löschen const filePath = path.join(process.cwd(), documentPath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } // Pfad in DB auf null setzen await prisma.contract.update({ where: { id: contractId }, data: { [fieldName]: null }, }); res.json({ success: true }); } catch (error) { console.error('Delete error:', error); res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' }); } } // Kündigungsschreiben router.post( '/contracts/:id/cancellation-letter', authenticate, requirePermission('contracts:update'), setUploadDir('cancellation-letters'), upload.single('document'), validateUploadedFile, (req: AuthRequest, res: Response) => handleContractDocumentUpload(req, res, 'cancellationLetterPath', 'cancellation-letters') ); router.delete( '/contracts/:id/cancellation-letter', authenticate, requirePermission('contracts:update'), (req: AuthRequest, res: Response) => handleContractDocumentDelete(req, res, 'cancellationLetterPath') ); // Kündigungsbestätigung router.post( '/contracts/:id/cancellation-confirmation', authenticate, requirePermission('contracts:update'), setUploadDir('cancellation-confirmations'), upload.single('document'), validateUploadedFile, (req: AuthRequest, res: Response) => handleContractDocumentUpload(req, res, 'cancellationConfirmationPath', 'cancellation-confirmations') ); router.delete( '/contracts/:id/cancellation-confirmation', authenticate, requirePermission('contracts:update'), (req: AuthRequest, res: Response) => handleContractDocumentDelete(req, res, 'cancellationConfirmationPath') ); // Kündigungsschreiben Optionen router.post( '/contracts/:id/cancellation-letter-options', authenticate, requirePermission('contracts:update'), setUploadDir('cancellation-letters-options'), upload.single('document'), validateUploadedFile, (req: AuthRequest, res: Response) => handleContractDocumentUpload(req, res, 'cancellationLetterOptionsPath', 'cancellation-letters-options') ); router.delete( '/contracts/:id/cancellation-letter-options', authenticate, requirePermission('contracts:update'), (req: AuthRequest, res: Response) => handleContractDocumentDelete(req, res, 'cancellationLetterOptionsPath') ); // Kündigungsbestätigung Optionen router.post( '/contracts/:id/cancellation-confirmation-options', authenticate, requirePermission('contracts:update'), setUploadDir('cancellation-confirmations-options'), upload.single('document'), validateUploadedFile, (req: AuthRequest, res: Response) => handleContractDocumentUpload(req, res, 'cancellationConfirmationOptionsPath', 'cancellation-confirmations-options') ); router.delete( '/contracts/:id/cancellation-confirmation-options', authenticate, requirePermission('contracts:update'), (req: AuthRequest, res: Response) => handleContractDocumentDelete(req, res, 'cancellationConfirmationOptionsPath') ); // ==================== RECHNUNGS-DOKUMENTE ==================== // Upload für Rechnungs-Dokument router.post( '/invoices/:id', authenticate, requirePermission('contracts:update'), setUploadDir('invoices'), upload.single('document'), validateUploadedFile, async (req: AuthRequest, res: Response) => { try { if (!req.file) { res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' }); return; } const invoiceId = parseInt(req.params.id); // Pentest 56.1: Existenz- und Ownership-Check VOR DB-Update. const invoice = await prisma.invoice.findUnique({ where: { id: invoiceId } }); if (!invoice) { cleanupFile(req.file.path); res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' }); return; } const invoiceContractId = await resolveInvoiceContractId(invoiceId); if (invoiceContractId == null || !(await canAccessContract(req, res, invoiceContractId))) { cleanupFile(req.file.path); return; } const relativePath = `/uploads/invoices/${req.file.filename}`; if (invoice.documentPath) { const oldPath = path.join(process.cwd(), invoice.documentPath); if (fs.existsSync(oldPath)) { fs.unlinkSync(oldPath); } } // Invoice in der DB aktualisieren await prisma.invoice.update({ where: { id: invoiceId }, data: { documentPath: relativePath }, }); res.json({ success: true, data: { path: relativePath, filename: req.file.filename, originalName: req.file.originalname, size: req.file.size, }, }); } catch (error) { console.error('Invoice upload error:', error); res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' }); } } ); // Löschen von Rechnungs-Dokument router.delete( '/invoices/:id', authenticate, requirePermission('contracts:update'), async (req: AuthRequest, res: Response) => { try { const invoiceId = parseInt(req.params.id); const invoice = await prisma.invoice.findUnique({ where: { id: invoiceId } }); if (!invoice) { res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' }); return; } // Pentest 56.1: Ownership-Check vor Delete. const invoiceContractId = await resolveInvoiceContractId(invoiceId); if (invoiceContractId == null || !(await canAccessContract(req, res, invoiceContractId))) return; if (!invoice.documentPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' }); return; } // Datei löschen const filePath = path.join(process.cwd(), invoice.documentPath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } // documentPath in DB auf null setzen await prisma.invoice.update({ where: { id: invoiceId }, data: { documentPath: null }, }); res.json({ success: true }); } catch (error) { console.error('Invoice delete error:', error); res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' }); } } ); export default router;