gdpr audit implemented, email log, vollmachten, pdf delete cancel data privacy and vollmachten, removed message no id card in engergy car, and other contracts that are not telecom contracts, added insert counter for engery

This commit is contained in:
2026-03-21 11:59:53 +01:00
parent 89cf92eaf5
commit f2876f877e
1491 changed files with 265550 additions and 1292 deletions
@@ -0,0 +1,201 @@
import { Response } from 'express';
import { AuthRequest } from '../types/index.js';
import * as auditService from '../services/audit.service.js';
import { AuditAction, AuditSensitivity } from '@prisma/client';
/**
* Audit-Logs mit Filtern abrufen
*/
export async function getAuditLogs(req: AuthRequest, res: Response) {
try {
const {
userId,
customerId,
dataSubjectId,
action,
sensitivity,
resourceType,
resourceId,
startDate,
endDate,
success,
search,
page,
limit,
} = req.query;
const result = await auditService.searchAuditLogs({
userId: userId ? parseInt(userId as string) : undefined,
customerId: customerId ? parseInt(customerId as string) : undefined,
dataSubjectId: dataSubjectId ? parseInt(dataSubjectId as string) : undefined,
action: action as AuditAction | undefined,
sensitivity: sensitivity as AuditSensitivity | undefined,
resourceType: resourceType as string | undefined,
resourceId: resourceId as string | undefined,
startDate: startDate ? new Date(startDate as string) : undefined,
endDate: endDate ? new Date(endDate as string) : undefined,
success: success !== undefined ? success === 'true' : undefined,
search: search as string | undefined,
page: page ? parseInt(page as string) : 1,
limit: limit ? parseInt(limit as string) : 50,
});
res.json({ success: true, ...result });
} catch (error) {
console.error('Fehler beim Abrufen der Audit-Logs:', error);
res.status(500).json({ success: false, error: 'Fehler beim Abrufen der Audit-Logs' });
}
}
/**
* Einzelnes Audit-Log abrufen
*/
export async function getAuditLogById(req: AuthRequest, res: Response) {
try {
const id = parseInt(req.params.id);
const log = await auditService.getAuditLogById(id);
if (!log) {
return res.status(404).json({ success: false, error: 'Audit-Log nicht gefunden' });
}
res.json({ success: true, data: log });
} catch (error) {
console.error('Fehler beim Abrufen des Audit-Logs:', error);
res.status(500).json({ success: false, error: 'Fehler beim Abrufen des Audit-Logs' });
}
}
/**
* Audit-Logs für einen Kunden abrufen (DSGVO)
*/
export async function getAuditLogsByCustomer(req: AuthRequest, res: Response) {
try {
const customerId = parseInt(req.params.customerId);
const logs = await auditService.getAuditLogsByDataSubject(customerId);
res.json({ success: true, data: logs });
} catch (error) {
console.error('Fehler beim Abrufen der Kunden-Audit-Logs:', error);
res.status(500).json({ success: false, error: 'Fehler beim Abrufen der Audit-Logs' });
}
}
/**
* Audit-Logs exportieren
*/
export async function exportAuditLogs(req: AuthRequest, res: Response) {
try {
const format = (req.query.format as 'json' | 'csv') || 'json';
const {
action,
sensitivity,
resourceType,
startDate,
endDate,
} = req.query;
const content = await auditService.exportAuditLogs(
{
action: action as AuditAction | undefined,
sensitivity: sensitivity as AuditSensitivity | undefined,
resourceType: resourceType as string | undefined,
startDate: startDate ? new Date(startDate as string) : undefined,
endDate: endDate ? new Date(endDate as string) : undefined,
},
format
);
const contentType = format === 'csv' ? 'text/csv' : 'application/json';
const filename = `audit-logs-${new Date().toISOString().split('T')[0]}.${format}`;
res.setHeader('Content-Type', contentType);
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.send(content);
} catch (error) {
console.error('Fehler beim Exportieren der Audit-Logs:', error);
res.status(500).json({ success: false, error: 'Fehler beim Exportieren' });
}
}
/**
* Hash-Ketten-Integrität prüfen
*/
export async function verifyIntegrity(req: AuthRequest, res: Response) {
try {
const { fromId, toId } = req.query;
const result = await auditService.verifyIntegrity(
fromId ? parseInt(fromId as string) : undefined,
toId ? parseInt(toId as string) : undefined
);
res.json({
success: true,
data: {
valid: result.valid,
checkedCount: result.checkedCount,
invalidEntries: result.invalidEntries,
message: result.valid
? 'Alle Einträge sind valide'
: `${result.invalidEntries.length} manipulierte Einträge gefunden`,
},
});
} catch (error) {
console.error('Fehler bei der Integritätsprüfung:', error);
res.status(500).json({ success: false, error: 'Fehler bei der Integritätsprüfung' });
}
}
/**
* Retention-Policies abrufen
*/
export async function getRetentionPolicies(req: AuthRequest, res: Response) {
try {
const policies = await auditService.getRetentionPolicies();
res.json({ success: true, data: policies });
} catch (error) {
console.error('Fehler beim Abrufen der Retention-Policies:', error);
res.status(500).json({ success: false, error: 'Fehler beim Abrufen der Policies' });
}
}
/**
* Retention-Policy aktualisieren
*/
export async function updateRetentionPolicy(req: AuthRequest, res: Response) {
try {
const id = parseInt(req.params.id);
const { retentionDays, description, legalBasis, isActive } = req.body;
const policy = await auditService.updateRetentionPolicy(id, {
retentionDays,
description,
legalBasis,
isActive,
});
res.json({ success: true, data: policy });
} catch (error) {
console.error('Fehler beim Aktualisieren der Retention-Policy:', error);
res.status(500).json({ success: false, error: 'Fehler beim Aktualisieren' });
}
}
/**
* Retention-Cleanup manuell ausführen
*/
export async function runRetentionCleanup(req: AuthRequest, res: Response) {
try {
const result = await auditService.runRetentionCleanup();
res.json({
success: true,
data: result,
message: `${result.deletedCount} alte Audit-Logs wurden gelöscht`,
});
} catch (error) {
console.error('Fehler beim Retention-Cleanup:', error);
res.status(500).json({ success: false, error: 'Fehler beim Cleanup' });
}
}
@@ -317,7 +317,12 @@ export async function sendEmailFromAccount(req: Request, res: Response): Promise
};
// E-Mail senden
const result = await sendEmail(credentials, stressfreiEmail.email, emailParams);
const authReq = req as any;
const result = await sendEmail(credentials, stressfreiEmail.email, emailParams, {
context: 'customer-email',
customerId: stressfreiEmail.customerId,
triggeredBy: authReq.user?.email,
});
if (!result.success) {
res.status(400).json({
@@ -0,0 +1,107 @@
import { Request, Response } from 'express';
import * as consentPublicService from '../services/consent-public.service.js';
import { createAuditLog } from '../services/audit.service.js';
import { CONSENT_TYPE_LABELS } from '../services/consent.service.js';
import { ConsentType } from '@prisma/client';
/**
* Öffentliche Consent-Seite: Kundendaten + Datenschutztext + Status
*/
export async function getConsentPage(req: Request, res: Response) {
try {
const { hash } = req.params;
const result = await consentPublicService.getCustomerByConsentHash(hash);
if (!result) {
return res.status(404).json({ success: false, error: 'Ungültiger Link' });
}
const privacyPolicyHtml = await consentPublicService.getPrivacyPolicyHtml(result.customer.id);
// Consent-Status mit Labels
const consentsWithLabels = result.consents.map((c) => ({
consentType: c.consentType,
status: c.status,
label: CONSENT_TYPE_LABELS[c.consentType as ConsentType]?.label || c.consentType,
description: CONSENT_TYPE_LABELS[c.consentType as ConsentType]?.description || '',
grantedAt: c.grantedAt,
}));
res.json({
success: true,
data: {
customer: {
firstName: result.customer.firstName,
lastName: result.customer.lastName,
customerNumber: result.customer.customerNumber,
},
privacyPolicyHtml,
consents: consentsWithLabels,
allGranted: consentsWithLabels.every((c) => c.status === 'GRANTED'),
},
});
} catch (error) {
console.error('Fehler beim Laden der Consent-Seite:', error);
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
}
}
/**
* Alle 4 Einwilligungen erteilen (öffentlicher Link)
*/
export async function grantAllConsents(req: Request, res: Response) {
try {
const { hash } = req.params;
const ipAddress = req.ip || req.socket.remoteAddress || 'unknown';
const results = await consentPublicService.grantAllConsentsPublic(hash, ipAddress);
// Audit-Log (manuell, da keine Auth-Middleware)
const customer = await consentPublicService.getCustomerByConsentHash(hash);
if (customer) {
for (const type of Object.values(ConsentType)) {
await createAuditLog({
userEmail: customer.customer.email || 'public-link',
action: 'UPDATE',
sensitivity: 'HIGH',
resourceType: 'CustomerConsent',
resourceId: `${customer.customer.id}:${type}`,
resourceLabel: `Einwilligung ${type} erteilt via Public-Link`,
endpoint: `/api/public/consent/${hash}/grant`,
httpMethod: 'POST',
ipAddress,
dataSubjectId: customer.customer.id,
legalBasis: 'DSGVO Art. 6 Abs. 1 lit. a',
});
}
}
res.json({ success: true, data: results });
} catch (error: any) {
console.error('Fehler beim Erteilen der Einwilligungen:', error);
res.status(400).json({ success: false, error: error.message || 'Fehler beim Erteilen' });
}
}
/**
* Datenschutzerklärung als PDF
*/
export async function getConsentPdf(req: Request, res: Response) {
try {
const { hash } = req.params;
const result = await consentPublicService.getCustomerByConsentHash(hash);
if (!result) {
return res.status(404).json({ success: false, error: 'Ungültiger Link' });
}
const pdfBuffer = await consentPublicService.generateConsentPdf(result.customer.id);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'inline; filename="datenschutzerklaerung.pdf"');
res.send(pdfBuffer);
} catch (error) {
console.error('Fehler beim Generieren des PDFs:', error);
res.status(500).json({ success: false, error: 'Fehler beim Generieren' });
}
}
+21 -5
View File
@@ -3,6 +3,7 @@ import { PrismaClient } from '@prisma/client';
import * as contractService from '../services/contract.service.js';
import * as contractCockpitService from '../services/contractCockpit.service.js';
import * as contractHistoryService from '../services/contractHistory.service.js';
import * as authorizationService from '../services/authorization.service.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
const prisma = new PrismaClient();
@@ -20,11 +21,19 @@ export async function getContracts(req: AuthRequest, res: Response): Promise<voi
return;
}
// Für Kundenportal-Benutzer: nur eigene + vertretene Kunden-Verträge anzeigen
// Für Kundenportal-Benutzer: nur eigene + vertretene Kunden MIT Vollmacht
let customerIds: number[] | undefined;
if (req.user?.isCustomerPortal && req.user.customerId) {
// Eigene Customer-ID + alle vertretenen Kunden-IDs
customerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
// Eigene Customer-ID immer
customerIds = [req.user.customerId];
// Vertretene Kunden nur wenn Vollmacht erteilt
const representedIds: number[] = req.user.representedCustomerIds || [];
for (const repCustId of representedIds) {
const hasAuth = await authorizationService.hasAuthorization(repCustId, req.user.customerId);
if (hasAuth) {
customerIds.push(repCustId);
}
}
}
const result = await contractService.getAllContracts({
@@ -60,9 +69,16 @@ export async function getContract(req: AuthRequest, res: Response): Promise<void
return;
}
// Für Kundenportal-Benutzer: Zugriff nur auf eigene + vertretene Kunden-Verträge
// Für Kundenportal-Benutzer: Zugriff nur auf eigene + vertretene Kunden MIT Vollmacht
if (req.user?.isCustomerPortal && req.user.customerId) {
const allowedCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
const allowedCustomerIds = [req.user.customerId];
const representedIds: number[] = req.user.representedCustomerIds || [];
for (const repCustId of representedIds) {
const hasAuth = await authorizationService.hasAuthorization(repCustId, req.user.customerId);
if (hasAuth) {
allowedCustomerIds.push(repCustId);
}
}
if (!allowedCustomerIds.includes(contract.customerId)) {
res.status(403).json({
success: false,
+96 -1
View File
@@ -1,7 +1,10 @@
import { Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import * as customerService from '../services/customer.service.js';
import * as authService from '../services/auth.service.js';
import { ApiResponse } from '../types/index.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
const prisma = new PrismaClient();
// Customer CRUD
export async function getCustomers(req: Request, res: Response): Promise<void> {
@@ -331,6 +334,98 @@ export async function deleteMeterReading(req: Request, res: Response): Promise<v
}
}
// ==================== PORTAL: ZÄHLERSTAND MELDEN ====================
export async function reportMeterReading(req: AuthRequest, res: Response): Promise<void> {
try {
const user = req.user as any;
if (!user?.isCustomerPortal || !user?.customerId) {
res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' } as ApiResponse);
return;
}
const meterId = parseInt(req.params.meterId);
const { value, readingDate, notes } = req.body;
// Prüfe ob der Zähler zum Kunden gehört
const meter = await prisma.meter.findUnique({
where: { id: meterId },
select: { customerId: true },
});
if (!meter || meter.customerId !== user.customerId) {
res.status(403).json({ success: false, error: 'Kein Zugriff auf diesen Zähler' } as ApiResponse);
return;
}
const reading = await prisma.meterReading.create({
data: {
meterId,
value: parseFloat(value),
readingDate: readingDate ? new Date(readingDate) : new Date(),
notes,
reportedBy: user.email,
status: 'REPORTED',
},
});
res.status(201).json({ success: true, data: reading } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Melden des Zählerstands',
} as ApiResponse);
}
}
export async function getMyMeters(req: AuthRequest, res: Response): Promise<void> {
try {
const user = req.user as any;
if (!user?.isCustomerPortal || !user?.customerId) {
res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' } as ApiResponse);
return;
}
const meters = await prisma.meter.findMany({
where: { customerId: user.customerId, isActive: true },
include: {
readings: {
orderBy: { readingDate: 'desc' },
take: 5,
},
},
orderBy: { createdAt: 'asc' },
});
res.json({ success: true, data: meters } as ApiResponse);
} catch (error) {
res.status(500).json({ success: false, error: 'Fehler beim Laden der Zähler' } as ApiResponse);
}
}
export async function markReadingTransferred(req: AuthRequest, res: Response): Promise<void> {
try {
const meterId = parseInt(req.params.meterId);
const readingId = parseInt(req.params.readingId);
const reading = await prisma.meterReading.update({
where: { id: readingId },
data: {
status: 'TRANSFERRED',
transferredAt: new Date(),
transferredBy: req.user?.email,
},
});
res.json({ success: true, data: reading } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren',
} as ApiResponse);
}
}
// ==================== PORTAL SETTINGS ====================
export async function getPortalSettings(req: Request, res: Response): Promise<void> {
@@ -0,0 +1,43 @@
import { Response } from 'express';
import { AuthRequest } from '../types/index.js';
import * as emailLogService from '../services/emailLog.service.js';
export async function getEmailLogs(req: AuthRequest, res: Response) {
try {
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 50;
const success = req.query.success !== undefined ? req.query.success === 'true' : undefined;
const search = req.query.search as string || undefined;
const context = req.query.context as string || undefined;
const result = await emailLogService.getEmailLogs({ page, limit, success, search, context });
res.json({ success: true, ...result });
} catch (error) {
console.error('Fehler beim Laden der Email-Logs:', error);
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
}
}
export async function getEmailLogStats(req: AuthRequest, res: Response) {
try {
const stats = await emailLogService.getEmailLogStats();
res.json({ success: true, data: stats });
} catch (error) {
console.error('Fehler beim Laden der Email-Log-Stats:', error);
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
}
}
export async function getEmailLogDetail(req: AuthRequest, res: Response) {
try {
const id = parseInt(req.params.id);
const log = await emailLogService.getEmailLogById(id);
if (!log) {
return res.status(404).json({ success: false, error: 'Log-Eintrag nicht gefunden' });
}
res.json({ success: true, data: log });
} catch (error) {
console.error('Fehler beim Laden des Email-Log-Details:', error);
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
}
}
+899
View File
@@ -0,0 +1,899 @@
import { Response } from 'express';
import { AuthRequest } from '../types/index.js';
import * as gdprService from '../services/gdpr.service.js';
import * as consentService from '../services/consent.service.js';
import * as consentPublicService from '../services/consent-public.service.js';
import * as appSettingService from '../services/appSetting.service.js';
import { createAuditLog } from '../services/audit.service.js';
import { ConsentType, DeletionRequestStatus, PrismaClient } from '@prisma/client';
import path from 'path';
import fs from 'fs';
import { sendEmail, SmtpCredentials } from '../services/smtpService.js';
import { getSystemEmailCredentials } from '../services/emailProvider/emailProviderService.js';
import * as authorizationService from '../services/authorization.service.js';
const prisma = new PrismaClient();
/**
* Kundendaten exportieren (DSGVO Art. 15)
*/
export async function exportCustomerData(req: AuthRequest, res: Response) {
try {
const customerId = parseInt(req.params.customerId);
const format = (req.query.format as string) || 'json';
const data = await gdprService.exportCustomerData(customerId);
// Audit-Log für Export
await createAuditLog({
userId: req.user?.userId,
userEmail: req.user?.email || 'unknown',
action: 'EXPORT',
resourceType: 'GDPR',
resourceId: customerId.toString(),
resourceLabel: `Datenexport für ${data.dataSubject.name}`,
endpoint: req.path,
httpMethod: req.method,
ipAddress: req.socket.remoteAddress || 'unknown',
dataSubjectId: customerId,
legalBasis: 'DSGVO Art. 15',
});
if (format === 'json') {
res.setHeader('Content-Type', 'application/json');
res.setHeader(
'Content-Disposition',
`attachment; filename="datenexport_${data.dataSubject.customerNumber}_${new Date().toISOString().split('T')[0]}.json"`
);
res.json(data);
} else {
// Für PDF würde hier PDFKit verwendet werden
res.json({ success: true, data });
}
} catch (error) {
console.error('Fehler beim Datenexport:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Datenexport',
});
}
}
/**
* Löschanfrage erstellen
*/
export async function createDeletionRequest(req: AuthRequest, res: Response) {
try {
const customerId = parseInt(req.params.id);
const { requestSource } = req.body;
const request = await gdprService.createDeletionRequest({
customerId,
requestSource: requestSource || 'portal',
requestedBy: req.user?.email || 'unknown',
});
res.status(201).json({ success: true, data: request });
} catch (error) {
console.error('Fehler beim Erstellen der Löschanfrage:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen',
});
}
}
/**
* Alle Löschanfragen abrufen
*/
export async function getDeletionRequests(req: AuthRequest, res: Response) {
try {
const { status, page, limit } = req.query;
const result = await gdprService.getDeletionRequests({
status: status as DeletionRequestStatus | undefined,
page: page ? parseInt(page as string) : 1,
limit: limit ? parseInt(limit as string) : 20,
});
res.json({ success: true, ...result });
} catch (error) {
console.error('Fehler beim Abrufen der Löschanfragen:', error);
res.status(500).json({ success: false, error: 'Fehler beim Abrufen' });
}
}
/**
* Einzelne Löschanfrage abrufen
*/
export async function getDeletionRequest(req: AuthRequest, res: Response) {
try {
const id = parseInt(req.params.id);
const request = await gdprService.getDeletionRequest(id);
if (!request) {
return res.status(404).json({ success: false, error: 'Löschanfrage nicht gefunden' });
}
res.json({ success: true, data: request });
} catch (error) {
console.error('Fehler beim Abrufen der Löschanfrage:', error);
res.status(500).json({ success: false, error: 'Fehler beim Abrufen' });
}
}
/**
* Löschanfrage bearbeiten
*/
export async function processDeletionRequest(req: AuthRequest, res: Response) {
try {
const id = parseInt(req.params.id);
const { action, retentionReason } = req.body;
if (!['complete', 'partial', 'reject'].includes(action)) {
return res.status(400).json({
success: false,
error: 'Ungültige Aktion. Erlaubt: complete, partial, reject',
});
}
const result = await gdprService.processDeletionRequest(id, {
processedBy: req.user?.email || 'unknown',
action,
retentionReason,
});
// Audit-Log für Löschung
await createAuditLog({
userId: req.user?.userId,
userEmail: req.user?.email || 'unknown',
action: 'ANONYMIZE',
resourceType: 'GDPR',
resourceId: id.toString(),
resourceLabel: `Löschanfrage ${action}`,
endpoint: req.path,
httpMethod: req.method,
ipAddress: req.socket.remoteAddress || 'unknown',
dataSubjectId: result.customerId,
legalBasis: 'DSGVO Art. 17',
});
res.json({ success: true, data: result });
} catch (error) {
console.error('Fehler beim Bearbeiten der Löschanfrage:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Bearbeiten',
});
}
}
/**
* Löschnachweis-PDF herunterladen
*/
export async function getDeletionProof(req: AuthRequest, res: Response) {
try {
const id = parseInt(req.params.id);
const request = await gdprService.getDeletionRequest(id);
if (!request) {
return res.status(404).json({ success: false, error: 'Löschanfrage nicht gefunden' });
}
if (!request.proofDocument) {
return res.status(404).json({ success: false, error: 'Kein Löschnachweis vorhanden' });
}
const filepath = path.join(process.cwd(), 'uploads', request.proofDocument);
if (!fs.existsSync(filepath)) {
return res.status(404).json({ success: false, error: 'Datei nicht gefunden' });
}
res.download(filepath);
} catch (error) {
console.error('Fehler beim Download des Löschnachweises:', error);
res.status(500).json({ success: false, error: 'Fehler beim Download' });
}
}
/**
* DSGVO-Dashboard Statistiken
*/
export async function getDashboardStats(req: AuthRequest, res: Response) {
try {
const stats = await gdprService.getGDPRDashboardStats();
res.json({ success: true, data: stats });
} catch (error) {
console.error('Fehler beim Abrufen der Dashboard-Statistiken:', error);
res.status(500).json({ success: false, error: 'Fehler beim Abrufen' });
}
}
// ==================== CONSENT ENDPOINTS ====================
/**
* Einwilligungen eines Kunden abrufen
*/
export async function getCustomerConsents(req: AuthRequest, res: Response) {
try {
const customerId = parseInt(req.params.customerId);
const consents = await consentService.getCustomerConsents(customerId);
// Labels hinzufügen
const consentsWithLabels = consents.map((c) => ({
...c,
label: consentService.CONSENT_TYPE_LABELS[c.consentType as ConsentType]?.label,
description: consentService.CONSENT_TYPE_LABELS[c.consentType as ConsentType]?.description,
}));
res.json({ success: true, data: consentsWithLabels });
} catch (error) {
console.error('Fehler beim Abrufen der Einwilligungen:', error);
res.status(500).json({ success: false, error: 'Fehler beim Abrufen' });
}
}
/**
* Consent-Status prüfen (hat der Kunde vollständig zugestimmt?)
*/
export async function checkConsentStatus(req: AuthRequest, res: Response) {
try {
const customerId = parseInt(req.params.customerId);
const result = await consentService.hasFullConsent(customerId);
res.json({ success: true, data: result });
} catch (error) {
console.error('Fehler beim Consent-Check:', error);
res.status(500).json({ success: false, error: 'Fehler beim Consent-Check' });
}
}
/**
* Einwilligung aktualisieren (nur Kundenportal-Benutzer!)
*/
export async function updateCustomerConsent(req: AuthRequest, res: Response) {
try {
const customerId = parseInt(req.params.customerId);
const consentType = req.params.consentType as ConsentType;
const { status, source, documentPath, version } = req.body;
// Nur Kundenportal-Benutzer dürfen Einwilligungen ändern
if (!(req.user as any)?.isCustomerPortal) {
return res.status(403).json({
success: false,
error: 'Nur Kunden können Einwilligungen ändern',
});
}
// Portal: nur eigene + vertretene Kunden
const allowed = [
(req.user as any).customerId,
...((req.user as any).representedCustomerIds || []),
];
if (!allowed.includes(customerId)) {
return res.status(403).json({
success: false,
error: 'Keine Berechtigung für diesen Kunden',
});
}
if (!Object.values(ConsentType).includes(consentType)) {
return res.status(400).json({ success: false, error: 'Ungültiger Consent-Typ' });
}
const consent = await consentService.updateConsent(customerId, consentType, {
status,
source: source || 'portal',
documentPath,
version,
ipAddress: req.socket.remoteAddress,
createdBy: req.user?.email || 'unknown',
});
res.json({ success: true, data: consent });
} catch (error) {
console.error('Fehler beim Aktualisieren der Einwilligung:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren',
});
}
}
/**
* Consent-Übersicht für Dashboard
*/
export async function getConsentOverview(req: AuthRequest, res: Response) {
try {
const overview = await consentService.getConsentOverview();
// Labels hinzufügen
const overviewWithLabels = Object.entries(overview).map(([type, stats]) => ({
type,
label: consentService.CONSENT_TYPE_LABELS[type as ConsentType]?.label,
description: consentService.CONSENT_TYPE_LABELS[type as ConsentType]?.description,
...stats,
}));
res.json({ success: true, data: overviewWithLabels });
} catch (error) {
console.error('Fehler beim Abrufen der Consent-Übersicht:', error);
res.status(500).json({ success: false, error: 'Fehler beim Abrufen' });
}
}
// ==================== PRIVACY POLICY ENDPOINTS ====================
/**
* Datenschutzerklärung abrufen (HTML)
*/
export async function getPrivacyPolicy(req: AuthRequest, res: Response) {
try {
const html = await appSettingService.getSetting('privacyPolicyHtml');
res.json({ success: true, data: { html: html || '' } });
} catch (error) {
console.error('Fehler beim Abrufen der Datenschutzerklärung:', error);
res.status(500).json({ success: false, error: 'Fehler beim Abrufen' });
}
}
/**
* Datenschutzerklärung speichern (HTML)
*/
export async function updatePrivacyPolicy(req: AuthRequest, res: Response) {
try {
const { html } = req.body;
if (typeof html !== 'string') {
return res.status(400).json({ success: false, error: 'HTML-Inhalt erforderlich' });
}
await appSettingService.setSetting('privacyPolicyHtml', html);
res.json({ success: true, message: 'Datenschutzerklärung gespeichert' });
} catch (error) {
console.error('Fehler beim Speichern der Datenschutzerklärung:', error);
res.status(500).json({ success: false, error: 'Fehler beim Speichern' });
}
}
/**
* Vollmacht-Vorlage abrufen (HTML)
*/
export async function getAuthorizationTemplate(req: AuthRequest, res: Response) {
try {
const html = await appSettingService.getSetting('authorizationTemplateHtml');
res.json({ success: true, data: { html: html || '' } });
} catch (error) {
console.error('Fehler beim Abrufen der Vollmacht-Vorlage:', error);
res.status(500).json({ success: false, error: 'Fehler beim Abrufen' });
}
}
/**
* Vollmacht-Vorlage speichern (HTML)
*/
export async function updateAuthorizationTemplate(req: AuthRequest, res: Response) {
try {
const { html } = req.body;
if (typeof html !== 'string') {
return res.status(400).json({ success: false, error: 'HTML-Inhalt erforderlich' });
}
await appSettingService.setSetting('authorizationTemplateHtml', html);
res.json({ success: true, message: 'Vollmacht-Vorlage gespeichert' });
} catch (error) {
console.error('Fehler beim Speichern der Vollmacht-Vorlage:', error);
res.status(500).json({ success: false, error: 'Fehler beim Speichern' });
}
}
// ==================== SEND CONSENT LINK ====================
/**
* Consent-Link an Kunden senden
*/
// ==================== PORTAL ENDPOINTS ====================
/**
* Portal: Eigene Datenschutzseite (Privacy Policy + Consent-Status)
*/
export async function getMyPrivacy(req: AuthRequest, res: Response) {
try {
const user = req.user as any;
if (!user?.isCustomerPortal || !user?.customerId) {
return res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
}
const customerId = user.customerId;
const [privacyPolicyHtml, consents] = await Promise.all([
consentPublicService.getPrivacyPolicyHtml(customerId),
consentService.getCustomerConsents(customerId),
]);
const consentsWithLabels = consents.map((c) => ({
...c,
label: consentService.CONSENT_TYPE_LABELS[c.consentType as ConsentType]?.label,
description: consentService.CONSENT_TYPE_LABELS[c.consentType as ConsentType]?.description,
}));
res.json({
success: true,
data: {
privacyPolicyHtml,
consents: consentsWithLabels,
},
});
} catch (error) {
console.error('Fehler beim Laden der Portal-Datenschutzseite:', error);
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
}
}
/**
* Portal: Datenschutzerklärung als PDF
*/
export async function getMyPrivacyPdf(req: AuthRequest, res: Response) {
try {
const user = req.user as any;
if (!user?.isCustomerPortal || !user?.customerId) {
return res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
}
const pdfBuffer = await consentPublicService.generateConsentPdf(user.customerId);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'inline; filename="datenschutzerklaerung.pdf"');
res.send(pdfBuffer);
} catch (error) {
console.error('Fehler beim Generieren des Portal-PDFs:', error);
res.status(500).json({ success: false, error: 'Fehler beim Generieren' });
}
}
/**
* Portal: Eigenen Consent-Status prüfen
*/
export async function getMyConsentStatus(req: AuthRequest, res: Response) {
try {
const user = req.user as any;
if (!user?.isCustomerPortal || !user?.customerId) {
return res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
}
const result = await consentService.hasFullConsent(user.customerId);
res.json({ success: true, data: result });
} catch (error) {
console.error('Fehler beim Consent-Status:', error);
res.status(500).json({ success: false, error: 'Fehler beim Consent-Status' });
}
}
export async function sendConsentLink(req: AuthRequest, res: Response) {
try {
const customerId = parseInt(req.params.customerId);
const { channel } = req.body; // 'email', 'whatsapp', 'telegram', 'signal'
// ConsentHash sicherstellen
const hash = await consentPublicService.ensureConsentHash(customerId);
const baseUrl = process.env.PUBLIC_URL || req.headers.origin || 'http://localhost:5173';
const consentUrl = `${baseUrl}/datenschutz/${hash}`;
// Bei E-Mail: tatsächlich senden
if (channel === 'email') {
// Kunde laden
const customer = await prisma.customer.findUnique({
where: { id: customerId },
select: { id: true, firstName: true, lastName: true, email: true },
});
if (!customer?.email) {
return res.status(400).json({
success: false,
error: 'Kunde hat keine E-Mail-Adresse hinterlegt',
});
}
// System-E-Mail-Credentials vom aktiven Provider holen
const systemEmail = await getSystemEmailCredentials();
if (!systemEmail) {
return res.status(400).json({
success: false,
error: 'Keine System-E-Mail konfiguriert. Bitte in den Email-Provider-Einstellungen eine System-E-Mail-Adresse und Passwort hinterlegen.',
});
}
const credentials: SmtpCredentials = {
host: systemEmail.smtpServer,
port: systemEmail.smtpPort,
user: systemEmail.emailAddress,
password: systemEmail.password,
encryption: systemEmail.smtpEncryption,
allowSelfSignedCerts: systemEmail.allowSelfSignedCerts,
};
// E-Mail zusammenstellen
const emailHtml = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #1e40af;">Datenschutzerklärung Ihre Zustimmung</h2>
<p>Sehr geehrte(r) ${customer.firstName} ${customer.lastName},</p>
<p>
um Sie optimal beraten und betreuen zu können, benötigen wir Ihre Zustimmung zu unserer Datenschutzerklärung.
</p>
<p>
Bitte klicken Sie auf den folgenden Button, um unsere Datenschutzerklärung einzusehen und Ihre Einwilligung zu erteilen:
</p>
<p style="text-align: center; margin: 32px 0;">
<a href="${consentUrl}"
style="background-color: #2563eb; color: #ffffff; padding: 14px 32px; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 16px; display: inline-block;">
Datenschutzerklärung ansehen &amp; zustimmen
</a>
</p>
<p style="color: #6b7280; font-size: 14px;">
Alternativ können Sie auch diesen Link in Ihren Browser kopieren:<br>
<a href="${consentUrl}" style="color: #2563eb; word-break: break-all;">${consentUrl}</a>
</p>
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 24px 0;">
<p style="color: #9ca3af; font-size: 12px;">
Hacker-Net Telekommunikation Stefan Hacker<br>
Am Wunderburgpark 5b, 26135 Oldenburg<br>
info@hacker-net.de
</p>
</div>
`;
const result = await sendEmail(credentials, systemEmail.emailAddress, {
to: customer.email,
subject: 'Datenschutzerklärung Bitte um Ihre Zustimmung',
html: emailHtml,
}, {
context: 'consent-link',
customerId,
triggeredBy: req.user?.email,
});
if (!result.success) {
return res.status(400).json({
success: false,
error: `E-Mail-Versand fehlgeschlagen: ${result.error}`,
});
}
}
// Audit-Log
await createAuditLog({
userId: req.user?.userId,
userEmail: req.user?.email || 'unknown',
action: 'READ',
resourceType: 'CustomerConsent',
resourceId: customerId.toString(),
resourceLabel: `Consent-Link gesendet (${channel})`,
endpoint: req.path,
httpMethod: req.method,
ipAddress: req.socket.remoteAddress || 'unknown',
dataSubjectId: customerId,
legalBasis: 'DSGVO Art. 6 Abs. 1a',
});
res.json({
success: true,
data: {
url: consentUrl,
channel,
hash,
},
});
} catch (error) {
console.error('Fehler beim Senden des Consent-Links:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Senden',
});
}
}
// ==================== VOLLMACHTEN ====================
/**
* Vollmacht-Anfrage an Kunden senden (per E-Mail, WhatsApp, Telegram, Signal)
*/
export async function sendAuthorizationRequest(req: AuthRequest, res: Response) {
try {
const customerId = parseInt(req.params.customerId);
const representativeId = parseInt(req.params.representativeId);
const { channel } = req.body;
// Kunde (Vollmachtgeber) laden
const customer = await prisma.customer.findUnique({
where: { id: customerId },
select: { id: true, firstName: true, lastName: true, email: true },
});
// Vertreter (Bevollmächtigter) laden
const representative = await prisma.customer.findUnique({
where: { id: representativeId },
select: { id: true, firstName: true, lastName: true },
});
if (!customer || !representative) {
return res.status(404).json({ success: false, error: 'Kunde oder Vertreter nicht gefunden' });
}
const baseUrl = process.env.PUBLIC_URL || req.headers.origin || 'http://localhost:5173';
const portalUrl = `${baseUrl}/privacy`;
// E-Mail senden
if (channel === 'email') {
if (!customer.email) {
return res.status(400).json({ success: false, error: 'Kunde hat keine E-Mail-Adresse hinterlegt' });
}
const systemEmail = await getSystemEmailCredentials();
if (!systemEmail) {
return res.status(400).json({
success: false,
error: 'Keine System-E-Mail konfiguriert. Bitte in den Email-Provider-Einstellungen eine System-E-Mail-Adresse und Passwort hinterlegen.',
});
}
const credentials: SmtpCredentials = {
host: systemEmail.smtpServer,
port: systemEmail.smtpPort,
user: systemEmail.emailAddress,
password: systemEmail.password,
encryption: systemEmail.smtpEncryption,
allowSelfSignedCerts: systemEmail.allowSelfSignedCerts,
};
const emailHtml = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #1e40af;">Vollmacht Ihre Zustimmung erforderlich</h2>
<p>Sehr geehrte(r) ${customer.firstName} ${customer.lastName},</p>
<p>
<strong>${representative.firstName} ${representative.lastName}</strong> möchte als Ihr Vertreter Zugriff auf Ihre Vertragsdaten erhalten.
</p>
<p>
Damit dies möglich ist, benötigen wir Ihre Vollmacht. Sie können diese bequem über unser Kundenportal erteilen:
</p>
<p style="text-align: center; margin: 32px 0;">
<a href="${portalUrl}"
style="background-color: #2563eb; color: #ffffff; padding: 14px 32px; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 16px; display: inline-block;">
Vollmacht im Portal erteilen
</a>
</p>
<p style="color: #6b7280; font-size: 14px;">
Alternativ können Sie auch diesen Link in Ihren Browser kopieren:<br>
<a href="${portalUrl}" style="color: #2563eb; word-break: break-all;">${portalUrl}</a>
</p>
<p style="color: #6b7280; font-size: 14px;">
Sie können die Vollmacht jederzeit im Portal unter "Datenschutz" widerrufen.
</p>
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 24px 0;">
<p style="color: #9ca3af; font-size: 12px;">
Hacker-Net Telekommunikation Stefan Hacker<br>
Am Wunderburgpark 5b, 26135 Oldenburg<br>
info@hacker-net.de
</p>
</div>
`;
const result = await sendEmail(credentials, systemEmail.emailAddress, {
to: customer.email,
subject: `Vollmacht für ${representative.firstName} ${representative.lastName} Bitte um Ihre Zustimmung`,
html: emailHtml,
}, {
context: 'authorization-request',
customerId,
triggeredBy: req.user?.email,
});
if (!result.success) {
return res.status(400).json({
success: false,
error: `E-Mail-Versand fehlgeschlagen: ${result.error}`,
});
}
}
// Messaging-Text für WhatsApp/Telegram/Signal
const messageText = `Hallo ${customer.firstName}, ${representative.firstName} ${representative.lastName} möchte als Ihr Vertreter Zugriff auf Ihre Vertragsdaten. Bitte erteilen Sie die Vollmacht im Portal: ${portalUrl}`;
res.json({
success: true,
data: { channel, portalUrl, messageText },
});
} catch (error) {
console.error('Fehler beim Senden der Vollmacht-Anfrage:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Senden',
});
}
}
/**
* Vollmachten für einen Kunden abrufen (Admin-Ansicht)
*/
export async function getAuthorizations(req: AuthRequest, res: Response) {
try {
const customerId = parseInt(req.params.customerId);
// Sicherstellen dass Einträge für alle aktiven Vertreter existieren
await authorizationService.ensureAuthorizationEntries(customerId);
const authorizations = await authorizationService.getAuthorizationsForCustomer(customerId);
res.json({ success: true, data: authorizations });
} catch (error) {
console.error('Fehler beim Laden der Vollmachten:', error);
res.status(500).json({ success: false, error: 'Fehler beim Laden der Vollmachten' });
}
}
/**
* Vollmacht erteilen (Admin: z.B. Papier-Upload)
*/
export async function grantAuthorization(req: AuthRequest, res: Response) {
try {
const customerId = parseInt(req.params.customerId);
const representativeId = parseInt(req.params.representativeId);
const { source, notes } = req.body;
const auth = await authorizationService.grantAuthorization(customerId, representativeId, {
source: source || 'crm-backend',
notes,
});
res.json({ success: true, data: auth });
} catch (error) {
console.error('Fehler beim Erteilen der Vollmacht:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erteilen der Vollmacht',
});
}
}
/**
* Vollmacht widerrufen
*/
export async function withdrawAuthorization(req: AuthRequest, res: Response) {
try {
const customerId = parseInt(req.params.customerId);
const representativeId = parseInt(req.params.representativeId);
const auth = await authorizationService.withdrawAuthorization(customerId, representativeId);
res.json({ success: true, data: auth });
} catch (error) {
console.error('Fehler beim Widerrufen der Vollmacht:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Widerrufen',
});
}
}
/**
* Vollmacht-Dokument hochladen (PDF)
*/
export async function uploadAuthorizationDocument(req: AuthRequest, res: Response) {
try {
const customerId = parseInt(req.params.customerId);
const representativeId = parseInt(req.params.representativeId);
if (!req.file) {
return res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' });
}
const documentPath = `/uploads/authorizations/${req.file.filename}`;
const auth = await authorizationService.updateAuthorizationDocument(
customerId,
representativeId,
documentPath
);
res.json({ success: true, data: auth });
} catch (error) {
console.error('Fehler beim Upload des Vollmacht-Dokuments:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Upload',
});
}
}
/**
* Vollmacht-Dokument löschen
*/
export async function deleteAuthorizationDocument(req: AuthRequest, res: Response) {
try {
const customerId = parseInt(req.params.customerId);
const representativeId = parseInt(req.params.representativeId);
const auth = await authorizationService.deleteAuthorizationDocument(customerId, representativeId);
res.json({ success: true, data: auth });
} catch (error) {
console.error('Fehler beim Löschen des Vollmacht-Dokuments:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen',
});
}
}
/**
* Portal: Eigene Vollmachten abrufen (welche Vertreter dürfen meine Daten sehen?)
*/
export async function getMyAuthorizations(req: AuthRequest, res: Response) {
try {
const user = req.user as any;
if (!user?.isCustomerPortal || !user?.customerId) {
return res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
}
await authorizationService.ensureAuthorizationEntries(user.customerId);
const authorizations = await authorizationService.getAuthorizationsForCustomer(user.customerId);
res.json({ success: true, data: authorizations });
} catch (error) {
console.error('Fehler beim Laden der eigenen Vollmachten:', error);
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
}
}
/**
* Portal: Vollmacht erteilen/widerrufen
*/
export async function toggleMyAuthorization(req: AuthRequest, res: Response) {
try {
const user = req.user as any;
if (!user?.isCustomerPortal || !user?.customerId) {
return res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
}
const representativeId = parseInt(req.params.representativeId);
const { grant } = req.body;
let auth;
if (grant) {
auth = await authorizationService.grantAuthorization(user.customerId, representativeId, {
source: 'portal',
});
} else {
auth = await authorizationService.withdrawAuthorization(user.customerId, representativeId);
}
res.json({ success: true, data: auth });
} catch (error) {
console.error('Fehler beim Ändern der Vollmacht:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Ändern',
});
}
}
/**
* Portal: Prüfe Vollmacht-Status für alle vertretenen Kunden
*/
export async function getMyAuthorizationStatus(req: AuthRequest, res: Response) {
try {
const user = req.user as any;
if (!user?.isCustomerPortal || !user?.customerId) {
return res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
}
// IDs der Kunden die dieser Vertreter vertritt
const representedIds: number[] = user.representedCustomerIds || [];
// Für jeden vertretenen Kunden prüfen ob Vollmacht erteilt
const statuses: { customerId: number; hasAuthorization: boolean }[] = [];
for (const custId of representedIds) {
const has = await authorizationService.hasAuthorization(custId, user.customerId);
statuses.push({ customerId: custId, hasAuthorization: has });
}
res.json({ success: true, data: statuses });
} catch (error) {
console.error('Fehler beim Vollmacht-Status:', error);
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
}
}