25681075b4
24.6 (Portal kann Consent auf PENDING zurücksetzen): - gdpr.controller updateCustomerConsent prüft jetzt explizit, dass der Portal-User nur GRANTED oder WITHDRAWN setzen kann. PENDING ist nur der initiale System-Status; ein Reset darauf hätte die DSGVO-Auswertung verfälscht. 26.7 (documentPath ohne Validierung): - Neuer Helper isValidDocumentPath + assertValidDocumentPath in utils/sanitize: nur /?uploads/<safe>, keine "..", keine javascript:/data:/vbscript:, kein HTML. - consent.service.updateConsent ruft den Assert auf – Defense-in- Depth gegen zukünftige Caller, die documentPath aus User-Input durchreichen könnten. - authorization.service.grantAuthorization analog. - Cleanup-Skript (prisma/cleanup-xss-and-mass-assignment) entfernt seine lokale Kopie der Path-Validierung und nutzt den shared Helper – Single Source of Truth. 27.1 (Altdaten in Staging-DB): - Cleanup-Skript läuft sowieso bei jedem Container-Start. Nina- Records mit "../../../etc/passwd" werden beim nächsten Restart genullt (oder verschwinden mit dem VM-Snapshot-Wechsel). Live-Test isValidDocumentPath: 13/13 OK – legitime Pfade durch, Traversal/JS-URI/HTML blockiert. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
233 lines
6.7 KiB
TypeScript
233 lines
6.7 KiB
TypeScript
import prisma from '../lib/prisma.js';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { assertValidDocumentPath } from '../utils/sanitize.js';
|
|
|
|
/**
|
|
* Vollmachten für einen Kunden abrufen (wer darf diesen Kunden einsehen?)
|
|
*/
|
|
export async function getAuthorizationsForCustomer(customerId: number) {
|
|
return prisma.representativeAuthorization.findMany({
|
|
where: { customerId },
|
|
include: {
|
|
representative: {
|
|
select: { id: true, customerNumber: true, firstName: true, lastName: true },
|
|
},
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Vollmachten die ein Vertreter erhalten hat (welche Kunden darf er einsehen?)
|
|
*/
|
|
export async function getAuthorizationsForRepresentative(representativeId: number) {
|
|
return prisma.representativeAuthorization.findMany({
|
|
where: { representativeId },
|
|
include: {
|
|
customer: {
|
|
select: { id: true, customerNumber: true, firstName: true, lastName: true },
|
|
},
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Prüft ob ein Vertreter eine Vollmacht für einen Kunden hat
|
|
*/
|
|
export async function hasAuthorization(customerId: number, representativeId: number): Promise<boolean> {
|
|
const auth = await prisma.representativeAuthorization.findUnique({
|
|
where: {
|
|
customerId_representativeId: { customerId, representativeId },
|
|
},
|
|
});
|
|
|
|
return auth?.isGranted === true;
|
|
}
|
|
|
|
/**
|
|
* Vollmacht erteilen oder aktualisieren
|
|
*/
|
|
export async function grantAuthorization(
|
|
customerId: number,
|
|
representativeId: number,
|
|
data: { source?: string; documentPath?: string; notes?: string }
|
|
) {
|
|
// Pentest 26.7 (Defense-in-Depth): documentPath nur als /uploads/<safe>.
|
|
assertValidDocumentPath(data.documentPath, 'documentPath');
|
|
return prisma.representativeAuthorization.upsert({
|
|
where: {
|
|
customerId_representativeId: { customerId, representativeId },
|
|
},
|
|
update: {
|
|
isGranted: true,
|
|
grantedAt: new Date(),
|
|
withdrawnAt: null,
|
|
source: data.source,
|
|
documentPath: data.documentPath ?? undefined,
|
|
notes: data.notes ?? undefined,
|
|
},
|
|
create: {
|
|
customerId,
|
|
representativeId,
|
|
isGranted: true,
|
|
grantedAt: new Date(),
|
|
source: data.source || 'crm-backend',
|
|
documentPath: data.documentPath,
|
|
notes: data.notes,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Vollmacht widerrufen + PDF löschen falls vorhanden
|
|
*/
|
|
export async function withdrawAuthorization(customerId: number, representativeId: number) {
|
|
// Erst prüfen ob eine PDF vorhanden ist
|
|
const existing = await prisma.representativeAuthorization.findUnique({
|
|
where: { customerId_representativeId: { customerId, representativeId } },
|
|
select: { documentPath: true },
|
|
});
|
|
|
|
// PDF vom Filesystem löschen
|
|
if (existing?.documentPath) {
|
|
try {
|
|
const filePath = path.join(process.cwd(), existing.documentPath);
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
}
|
|
} catch (err) {
|
|
console.error('Fehler beim Löschen der Vollmacht-PDF:', err);
|
|
}
|
|
}
|
|
|
|
return prisma.representativeAuthorization.update({
|
|
where: {
|
|
customerId_representativeId: { customerId, representativeId },
|
|
},
|
|
data: {
|
|
isGranted: false,
|
|
withdrawnAt: new Date(),
|
|
documentPath: null,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Vollmacht-Dokument (PDF) hochladen
|
|
*/
|
|
export async function updateAuthorizationDocument(
|
|
customerId: number,
|
|
representativeId: number,
|
|
documentPath: string
|
|
) {
|
|
// Wenn Dokument hochgeladen wird, gilt das als Vollmacht erteilen
|
|
return prisma.representativeAuthorization.upsert({
|
|
where: {
|
|
customerId_representativeId: { customerId, representativeId },
|
|
},
|
|
update: {
|
|
documentPath,
|
|
isGranted: true,
|
|
grantedAt: new Date(),
|
|
withdrawnAt: null,
|
|
source: 'papier',
|
|
},
|
|
create: {
|
|
customerId,
|
|
representativeId,
|
|
documentPath,
|
|
isGranted: true,
|
|
grantedAt: new Date(),
|
|
source: 'papier',
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Vollmacht-Dokument löschen
|
|
*/
|
|
export async function deleteAuthorizationDocument(customerId: number, representativeId: number) {
|
|
// Prüfen ob die Vollmacht per Papier erteilt wurde
|
|
const auth = await prisma.representativeAuthorization.findUnique({
|
|
where: { customerId_representativeId: { customerId, representativeId } },
|
|
select: { source: true, documentPath: true },
|
|
});
|
|
|
|
if (!auth) throw new Error('Vollmacht nicht gefunden');
|
|
|
|
// Datei löschen
|
|
if (auth.documentPath) {
|
|
try {
|
|
const filePath = path.join(process.cwd(), auth.documentPath);
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
}
|
|
} catch (err) {
|
|
console.error('Fehler beim Löschen der Vollmacht-PDF:', err);
|
|
}
|
|
}
|
|
|
|
// Wenn per Papier erteilt → Vollmacht widerrufen
|
|
// Wenn per Portal/Online erteilt → nur PDF entfernen, Vollmacht bleibt
|
|
const withdrawData = auth.source === 'papier'
|
|
? { documentPath: null, isGranted: false, withdrawnAt: new Date() }
|
|
: { documentPath: null };
|
|
|
|
return prisma.representativeAuthorization.update({
|
|
where: { customerId_representativeId: { customerId, representativeId } },
|
|
data: withdrawData,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Alle genehmigten Vertreter-IDs für einen Kunden
|
|
* (Welche Vertreter dürfen die Verträge dieses Kunden sehen?)
|
|
*/
|
|
export async function getAuthorizedRepresentativeIds(customerId: number): Promise<number[]> {
|
|
const auths = await prisma.representativeAuthorization.findMany({
|
|
where: { customerId, isGranted: true },
|
|
select: { representativeId: true },
|
|
});
|
|
return auths.map((a) => a.representativeId);
|
|
}
|
|
|
|
/**
|
|
* Alle Kunden-IDs für die ein Vertreter eine Vollmacht hat
|
|
*/
|
|
export async function getAuthorizedCustomerIds(representativeId: number): Promise<number[]> {
|
|
const auths = await prisma.representativeAuthorization.findMany({
|
|
where: { representativeId, isGranted: true },
|
|
select: { customerId: true },
|
|
});
|
|
return auths.map((a) => a.customerId);
|
|
}
|
|
|
|
/**
|
|
* Erstellt fehlende Vollmacht-Einträge für bestehende Vertreterbeziehungen
|
|
* (wird aufgerufen wenn man den Tab aufruft)
|
|
*/
|
|
export async function ensureAuthorizationEntries(customerId: number) {
|
|
// Alle aktiven Vertreter für diesen Kunden
|
|
const representatives = await prisma.customerRepresentative.findMany({
|
|
where: { customerId, isActive: true },
|
|
select: { representativeId: true },
|
|
});
|
|
|
|
for (const rep of representatives) {
|
|
// Erstelle Eintrag falls nicht vorhanden
|
|
await prisma.representativeAuthorization.upsert({
|
|
where: {
|
|
customerId_representativeId: { customerId, representativeId: rep.representativeId },
|
|
},
|
|
update: {}, // Nichts ändern wenn schon vorhanden
|
|
create: {
|
|
customerId,
|
|
representativeId: rep.representativeId,
|
|
isGranted: false,
|
|
},
|
|
});
|
|
}
|
|
}
|