a023e96012
56.1 HIGH (IDOR auf Upload-Endpoints):
- /upload/bank-cards/:id (POST/DELETE): canAccessBankCard +
Existenz-Check, multer-Datei wird bei Reject sauber aufgeräumt.
- /upload/documents/:id (POST/DELETE): canAccessIdentityDocument
+ Existenz-Check + Cleanup.
- /upload/customers/:id/{business-registration,commercial-register,
privacy-policy} (POST/DELETE): canAccessCustomer + Cleanup.
- /upload/invoices/:id (POST/DELETE): canAccessContract über
Invoice→Contract-Resolve + Cleanup.
56.2 HIGH (IDOR + Consent-Eskalation bei privacy-policy):
- Vor dem upsert auf alle 4 CustomerConsent-Einträge (=GRANTED)
läuft jetzt canAccessCustomer. Portal-Vertreter ohne Vollmacht
oder Mitarbeiter mit anderer Customer-Beschränkung kommen
damit nicht mehr durch.
56.3 LATENT (updateContract / deleteContract):
- Defense-in-Depth: canAccessContract jetzt explizit im Controller,
nicht nur über die Route-Permission.
56.4 MEDIUM (invoiceType ungeprüft in addInvoiceByContract):
- Neuer assertValidInvoiceType-Helper mit Whitelist
['INTERIM','FINAL','NOT_AVAILABLE'] in addInvoice,
updateInvoice und addInvoiceByContract. updateInvoice nur
bei explizit gesetztem Wert; addInvoiceByContract zusätzlich
die fehlende Required-Field-Validierung ergänzt.
56.5 LOW (GDPR-Löschanfragen ohne Ownership-Check):
- POST /api/gdpr/deletions liest customerId jetzt aus dem Body
(Route hat kein :id-Segment), validiert auf positive Zahl und
ruft canAccessCustomer auf, bevor die Löschanfrage erstellt wird.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
217 lines
7.4 KiB
TypeScript
217 lines
7.4 KiB
TypeScript
import { Request, Response } from 'express';
|
||
import * as invoiceService from '../services/invoice.service.js';
|
||
import { logChange } from '../services/audit.service.js';
|
||
import { ApiResponse, AuthRequest } from '../types/index.js';
|
||
import { canAccessContract, canAccessEnergyContractDetails } from '../utils/accessControl.js';
|
||
|
||
/**
|
||
* Alle Rechnungen für ein EnergyContractDetails abrufen
|
||
*/
|
||
export async function getInvoices(req: AuthRequest, res: Response): Promise<void> {
|
||
try {
|
||
const ecdId = parseInt(req.params.ecdId);
|
||
if (!(await canAccessEnergyContractDetails(req, res, ecdId))) return;
|
||
const invoices = await invoiceService.getInvoices(ecdId);
|
||
res.json({ success: true, data: invoices } as ApiResponse);
|
||
} catch (error) {
|
||
console.error('getInvoices error:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'Fehler beim Laden der Rechnungen',
|
||
} as ApiResponse);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Einzelne Rechnung abrufen
|
||
*/
|
||
export async function getInvoice(req: AuthRequest, res: Response): Promise<void> {
|
||
try {
|
||
const ecdId = parseInt(req.params.ecdId);
|
||
const invoiceId = parseInt(req.params.invoiceId);
|
||
if (!(await canAccessEnergyContractDetails(req, res, ecdId))) return;
|
||
const invoice = await invoiceService.getInvoice(ecdId, invoiceId);
|
||
|
||
if (!invoice) {
|
||
res.status(404).json({
|
||
success: false,
|
||
error: 'Rechnung nicht gefunden',
|
||
} as ApiResponse);
|
||
return;
|
||
}
|
||
|
||
res.json({ success: true, data: invoice } as ApiResponse);
|
||
} catch (error) {
|
||
console.error('getInvoice error:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'Fehler beim Laden der Rechnung',
|
||
} as ApiResponse);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Neue Rechnung hinzufügen
|
||
*/
|
||
// Pentest 56.4 (MEDIUM, 2026-06-01): invoiceType wurde an manchen
|
||
// Endpunkten nicht gegen die Enum-Whitelist validiert; ein beliebiger
|
||
// String landete als invoiceType in der DB und konnte Frontend-
|
||
// Filter/Reports verwirren oder XSS in Audit-Labels einschleusen.
|
||
type ValidInvoiceType = 'INTERIM' | 'FINAL' | 'NOT_AVAILABLE';
|
||
const VALID_INVOICE_TYPES: Set<ValidInvoiceType> = new Set(['INTERIM', 'FINAL', 'NOT_AVAILABLE']);
|
||
function assertValidInvoiceType(value: unknown, res: Response): value is ValidInvoiceType {
|
||
if (typeof value !== 'string' || !VALID_INVOICE_TYPES.has(value as ValidInvoiceType)) {
|
||
res.status(400).json({
|
||
success: false,
|
||
error: `Ungültiger Rechnungstyp. Erlaubt: ${[...VALID_INVOICE_TYPES].join(', ')}.`,
|
||
} as ApiResponse);
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
export async function addInvoice(req: AuthRequest, res: Response): Promise<void> {
|
||
try {
|
||
const ecdId = parseInt(req.params.ecdId);
|
||
if (!(await canAccessEnergyContractDetails(req, res, ecdId))) return;
|
||
const { invoiceDate, invoiceType, documentPath, notes } = req.body;
|
||
|
||
if (!invoiceDate || !invoiceType) {
|
||
res.status(400).json({
|
||
success: false,
|
||
error: 'invoiceDate und invoiceType sind erforderlich',
|
||
} as ApiResponse);
|
||
return;
|
||
}
|
||
if (!assertValidInvoiceType(invoiceType, res)) return;
|
||
|
||
const invoice = await invoiceService.addInvoice(ecdId, {
|
||
invoiceDate: new Date(invoiceDate),
|
||
invoiceType,
|
||
documentPath,
|
||
notes,
|
||
});
|
||
|
||
await logChange({
|
||
req, action: 'CREATE', resourceType: 'Invoice',
|
||
resourceId: invoice.id.toString(),
|
||
label: `Rechnung (${invoiceType}) hinzugefügt`,
|
||
});
|
||
|
||
res.status(201).json({ success: true, data: invoice } as ApiResponse);
|
||
} catch (error) {
|
||
console.error('addInvoice error:', error);
|
||
res.status(400).json({
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen der Rechnung',
|
||
} as ApiResponse);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Rechnung aktualisieren
|
||
*/
|
||
export async function updateInvoice(req: AuthRequest, res: Response): Promise<void> {
|
||
try {
|
||
const ecdId = parseInt(req.params.ecdId);
|
||
const invoiceId = parseInt(req.params.invoiceId);
|
||
if (!(await canAccessEnergyContractDetails(req, res, ecdId))) return;
|
||
const { invoiceDate, invoiceType, documentPath, notes } = req.body;
|
||
// 56.4: invoiceType ist beim Update optional – nur prüfen wenn gesetzt.
|
||
if (invoiceType !== undefined && !assertValidInvoiceType(invoiceType, res)) return;
|
||
|
||
const invoice = await invoiceService.updateInvoice(ecdId, invoiceId, {
|
||
invoiceDate: invoiceDate ? new Date(invoiceDate) : undefined,
|
||
invoiceType,
|
||
documentPath,
|
||
notes,
|
||
});
|
||
|
||
await logChange({
|
||
req, action: 'UPDATE', resourceType: 'Invoice',
|
||
resourceId: invoiceId.toString(),
|
||
label: `Rechnung aktualisiert`,
|
||
});
|
||
|
||
res.json({ success: true, data: invoice } as ApiResponse);
|
||
} catch (error) {
|
||
console.error('updateInvoice error:', error);
|
||
res.status(400).json({
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Rechnung',
|
||
} as ApiResponse);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Rechnung löschen
|
||
*/
|
||
export async function deleteInvoice(req: AuthRequest, res: Response): Promise<void> {
|
||
try {
|
||
const ecdId = parseInt(req.params.ecdId);
|
||
const invoiceId = parseInt(req.params.invoiceId);
|
||
if (!(await canAccessEnergyContractDetails(req, res, ecdId))) return;
|
||
|
||
await invoiceService.deleteInvoice(ecdId, invoiceId);
|
||
|
||
await logChange({
|
||
req, action: 'DELETE', resourceType: 'Invoice',
|
||
resourceId: invoiceId.toString(),
|
||
label: `Rechnung gelöscht`,
|
||
});
|
||
|
||
res.json({ success: true, data: null } as ApiResponse);
|
||
} catch (error) {
|
||
console.error('deleteInvoice error:', error);
|
||
res.status(400).json({
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Rechnung',
|
||
} as ApiResponse);
|
||
}
|
||
}
|
||
|
||
// ==================== CONTRACT-BASIERTE RECHNUNGEN (für alle Vertragstypen) ====================
|
||
|
||
export async function getInvoicesByContract(req: AuthRequest, res: Response): Promise<void> {
|
||
try {
|
||
const contractId = parseInt(req.params.id);
|
||
if (!(await canAccessContract(req, res, contractId))) return;
|
||
const invoices = await invoiceService.getInvoicesByContract(contractId);
|
||
res.json({ success: true, data: invoices } as ApiResponse);
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, error: 'Fehler beim Laden der Rechnungen' } as ApiResponse);
|
||
}
|
||
}
|
||
|
||
export async function addInvoiceByContract(req: AuthRequest, res: Response): Promise<void> {
|
||
try {
|
||
const contractId = parseInt(req.params.id);
|
||
if (!(await canAccessContract(req, res, contractId))) return;
|
||
const { invoiceDate, invoiceType, notes } = req.body;
|
||
if (!invoiceDate || !invoiceType) {
|
||
res.status(400).json({
|
||
success: false,
|
||
error: 'invoiceDate und invoiceType sind erforderlich',
|
||
} as ApiResponse);
|
||
return;
|
||
}
|
||
if (!assertValidInvoiceType(invoiceType, res)) return;
|
||
const invoice = await invoiceService.addInvoiceByContract(contractId, {
|
||
invoiceDate: new Date(invoiceDate),
|
||
invoiceType,
|
||
notes,
|
||
});
|
||
await logChange({
|
||
req, action: 'CREATE', resourceType: 'Invoice',
|
||
resourceId: invoice.id.toString(),
|
||
label: `Rechnung (${invoiceType}) hinzugefügt`,
|
||
});
|
||
res.status(201).json({ success: true, data: invoice } as ApiResponse);
|
||
} catch (error) {
|
||
res.status(400).json({
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen',
|
||
} as ApiResponse);
|
||
}
|
||
}
|