Pentest 56.1/56.2/56.3/56.4/56.5: Ownership-Checks + InvoiceType-Validierung
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>
This commit is contained in:
@@ -181,6 +181,11 @@ export async function createContract(req: AuthRequest, res: Response): Promise<v
|
|||||||
export async function updateContract(req: AuthRequest, res: Response): Promise<void> {
|
export async function updateContract(req: AuthRequest, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const contractId = parseInt(req.params.id);
|
const contractId = parseInt(req.params.id);
|
||||||
|
// Pentest 56.3 (latent, 2026-06-01): Defense-in-Depth –
|
||||||
|
// canAccessContract explizit aufrufen, statt sich nur auf die
|
||||||
|
// Route-Permission zu verlassen. Portal-User mit kompromittierter
|
||||||
|
// Token-Permission würden sonst beliebige Verträge editieren können.
|
||||||
|
if (!(await canAccessContract(req, res, contractId))) return;
|
||||||
// Vorherigen Stand laden für Audit-Vergleich
|
// Vorherigen Stand laden für Audit-Vergleich
|
||||||
const before = await prisma.contract.findUnique({
|
const before = await prisma.contract.findUnique({
|
||||||
where: { id: contractId },
|
where: { id: contractId },
|
||||||
@@ -264,6 +269,8 @@ export async function updateContract(req: AuthRequest, res: Response): Promise<v
|
|||||||
export async function deleteContract(req: Request, res: Response): Promise<void> {
|
export async function deleteContract(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const contractId = parseInt(req.params.id);
|
const contractId = parseInt(req.params.id);
|
||||||
|
// Pentest 56.3 (latent): Defense-in-Depth – Ownership-Check vor Delete.
|
||||||
|
if (!(await canAccessContract(req as AuthRequest, res, contractId))) return;
|
||||||
const contract = await prisma.contract.findUnique({ where: { id: contractId }, select: { contractNumber: true, customerId: true } });
|
const contract = await prisma.contract.findUnique({ where: { id: contractId }, select: { contractNumber: true, customerId: true } });
|
||||||
await contractService.deleteContract(contractId);
|
await contractService.deleteContract(contractId);
|
||||||
await logChange({
|
await logChange({
|
||||||
|
|||||||
@@ -65,7 +65,20 @@ export async function exportCustomerData(req: AuthRequest, res: Response) {
|
|||||||
*/
|
*/
|
||||||
export async function createDeletionRequest(req: AuthRequest, res: Response) {
|
export async function createDeletionRequest(req: AuthRequest, res: Response) {
|
||||||
try {
|
try {
|
||||||
const customerId = parseInt(req.params.id);
|
// Pentest 56.5 (LOW, 2026-06-01): customerId muss als gültige Zahl
|
||||||
|
// aus dem Body kommen (Route hat kein :id-Segment) – und der Caller
|
||||||
|
// braucht Zugriff auf den Kunden. Ohne den Check konnte jemand mit
|
||||||
|
// gdpr:delete-Permission Löschanfragen für beliebige Kunden stellen
|
||||||
|
// (Insider-Sabotage durch Portal-Vertreter ohne Vollmacht).
|
||||||
|
const bodyCustomerId = req.body?.customerId;
|
||||||
|
const customerId = typeof bodyCustomerId === 'number'
|
||||||
|
? bodyCustomerId
|
||||||
|
: parseInt(bodyCustomerId);
|
||||||
|
if (!Number.isFinite(customerId) || customerId < 1) {
|
||||||
|
res.status(400).json({ success: false, error: 'customerId fehlt oder ungültig' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(await canAccessCustomer(req, res, customerId))) return;
|
||||||
const { requestSource } = req.body;
|
const { requestSource } = req.body;
|
||||||
|
|
||||||
const request = await gdprService.createDeletionRequest({
|
const request = await gdprService.createDeletionRequest({
|
||||||
|
|||||||
@@ -53,6 +53,23 @@ export async function getInvoice(req: AuthRequest, res: Response): Promise<void>
|
|||||||
/**
|
/**
|
||||||
* Neue Rechnung hinzufügen
|
* 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> {
|
export async function addInvoice(req: AuthRequest, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const ecdId = parseInt(req.params.ecdId);
|
const ecdId = parseInt(req.params.ecdId);
|
||||||
@@ -66,6 +83,7 @@ export async function addInvoice(req: AuthRequest, res: Response): Promise<void>
|
|||||||
} as ApiResponse);
|
} as ApiResponse);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!assertValidInvoiceType(invoiceType, res)) return;
|
||||||
|
|
||||||
const invoice = await invoiceService.addInvoice(ecdId, {
|
const invoice = await invoiceService.addInvoice(ecdId, {
|
||||||
invoiceDate: new Date(invoiceDate),
|
invoiceDate: new Date(invoiceDate),
|
||||||
@@ -99,6 +117,8 @@ export async function updateInvoice(req: AuthRequest, res: Response): Promise<vo
|
|||||||
const invoiceId = parseInt(req.params.invoiceId);
|
const invoiceId = parseInt(req.params.invoiceId);
|
||||||
if (!(await canAccessEnergyContractDetails(req, res, ecdId))) return;
|
if (!(await canAccessEnergyContractDetails(req, res, ecdId))) return;
|
||||||
const { invoiceDate, invoiceType, documentPath, notes } = req.body;
|
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, {
|
const invoice = await invoiceService.updateInvoice(ecdId, invoiceId, {
|
||||||
invoiceDate: invoiceDate ? new Date(invoiceDate) : undefined,
|
invoiceDate: invoiceDate ? new Date(invoiceDate) : undefined,
|
||||||
@@ -168,6 +188,14 @@ export async function addInvoiceByContract(req: AuthRequest, res: Response): Pro
|
|||||||
const contractId = parseInt(req.params.id);
|
const contractId = parseInt(req.params.id);
|
||||||
if (!(await canAccessContract(req, res, contractId))) return;
|
if (!(await canAccessContract(req, res, contractId))) return;
|
||||||
const { invoiceDate, invoiceType, notes } = req.body;
|
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, {
|
const invoice = await invoiceService.addInvoiceByContract(contractId, {
|
||||||
invoiceDate: new Date(invoiceDate),
|
invoiceDate: new Date(invoiceDate),
|
||||||
invoiceType,
|
invoiceType,
|
||||||
|
|||||||
@@ -6,7 +6,31 @@ import prisma from '../lib/prisma.js';
|
|||||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||||
import { AuthRequest } from '../types/index.js';
|
import { AuthRequest } from '../types/index.js';
|
||||||
import { logChange } from '../services/audit.service.js';
|
import { logChange } from '../services/audit.service.js';
|
||||||
import { canAccessContract } from '../utils/accessControl.js';
|
import {
|
||||||
|
canAccessContract,
|
||||||
|
canAccessCustomer,
|
||||||
|
canAccessBankCard,
|
||||||
|
canAccessIdentityDocument,
|
||||||
|
} from '../utils/accessControl.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<number | null> {
|
||||||
|
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();
|
const router = Router();
|
||||||
|
|
||||||
@@ -164,9 +188,20 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bankCardId = parseInt(req.params.id);
|
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}`;
|
const relativePath = `/uploads/bank-cards/${req.file.filename}`;
|
||||||
|
|
||||||
// Bankkarte in der DB aktualisieren
|
|
||||||
await prisma.bankCard.update({
|
await prisma.bankCard.update({
|
||||||
where: { id: bankCardId },
|
where: { id: bankCardId },
|
||||||
data: { documentPath: relativePath },
|
data: { documentPath: relativePath },
|
||||||
@@ -183,6 +218,7 @@ router.post(
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error:', error);
|
||||||
|
cleanupFile(req.file?.path);
|
||||||
res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' });
|
res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,9 +240,20 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const documentId = parseInt(req.params.id);
|
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}`;
|
const relativePath = `/uploads/documents/${req.file.filename}`;
|
||||||
|
|
||||||
// Ausweis in der DB aktualisieren
|
|
||||||
await prisma.identityDocument.update({
|
await prisma.identityDocument.update({
|
||||||
where: { id: documentId },
|
where: { id: documentId },
|
||||||
data: { documentPath: relativePath },
|
data: { documentPath: relativePath },
|
||||||
@@ -223,6 +270,7 @@ router.post(
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error:', error);
|
||||||
|
cleanupFile(req.file?.path);
|
||||||
res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' });
|
res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,6 +294,8 @@ router.delete(
|
|||||||
res.status(404).json({ success: false, error: 'Bankkarte nicht gefunden' });
|
res.status(404).json({ success: false, error: 'Bankkarte nicht gefunden' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Pentest 56.1: Ownership-Check.
|
||||||
|
if (!(await canAccessBankCard(req, res, bankCardId))) return;
|
||||||
|
|
||||||
if (!bankCard.documentPath) {
|
if (!bankCard.documentPath) {
|
||||||
res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' });
|
res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' });
|
||||||
@@ -290,6 +340,8 @@ router.delete(
|
|||||||
res.status(404).json({ success: false, error: 'Ausweis nicht gefunden' });
|
res.status(404).json({ success: false, error: 'Ausweis nicht gefunden' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Pentest 56.1: Ownership-Check.
|
||||||
|
if (!(await canAccessIdentityDocument(req, res, documentId))) return;
|
||||||
|
|
||||||
if (!document.documentPath) {
|
if (!document.documentPath) {
|
||||||
res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' });
|
res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' });
|
||||||
@@ -334,6 +386,11 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customerId = parseInt(req.params.id);
|
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}`;
|
const relativePath = `/uploads/business-registrations/${req.file.filename}`;
|
||||||
|
|
||||||
// Alte Datei löschen falls vorhanden
|
// Alte Datei löschen falls vorhanden
|
||||||
@@ -383,6 +440,11 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customerId = parseInt(req.params.id);
|
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}`;
|
const relativePath = `/uploads/commercial-registers/${req.file.filename}`;
|
||||||
|
|
||||||
// Alte Datei löschen falls vorhanden
|
// Alte Datei löschen falls vorhanden
|
||||||
@@ -424,6 +486,8 @@ router.delete(
|
|||||||
async (req: AuthRequest, res: Response) => {
|
async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const customerId = parseInt(req.params.id);
|
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 } });
|
const customer = await prisma.customer.findUnique({ where: { id: customerId } });
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
@@ -464,6 +528,8 @@ router.delete(
|
|||||||
async (req: AuthRequest, res: Response) => {
|
async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const customerId = parseInt(req.params.id);
|
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 } });
|
const customer = await prisma.customer.findUnique({ where: { id: customerId } });
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
@@ -514,6 +580,14 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customerId = parseInt(req.params.id);
|
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}`;
|
const relativePath = `/uploads/privacy-policies/${req.file.filename}`;
|
||||||
|
|
||||||
// Alte Datei löschen falls vorhanden
|
// Alte Datei löschen falls vorhanden
|
||||||
@@ -574,6 +648,8 @@ router.delete(
|
|||||||
async (req: AuthRequest, res: Response) => {
|
async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const customerId = parseInt(req.params.id);
|
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 } });
|
const customer = await prisma.customer.findUnique({ where: { id: customerId } });
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
@@ -846,14 +922,20 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const invoiceId = parseInt(req.params.id);
|
const invoiceId = parseInt(req.params.id);
|
||||||
const relativePath = `/uploads/invoices/${req.file.filename}`;
|
// Pentest 56.1: Existenz- und Ownership-Check VOR DB-Update.
|
||||||
|
|
||||||
// Alte Datei löschen falls vorhanden
|
|
||||||
const invoice = await prisma.invoice.findUnique({ where: { id: invoiceId } });
|
const invoice = await prisma.invoice.findUnique({ where: { id: invoiceId } });
|
||||||
if (!invoice) {
|
if (!invoice) {
|
||||||
|
cleanupFile(req.file.path);
|
||||||
res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' });
|
res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' });
|
||||||
return;
|
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) {
|
if (invoice.documentPath) {
|
||||||
const oldPath = path.join(process.cwd(), invoice.documentPath);
|
const oldPath = path.join(process.cwd(), invoice.documentPath);
|
||||||
@@ -898,6 +980,9 @@ router.delete(
|
|||||||
res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' });
|
res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' });
|
||||||
return;
|
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) {
|
if (!invoice.documentPath) {
|
||||||
res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' });
|
res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' });
|
||||||
|
|||||||
Reference in New Issue
Block a user