complete new audit system

This commit is contained in:
2026-03-21 18:23:54 +01:00
parent 4f359df161
commit 219e1930f7
159 changed files with 2841 additions and 736 deletions
@@ -1,5 +1,7 @@
import { Response } from 'express';
import prisma from '../lib/prisma.js';
import * as appSettingService from '../services/appSetting.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
export async function getAllSettings(req: AuthRequest, res: Response): Promise<void> {
@@ -39,7 +41,22 @@ export async function updateSetting(req: AuthRequest, res: Response): Promise<vo
return;
}
await appSettingService.setSetting(key, String(value));
// Vorherigen Stand laden für Audit
const before = await prisma.appSetting.findUnique({ where: { key } });
const oldValue = before?.value ?? '-';
const newValue = String(value);
await appSettingService.setSetting(key, newValue);
const label = oldValue !== newValue
? `Einstellung "${key}" geändert: ${oldValue}${newValue}`
: `Einstellung "${key}" geändert`;
await logChange({
req, action: 'UPDATE', resourceType: 'AppSetting',
resourceId: key,
label,
details: oldValue !== newValue ? { [key]: { von: oldValue, nach: newValue } } : undefined,
});
res.json({ success: true, message: 'Einstellung gespeichert' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -61,10 +78,27 @@ export async function updateSettings(req: AuthRequest, res: Response): Promise<v
return;
}
// Vorherige Werte laden für Audit
const changes: Record<string, { von: unknown; nach: unknown }> = {};
for (const [key, value] of Object.entries(settings)) {
await appSettingService.setSetting(key, String(value));
const before = await prisma.appSetting.findUnique({ where: { key } });
const oldValue = before?.value ?? '-';
const newValue = String(value);
if (oldValue !== newValue) {
changes[key] = { von: oldValue, nach: newValue };
}
await appSettingService.setSetting(key, newValue);
}
const changeList = Object.entries(changes).map(([k, c]) => `${k}: ${c.von}${c.nach}`).join(', ');
await logChange({
req, action: 'UPDATE', resourceType: 'AppSetting',
label: changeList
? `Einstellungen aktualisiert: ${changeList}`
: `Einstellungen aktualisiert (${Object.keys(settings).join(', ')})`,
details: Object.keys(changes).length > 0 ? changes : undefined,
});
res.json({ success: true, message: 'Einstellungen gespeichert' } as ApiResponse);
} catch (error) {
res.status(400).json({
+33 -6
View File
@@ -1,6 +1,7 @@
import { Response } from 'express';
import { AuthRequest } from '../types/index.js';
import * as auditService from '../services/audit.service.js';
import { logChange } from '../services/audit.service.js';
import { AuditAction, AuditSensitivity } from '@prisma/client';
/**
@@ -106,12 +107,15 @@ export async function exportAuditLogs(req: AuthRequest, res: Response) {
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);
if (format === 'csv') {
const filename = `audit-logs-${new Date().toISOString().split('T')[0]}.csv`;
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
// BOM für Excel UTF-8 Erkennung
res.send('\uFEFF' + content);
} else {
res.json({ success: true, data: JSON.parse(content) })
}
} catch (error) {
console.error('Fehler beim Exportieren der Audit-Logs:', error);
res.status(500).json({ success: false, error: 'Fehler beim Exportieren' });
@@ -147,6 +151,23 @@ export async function verifyIntegrity(req: AuthRequest, res: Response) {
}
}
/**
* Hash-Kette reparieren (alle Hashes neu berechnen)
*/
export async function rehashAll(req: AuthRequest, res: Response) {
try {
const result = await auditService.rehashAll();
res.json({
success: true,
data: result,
message: `${result.rehashedCount} Einträge neu gehasht. Kette ist jetzt intakt.`,
});
} catch (error) {
console.error('Fehler beim Re-Hashing:', error);
res.status(500).json({ success: false, error: 'Fehler beim Re-Hashing' });
}
}
/**
* Retention-Policies abrufen
*/
@@ -175,6 +196,12 @@ export async function updateRetentionPolicy(req: AuthRequest, res: Response) {
isActive,
});
await logChange({
req, action: 'UPDATE', resourceType: 'RetentionPolicy',
resourceId: id.toString(),
label: `Aufbewahrungsrichtlinie aktualisiert`,
});
res.json({ success: true, data: policy });
} catch (error) {
console.error('Fehler beim Aktualisieren der Retention-Policy:', error);
@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import * as backupService from '../services/backup.service.js';
import { logChange } from '../services/audit.service.js';
/**
* Liste aller Backups abrufen
@@ -23,6 +24,10 @@ export async function createBackup(req: Request, res: Response) {
const result = await backupService.createBackup();
if (result.success) {
await logChange({
req, action: 'CREATE', resourceType: 'Backup',
label: `Backup ${result.backupName} erstellt`,
});
res.json({ data: { backupName: result.backupName }, message: 'Backup erfolgreich erstellt' });
} else {
res.status(500).json({ error: 'Backup fehlgeschlagen', details: result.error });
@@ -47,6 +52,10 @@ export async function restoreBackup(req: Request, res: Response) {
const result = await backupService.restoreBackup(name);
if (result.success) {
await logChange({
req, action: 'UPDATE', resourceType: 'Backup',
label: `Backup ${name} wiederhergestellt`,
});
res.json({
data: {
restoredRecords: result.restoredRecords,
@@ -77,6 +86,10 @@ export async function deleteBackup(req: Request, res: Response) {
const result = await backupService.deleteBackup(name);
if (result.success) {
await logChange({
req, action: 'DELETE', resourceType: 'Backup',
label: `Backup ${name} gelöscht`,
});
res.json({ message: 'Backup gelöscht' });
} else {
res.status(500).json({ error: 'Löschen fehlgeschlagen', details: result.error });
@@ -157,6 +170,10 @@ export async function factoryReset(req: Request, res: Response) {
const result = await backupService.factoryReset();
if (result.success) {
await logChange({
req, action: 'DELETE', resourceType: 'System',
label: `Werkseinstellungen wiederhergestellt`,
});
res.json({
message: 'Werkseinstellungen wiederhergestellt. Bitte melden Sie sich mit admin@admin.com / admin an.',
});
@@ -11,12 +11,11 @@ import { decrypt } from '../utils/encryption.js';
import { ApiResponse } from '../types/index.js';
import { getCustomerTargets, getContractTargets, getIdentityDocumentTargets, getBankCardTargets, documentTargets } from '../config/documentTargets.config.js';
import { generateEmailPdf } from '../services/pdfService.js';
import { PrismaClient, DocumentType } from '@prisma/client';
import { DocumentType } from '@prisma/client';
import prisma from '../lib/prisma.js';
import path from 'path';
import fs from 'fs';
const prisma = new PrismaClient();
// ==================== E-MAIL LIST ====================
// E-Mails für einen Kunden abrufen
@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import * as cancellationPeriodService from '../services/cancellation-period.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse } from '../types/index.js';
export async function getCancellationPeriods(req: Request, res: Response): Promise<void> {
@@ -37,6 +38,11 @@ export async function getCancellationPeriod(req: Request, res: Response): Promis
export async function createCancellationPeriod(req: Request, res: Response): Promise<void> {
try {
const period = await cancellationPeriodService.createCancellationPeriod(req.body);
await logChange({
req, action: 'CREATE', resourceType: 'CancellationPeriod',
resourceId: period.id.toString(),
label: `Kündigungsfrist ${period.description} angelegt`,
});
res.status(201).json({ success: true, data: period } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -49,6 +55,11 @@ export async function createCancellationPeriod(req: Request, res: Response): Pro
export async function updateCancellationPeriod(req: Request, res: Response): Promise<void> {
try {
const period = await cancellationPeriodService.updateCancellationPeriod(parseInt(req.params.id), req.body);
await logChange({
req, action: 'UPDATE', resourceType: 'CancellationPeriod',
resourceId: period.id.toString(),
label: `Kündigungsfrist ${period.description} aktualisiert`,
});
res.json({ success: true, data: period } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -60,7 +71,14 @@ export async function updateCancellationPeriod(req: Request, res: Response): Pro
export async function deleteCancellationPeriod(req: Request, res: Response): Promise<void> {
try {
await cancellationPeriodService.deleteCancellationPeriod(parseInt(req.params.id));
const periodId = parseInt(req.params.id);
const period = await cancellationPeriodService.getCancellationPeriodById(periodId);
await cancellationPeriodService.deleteCancellationPeriod(periodId);
await logChange({
req, action: 'DELETE', resourceType: 'CancellationPeriod',
resourceId: periodId.toString(),
label: `Kündigungsfrist ${period?.description || periodId} gelöscht`,
});
res.json({ success: true, message: 'Kündigungsfrist gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import * as contractDurationService from '../services/contract-duration.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse } from '../types/index.js';
export async function getContractDurations(req: Request, res: Response): Promise<void> {
@@ -37,6 +38,11 @@ export async function getContractDuration(req: Request, res: Response): Promise<
export async function createContractDuration(req: Request, res: Response): Promise<void> {
try {
const duration = await contractDurationService.createContractDuration(req.body);
await logChange({
req, action: 'CREATE', resourceType: 'ContractDuration',
resourceId: duration.id.toString(),
label: `Laufzeit ${duration.description} angelegt`,
});
res.status(201).json({ success: true, data: duration } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -49,6 +55,11 @@ export async function createContractDuration(req: Request, res: Response): Promi
export async function updateContractDuration(req: Request, res: Response): Promise<void> {
try {
const duration = await contractDurationService.updateContractDuration(parseInt(req.params.id), req.body);
await logChange({
req, action: 'UPDATE', resourceType: 'ContractDuration',
resourceId: duration.id.toString(),
label: `Laufzeit ${duration.description} aktualisiert`,
});
res.json({ success: true, data: duration } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -60,7 +71,14 @@ export async function updateContractDuration(req: Request, res: Response): Promi
export async function deleteContractDuration(req: Request, res: Response): Promise<void> {
try {
await contractDurationService.deleteContractDuration(parseInt(req.params.id));
const durationId = parseInt(req.params.id);
const duration = await contractDurationService.getContractDurationById(durationId);
await contractDurationService.deleteContractDuration(durationId);
await logChange({
req, action: 'DELETE', resourceType: 'ContractDuration',
resourceId: durationId.toString(),
label: `Laufzeit ${duration?.description || durationId} gelöscht`,
});
res.json({ success: true, message: 'Laufzeit gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
+105 -6
View File
@@ -1,12 +1,11 @@
import { Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import prisma from '../lib/prisma.js';
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();
import { logChange } from '../services/audit.service.js';
export async function getContracts(req: AuthRequest, res: Response): Promise<void> {
try {
@@ -100,6 +99,12 @@ export async function getContract(req: AuthRequest, res: Response): Promise<void
export async function createContract(req: Request, res: Response): Promise<void> {
try {
const contract = await contractService.createContract(req.body);
await logChange({
req, action: 'CREATE', resourceType: 'Contract',
resourceId: contract.id.toString(),
label: `Vertrag ${contract.contractNumber} angelegt`,
customerId: contract.customerId,
});
res.status(201).json({ success: true, data: contract } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -109,9 +114,69 @@ export async function createContract(req: Request, res: Response): Promise<void>
}
}
export async function updateContract(req: Request, res: Response): Promise<void> {
export async function updateContract(req: AuthRequest, res: Response): Promise<void> {
try {
const contract = await contractService.updateContract(parseInt(req.params.id), req.body);
const contractId = parseInt(req.params.id);
// Vorherigen Stand laden für Audit-Vergleich
const before = await prisma.contract.findUnique({
where: { id: contractId },
include: { energyDetails: true, internetDetails: true, mobileDetails: true, tvDetails: true, carInsuranceDetails: true },
});
const contract = await contractService.updateContract(contractId, req.body);
// Geänderte Felder ermitteln
const changes: Record<string, { von: unknown; nach: unknown }> = {};
const fieldLabels: Record<string, string> = {
status: 'Status', startDate: 'Vertragsbeginn', endDate: 'Vertragsende',
portalUsername: 'Portal-Benutzername', customerNumberAtProvider: 'Kundennummer beim Anbieter',
providerId: 'Anbieter', tariffId: 'Tarif', cancellationPeriodId: 'Kündigungsfrist',
contractDurationId: 'Vertragslaufzeit', platformId: 'Vertriebsplattform',
cancellationDate: 'Kündigungsdatum', cancellationSentDate: 'Kündigung gesendet am',
identityDocumentId: 'Ausweis', bankCardId: 'Bankverbindung', addressId: 'Adresse',
commission: 'Provision', notes: 'Notizen',
};
const energyLabels: Record<string, string> = {
meterId: 'Zähler', maloId: 'MaLo-ID', annualConsumption: 'Jahresverbrauch',
basePrice: 'Grundpreis', unitPrice: 'Arbeitspreis', unitPriceNt: 'NT-Arbeitspreis', bonus: 'Bonus',
};
// Hauptfelder vergleichen
const body = req.body;
if (before) {
for (const [key, newVal] of Object.entries(body)) {
if (['energyDetails', 'internetDetails', 'mobileDetails', 'tvDetails', 'carInsuranceDetails', 'password'].includes(key)) continue;
const oldVal = (before as any)[key];
const norm = (v: unknown) => (v === null || v === undefined || v === '' ? null : v);
if (JSON.stringify(norm(oldVal)) !== JSON.stringify(norm(newVal))) {
const label = fieldLabels[key] || key;
changes[label] = { von: oldVal ?? '-', nach: newVal ?? '-' };
}
}
// Energie-Details vergleichen
if (body.energyDetails && before.energyDetails) {
for (const [key, newVal] of Object.entries(body.energyDetails)) {
const oldVal = (before.energyDetails as any)[key];
const norm = (v: unknown) => (v === null || v === undefined || v === '' ? null : v);
if (JSON.stringify(norm(oldVal)) !== JSON.stringify(norm(newVal))) {
const label = energyLabels[key] || key;
changes[label] = { von: oldVal ?? '-', nach: newVal ?? '-' };
}
}
}
}
const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von}${c.nach}`).join(', ');
await logChange({
req, action: 'UPDATE', resourceType: 'Contract',
resourceId: contractId.toString(),
label: changeList
? `Vertrag ${before?.contractNumber || contractId} aktualisiert: ${changeList}`
: `Vertrag ${before?.contractNumber || contractId} aktualisiert`,
details: Object.keys(changes).length > 0 ? changes : undefined,
customerId: before?.customerId,
});
res.json({ success: true, data: contract } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -123,7 +188,15 @@ export async function updateContract(req: Request, res: Response): Promise<void>
export async function deleteContract(req: Request, res: Response): Promise<void> {
try {
await contractService.deleteContract(parseInt(req.params.id));
const contractId = parseInt(req.params.id);
const contract = await prisma.contract.findUnique({ where: { id: contractId }, select: { contractNumber: true, customerId: true } });
await contractService.deleteContract(contractId);
await logChange({
req, action: 'DELETE', resourceType: 'Contract',
resourceId: contractId.toString(),
label: `Vertrag ${contract?.contractNumber} gelöscht`,
customerId: contract?.customerId,
});
res.json({ success: true, message: 'Vertrag gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -165,6 +238,13 @@ export async function createFollowUp(req: AuthRequest, res: Response): Promise<v
createdBy
);
await logChange({
req, action: 'CREATE', resourceType: 'Contract',
resourceId: contract.id.toString(),
label: `Folgevertrag erstellt für ${previousContract.contractNumber}`,
customerId: contract.customerId,
});
res.status(201).json({ success: true, data: contract } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -295,6 +375,13 @@ export async function addSuccessorMeter(req: AuthRequest, res: Response): Promis
data: { meterId: parseInt(meterId) },
});
await logChange({
req, action: 'CREATE', resourceType: 'ContractMeter',
resourceId: contractMeter.id.toString(),
label: `Folgezähler hinzugefügt zu Vertrag #${contractId}`,
customerId: contract.customerId,
});
res.json({ success: true, data: contractMeter } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -307,7 +394,13 @@ export async function addSuccessorMeter(req: AuthRequest, res: Response): Promis
export async function removeContractMeter(req: AuthRequest, res: Response): Promise<void> {
try {
const contractMeterId = parseInt(req.params.contractMeterId);
const contractId = parseInt(req.params.id);
await prisma.contractMeter.delete({ where: { id: contractMeterId } });
await logChange({
req, action: 'DELETE', resourceType: 'ContractMeter',
resourceId: contractMeterId.toString(),
label: `Folgezähler entfernt von Vertrag #${contractId}`,
});
res.json({ success: true, data: null } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -346,6 +439,12 @@ export async function snoozeContract(req: Request, res: Response): Promise<void>
},
});
await logChange({
req, action: 'UPDATE', resourceType: 'Contract',
resourceId: id.toString(),
label: `Vertrag ${updated.contractNumber} zurückgestellt`,
});
res.json({
success: true,
data: updated,
@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import * as contractCategoryService from '../services/contractCategory.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse } from '../types/index.js';
export async function getContractCategories(req: Request, res: Response): Promise<void> {
@@ -37,6 +38,11 @@ export async function getContractCategory(req: Request, res: Response): Promise<
export async function createContractCategory(req: Request, res: Response): Promise<void> {
try {
const category = await contractCategoryService.createContractCategory(req.body);
await logChange({
req, action: 'CREATE', resourceType: 'ContractCategory',
resourceId: category.id.toString(),
label: `Vertragskategorie ${category.name} angelegt`,
});
res.status(201).json({ success: true, data: category } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -49,6 +55,11 @@ export async function createContractCategory(req: Request, res: Response): Promi
export async function updateContractCategory(req: Request, res: Response): Promise<void> {
try {
const category = await contractCategoryService.updateContractCategory(parseInt(req.params.id), req.body);
await logChange({
req, action: 'UPDATE', resourceType: 'ContractCategory',
resourceId: category.id.toString(),
label: `Vertragskategorie ${category.name} aktualisiert`,
});
res.json({ success: true, data: category } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -60,7 +71,14 @@ export async function updateContractCategory(req: Request, res: Response): Promi
export async function deleteContractCategory(req: Request, res: Response): Promise<void> {
try {
await contractCategoryService.deleteContractCategory(parseInt(req.params.id));
const categoryId = parseInt(req.params.id);
const category = await contractCategoryService.getContractCategoryById(categoryId);
await contractCategoryService.deleteContractCategory(categoryId);
await logChange({
req, action: 'DELETE', resourceType: 'ContractCategory',
resourceId: categoryId.toString(),
label: `Vertragskategorie ${category?.name || categoryId} gelöscht`,
});
res.json({ success: true, message: 'Vertragskategorie gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import * as contractHistoryService from '../services/contractHistory.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
export async function getHistoryEntries(req: AuthRequest, res: Response): Promise<void> {
@@ -35,6 +36,12 @@ export async function createHistoryEntry(req: AuthRequest, res: Response): Promi
createdBy: req.user?.email || 'unbekannt',
});
await logChange({
req, action: 'CREATE', resourceType: 'ContractHistory',
resourceId: entry.id.toString(),
label: `Historieneintrag "${title.trim()}" erstellt für Vertrag #${contractId}`,
});
res.status(201).json({ success: true, data: entry } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -55,6 +62,12 @@ export async function updateHistoryEntry(req: AuthRequest, res: Response): Promi
description: description?.trim(),
});
await logChange({
req, action: 'UPDATE', resourceType: 'ContractHistory',
resourceId: entryId.toString(),
label: `Historieneintrag aktualisiert für Vertrag #${contractId}`,
});
res.json({ success: true, data: entry } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -71,6 +84,12 @@ export async function deleteHistoryEntry(req: AuthRequest, res: Response): Promi
await contractHistoryService.deleteHistoryEntry(contractId, entryId);
await logChange({
req, action: 'DELETE', resourceType: 'ContractHistory',
resourceId: entryId.toString(),
label: `Historieneintrag gelöscht für Vertrag #${contractId}`,
});
res.json({ success: true, message: 'Eintrag gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -3,6 +3,7 @@ import * as contractTaskService from '../services/contractTask.service.js';
import * as contractService from '../services/contract.service.js';
import * as customerService from '../services/customer.service.js';
import * as appSettingService from '../services/appSetting.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
// ==================== ALL TASKS (Dashboard & Task List) ====================
@@ -147,6 +148,12 @@ export async function createTask(req: AuthRequest, res: Response): Promise<void>
createdBy,
});
await logChange({
req, action: 'CREATE', resourceType: 'ContractTask',
resourceId: task.id.toString(),
label: `Aufgabe "${title}" erstellt`,
});
res.status(201).json({ success: true, data: task } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -212,6 +219,12 @@ export async function createSupportTicket(req: AuthRequest, res: Response): Prom
createdBy,
});
await logChange({
req, action: 'CREATE', resourceType: 'ContractTask',
resourceId: task.id.toString(),
label: `Support-Anfrage "${title}" erstellt`,
});
res.status(201).json({ success: true, data: task } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -232,6 +245,12 @@ export async function updateTask(req: AuthRequest, res: Response): Promise<void>
visibleInPortal,
});
await logChange({
req, action: 'UPDATE', resourceType: 'ContractTask',
resourceId: taskId.toString(),
label: `Aufgabe aktualisiert`,
});
res.json({ success: true, data: task } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -245,6 +264,11 @@ export async function completeTask(req: AuthRequest, res: Response): Promise<voi
try {
const taskId = parseInt(req.params.taskId);
const task = await contractTaskService.completeTask(taskId);
await logChange({
req, action: 'UPDATE', resourceType: 'ContractTask',
resourceId: taskId.toString(),
label: `Aufgabe abgeschlossen`,
});
res.json({ success: true, data: task } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -258,6 +282,11 @@ export async function reopenTask(req: AuthRequest, res: Response): Promise<void>
try {
const taskId = parseInt(req.params.taskId);
const task = await contractTaskService.reopenTask(taskId);
await logChange({
req, action: 'UPDATE', resourceType: 'ContractTask',
resourceId: taskId.toString(),
label: `Aufgabe wiedereröffnet`,
});
res.json({ success: true, data: task } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -271,6 +300,11 @@ export async function deleteTask(req: AuthRequest, res: Response): Promise<void>
try {
const taskId = parseInt(req.params.taskId);
await contractTaskService.deleteTask(taskId);
await logChange({
req, action: 'DELETE', resourceType: 'ContractTask',
resourceId: taskId.toString(),
label: `Aufgabe gelöscht`,
});
res.json({ success: true, message: 'Aufgabe gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -303,6 +337,12 @@ export async function createSubtask(req: AuthRequest, res: Response): Promise<vo
createdBy,
});
await logChange({
req, action: 'CREATE', resourceType: 'ContractSubtask',
resourceId: subtask.id.toString(),
label: `Unteraufgabe "${title}" erstellt`,
});
res.status(201).json({ success: true, data: subtask } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -369,6 +409,12 @@ export async function createCustomerReply(req: AuthRequest, res: Response): Prom
createdBy,
});
await logChange({
req, action: 'CREATE', resourceType: 'ContractSubtask',
resourceId: subtask.id.toString(),
label: `Kundenantwort erstellt`,
});
res.status(201).json({ success: true, data: subtask } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -392,6 +438,11 @@ export async function updateSubtask(req: AuthRequest, res: Response): Promise<vo
}
const subtask = await contractTaskService.updateSubtask(subtaskId, { title });
await logChange({
req, action: 'UPDATE', resourceType: 'ContractSubtask',
resourceId: subtaskId.toString(),
label: `Unteraufgabe aktualisiert`,
});
res.json({ success: true, data: subtask } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -405,6 +456,11 @@ export async function completeSubtask(req: AuthRequest, res: Response): Promise<
try {
const subtaskId = parseInt(req.params.subtaskId);
const subtask = await contractTaskService.completeSubtask(subtaskId);
await logChange({
req, action: 'UPDATE', resourceType: 'ContractSubtask',
resourceId: subtaskId.toString(),
label: `Unteraufgabe abgeschlossen`,
});
res.json({ success: true, data: subtask } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -418,6 +474,11 @@ export async function reopenSubtask(req: AuthRequest, res: Response): Promise<vo
try {
const subtaskId = parseInt(req.params.subtaskId);
const subtask = await contractTaskService.reopenSubtask(subtaskId);
await logChange({
req, action: 'UPDATE', resourceType: 'ContractSubtask',
resourceId: subtaskId.toString(),
label: `Unteraufgabe wiedereröffnet`,
});
res.json({ success: true, data: subtask } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -431,6 +492,11 @@ export async function deleteSubtask(req: AuthRequest, res: Response): Promise<vo
try {
const subtaskId = parseInt(req.params.subtaskId);
await contractTaskService.deleteSubtask(subtaskId);
await logChange({
req, action: 'DELETE', resourceType: 'ContractSubtask',
resourceId: subtaskId.toString(),
label: `Unteraufgabe gelöscht`,
});
res.json({ success: true, message: 'Unteraufgabe gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
+456 -25
View File
@@ -1,11 +1,10 @@
import { Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import prisma from '../lib/prisma.js';
import * as customerService from '../services/customer.service.js';
import * as authService from '../services/auth.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
const prisma = new PrismaClient();
// Customer CRUD
export async function getCustomers(req: Request, res: Response): Promise<void> {
try {
@@ -46,6 +45,12 @@ export async function createCustomer(req: Request, res: Response): Promise<void>
data.birthDate = new Date(data.birthDate);
}
const customer = await customerService.createCustomer(data);
await logChange({
req, action: 'CREATE', resourceType: 'Customer',
resourceId: customer.id.toString(),
label: `Kunde ${customer.customerNumber} angelegt (${customer.firstName} ${customer.lastName})`,
customerId: customer.id,
});
res.status(201).json({ success: true, data: customer } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -57,12 +62,70 @@ export async function createCustomer(req: Request, res: Response): Promise<void>
export async function updateCustomer(req: Request, res: Response): Promise<void> {
try {
const customerId = parseInt(req.params.id);
const data = { ...req.body };
// Convert birthDate string to Date if present
if (data.birthDate) {
// Vorherigen Stand laden für Audit
const before = await prisma.customer.findUnique({ where: { id: customerId } });
// Convert birthDate string to Date if present, empty string to null
if (data.birthDate === '' || data.birthDate === null) {
data.birthDate = null;
} else if (data.birthDate) {
data.birthDate = new Date(data.birthDate);
}
const customer = await customerService.updateCustomer(parseInt(req.params.id), data);
// Leere Strings in optionalen Feldern zu null konvertieren
const nullableFields = ['salutation', 'birthPlace', 'phone', 'mobile', 'email', 'companyName', 'taxNumber', 'businessRegistration', 'commercialRegister', 'commercialRegisterNumber', 'notes'];
for (const field of nullableFields) {
if (data[field] === '') data[field] = null;
}
const customer = await customerService.updateCustomer(customerId, data);
// Audit: Geänderte Felder ermitteln und loggen
if (before) {
const changes: Record<string, { von: unknown; nach: unknown }> = {};
const fieldLabels: Record<string, string> = {
salutation: 'Anrede', firstName: 'Vorname', lastName: 'Nachname', email: 'E-Mail',
phone: 'Telefon', mobile: 'Mobil', birthDate: 'Geburtsdatum', birthPlace: 'Geburtsort',
companyName: 'Firma', type: 'Typ', taxNumber: 'Steuernummer', notes: 'Notizen',
};
for (const [key, value] of Object.entries(data)) {
// Technische/interne Felder überspringen
if (['id', 'createdAt', 'updatedAt', 'customerNumber', 'portalPasswordHash', 'portalPasswordEncrypted'].includes(key)) continue;
const oldVal = (before as any)[key];
const newVal = value;
// Normalisieren: null, undefined, "" werden alle als "leer" behandelt
const normalize = (v: unknown) => {
if (v === null || v === undefined || v === '') return null;
if (v instanceof Date) return v.toISOString().split('T')[0];
return v;
};
const oldNorm = normalize(oldVal);
const newNorm = normalize(newVal);
if (JSON.stringify(oldNorm) !== JSON.stringify(newNorm)) {
const label = fieldLabels[key] || key;
const formatVal = (v: unknown) => {
if (v === null || v === undefined || v === '') return '-';
if (v instanceof Date) return v.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
if (typeof v === 'boolean') return v ? 'Ja' : 'Nein';
return String(v);
};
changes[label] = { von: formatVal(oldVal), nach: formatVal(newVal) };
}
}
if (Object.keys(changes).length > 0) {
const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von}${c.nach}`).join(', ');
await logChange({
req, action: 'UPDATE', resourceType: 'Customer',
resourceId: customerId.toString(),
label: `Kunde ${before.customerNumber} aktualisiert: ${changeList}`,
details: changes,
customerId,
});
}
}
res.json({ success: true, data: customer } as ApiResponse);
} catch (error) {
console.error('Update customer error:', error);
@@ -75,7 +138,15 @@ export async function updateCustomer(req: Request, res: Response): Promise<void>
export async function deleteCustomer(req: Request, res: Response): Promise<void> {
try {
await customerService.deleteCustomer(parseInt(req.params.id));
const customerId = parseInt(req.params.id);
const customer = await prisma.customer.findUnique({ where: { id: customerId }, select: { customerNumber: true, firstName: true, lastName: true } });
await customerService.deleteCustomer(customerId);
await logChange({
req, action: 'DELETE', resourceType: 'Customer',
resourceId: customerId.toString(),
label: `Kunde ${customer?.customerNumber} gelöscht (${customer?.firstName} ${customer?.lastName})`,
customerId,
});
res.json({ success: true, message: 'Kunde gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -97,7 +168,14 @@ export async function getAddresses(req: Request, res: Response): Promise<void> {
export async function createAddress(req: Request, res: Response): Promise<void> {
try {
const address = await customerService.createAddress(parseInt(req.params.customerId), req.body);
const customerId = parseInt(req.params.customerId);
const address = await customerService.createAddress(customerId, req.body);
await logChange({
req, action: 'CREATE', resourceType: 'Address',
resourceId: address.id.toString(),
label: `Adresse hinzugefügt für Kunde #${customerId}`,
customerId,
});
res.status(201).json({ success: true, data: address } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -109,7 +187,53 @@ export async function createAddress(req: Request, res: Response): Promise<void>
export async function updateAddress(req: Request, res: Response): Promise<void> {
try {
const address = await customerService.updateAddress(parseInt(req.params.id), req.body);
const addressId = parseInt(req.params.id);
const data = req.body;
// Vorherigen Stand laden für Audit
const before = await prisma.address.findUnique({ where: { id: addressId } });
const address = await customerService.updateAddress(addressId, data);
const customerId = address.customerId;
// Audit: Geänderte Felder ermitteln und loggen
if (before) {
const changes: Record<string, { von: unknown; nach: unknown }> = {};
const fieldLabels: Record<string, string> = {
street: 'Straße', houseNumber: 'Hausnummer', postalCode: 'PLZ',
city: 'Stadt', country: 'Land', type: 'Typ', isDefault: 'Standard',
};
for (const [key, newVal] of Object.entries(data)) {
if (['id', 'createdAt', 'updatedAt'].includes(key)) continue;
const oldVal = (before as any)[key];
const norm = (v: unknown) => (v === null || v === undefined || v === '' ? null : v);
if (JSON.stringify(norm(oldVal)) !== JSON.stringify(norm(newVal))) {
const label = fieldLabels[key] || key;
const formatVal = (v: unknown) => {
if (v === null || v === undefined || v === '') return '-';
if (typeof v === 'boolean') return v ? 'Ja' : 'Nein';
return String(v);
};
changes[label] = { von: formatVal(oldVal), nach: formatVal(newVal) };
}
}
const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von}${c.nach}`).join(', ');
await logChange({
req, action: 'UPDATE', resourceType: 'Address',
resourceId: address.id.toString(),
label: changeList ? `Adresse aktualisiert für Kunde #${customerId}: ${changeList}` : `Adresse aktualisiert für Kunde #${customerId}`,
details: Object.keys(changes).length > 0 ? changes : undefined,
customerId,
});
} else {
await logChange({
req, action: 'UPDATE', resourceType: 'Address',
resourceId: address.id.toString(),
label: `Adresse aktualisiert für Kunde #${customerId}`,
customerId,
});
}
res.json({ success: true, data: address } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -121,7 +245,16 @@ export async function updateAddress(req: Request, res: Response): Promise<void>
export async function deleteAddress(req: Request, res: Response): Promise<void> {
try {
await customerService.deleteAddress(parseInt(req.params.id));
const addressId = parseInt(req.params.id);
const addr = await prisma.address.findUnique({ where: { id: addressId }, select: { customerId: true } });
const customerId = addr?.customerId;
await customerService.deleteAddress(addressId);
await logChange({
req, action: 'DELETE', resourceType: 'Address',
resourceId: addressId.toString(),
label: `Adresse gelöscht für Kunde #${customerId}`,
customerId: customerId ?? undefined,
});
res.json({ success: true, message: 'Adresse gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -147,7 +280,14 @@ export async function getBankCards(req: Request, res: Response): Promise<void> {
export async function createBankCard(req: Request, res: Response): Promise<void> {
try {
const card = await customerService.createBankCard(parseInt(req.params.customerId), req.body);
const customerId = parseInt(req.params.customerId);
const card = await customerService.createBankCard(customerId, req.body);
await logChange({
req, action: 'CREATE', resourceType: 'BankCard',
resourceId: card.id.toString(),
label: `Bankverbindung hinzugefügt für Kunde #${customerId}`,
customerId,
});
res.status(201).json({ success: true, data: card } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -159,7 +299,53 @@ export async function createBankCard(req: Request, res: Response): Promise<void>
export async function updateBankCard(req: Request, res: Response): Promise<void> {
try {
const card = await customerService.updateBankCard(parseInt(req.params.id), req.body);
const cardId = parseInt(req.params.id);
const data = req.body;
// Vorherigen Stand laden für Audit
const before = await prisma.bankCard.findUnique({ where: { id: cardId } });
const card = await customerService.updateBankCard(cardId, data);
const customerId = card.customerId;
// Audit: Geänderte Felder ermitteln und loggen
if (before) {
const changes: Record<string, { von: unknown; nach: unknown }> = {};
const fieldLabels: Record<string, string> = {
iban: 'IBAN', bic: 'BIC', bankName: 'Bank',
accountHolder: 'Kontoinhaber', isActive: 'Aktiv',
};
for (const [key, newVal] of Object.entries(data)) {
if (['id', 'createdAt', 'updatedAt'].includes(key)) continue;
const oldVal = (before as any)[key];
const norm = (v: unknown) => (v === null || v === undefined || v === '' ? null : v);
if (JSON.stringify(norm(oldVal)) !== JSON.stringify(norm(newVal))) {
const label = fieldLabels[key] || key;
const formatVal = (v: unknown) => {
if (v === null || v === undefined || v === '') return '-';
if (typeof v === 'boolean') return v ? 'Ja' : 'Nein';
return String(v);
};
changes[label] = { von: formatVal(oldVal), nach: formatVal(newVal) };
}
}
const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von}${c.nach}`).join(', ');
await logChange({
req, action: 'UPDATE', resourceType: 'BankCard',
resourceId: card.id.toString(),
label: changeList ? `Bankverbindung aktualisiert für Kunde #${customerId}: ${changeList}` : `Bankverbindung aktualisiert für Kunde #${customerId}`,
details: Object.keys(changes).length > 0 ? changes : undefined,
customerId,
});
} else {
await logChange({
req, action: 'UPDATE', resourceType: 'BankCard',
resourceId: card.id.toString(),
label: `Bankverbindung aktualisiert für Kunde #${customerId}`,
customerId,
});
}
res.json({ success: true, data: card } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -171,7 +357,16 @@ export async function updateBankCard(req: Request, res: Response): Promise<void>
export async function deleteBankCard(req: Request, res: Response): Promise<void> {
try {
await customerService.deleteBankCard(parseInt(req.params.id));
const cardId = parseInt(req.params.id);
const card = await prisma.bankCard.findUnique({ where: { id: cardId }, select: { customerId: true } });
const customerId = card?.customerId;
await customerService.deleteBankCard(cardId);
await logChange({
req, action: 'DELETE', resourceType: 'BankCard',
resourceId: cardId.toString(),
label: `Bankverbindung gelöscht für Kunde #${customerId}`,
customerId: customerId ?? undefined,
});
res.json({ success: true, message: 'Bankkarte gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -197,7 +392,14 @@ export async function getDocuments(req: Request, res: Response): Promise<void> {
export async function createDocument(req: Request, res: Response): Promise<void> {
try {
const doc = await customerService.createDocument(parseInt(req.params.customerId), req.body);
const customerId = parseInt(req.params.customerId);
const doc = await customerService.createDocument(customerId, req.body);
await logChange({
req, action: 'CREATE', resourceType: 'IdentityDocument',
resourceId: doc.id.toString(),
label: `Ausweis hinzugefügt für Kunde #${customerId}`,
customerId,
});
res.status(201).json({ success: true, data: doc } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -209,7 +411,59 @@ export async function createDocument(req: Request, res: Response): Promise<void>
export async function updateDocument(req: Request, res: Response): Promise<void> {
try {
const doc = await customerService.updateDocument(parseInt(req.params.id), req.body);
const docId = parseInt(req.params.id);
const data = req.body;
// Vorherigen Stand laden für Audit
const before = await prisma.identityDocument.findUnique({ where: { id: docId } });
const doc = await customerService.updateDocument(docId, data);
const customerId = doc.customerId;
// Audit: Geänderte Felder ermitteln und loggen
if (before) {
const changes: Record<string, { von: unknown; nach: unknown }> = {};
const fieldLabels: Record<string, string> = {
type: 'Dokumenttyp', documentNumber: 'Dokumentnummer',
issuingAuthority: 'Ausstellungsbehörde', issueDate: 'Ausstellungsdatum',
expiryDate: 'Ablaufdatum', isActive: 'Aktiv', licenseClasses: 'Führerscheinklassen',
};
for (const [key, newVal] of Object.entries(data)) {
if (['id', 'createdAt', 'updatedAt'].includes(key)) continue;
const oldVal = (before as any)[key];
const norm = (v: unknown) => {
if (v === null || v === undefined || v === '') return null;
if (v instanceof Date) return v.toISOString().split('T')[0];
return v;
};
if (JSON.stringify(norm(oldVal)) !== JSON.stringify(norm(newVal))) {
const label = fieldLabels[key] || key;
const formatVal = (v: unknown) => {
if (v === null || v === undefined || v === '') return '-';
if (v instanceof Date) return v.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
if (typeof v === 'boolean') return v ? 'Ja' : 'Nein';
return String(v);
};
changes[label] = { von: formatVal(oldVal), nach: formatVal(newVal) };
}
}
const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von}${c.nach}`).join(', ');
await logChange({
req, action: 'UPDATE', resourceType: 'IdentityDocument',
resourceId: doc.id.toString(),
label: changeList ? `Ausweis aktualisiert für Kunde #${customerId}: ${changeList}` : `Ausweis aktualisiert für Kunde #${customerId}`,
details: Object.keys(changes).length > 0 ? changes : undefined,
customerId,
});
} else {
await logChange({
req, action: 'UPDATE', resourceType: 'IdentityDocument',
resourceId: doc.id.toString(),
label: `Ausweis aktualisiert für Kunde #${customerId}`,
customerId,
});
}
res.json({ success: true, data: doc } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -221,7 +475,16 @@ export async function updateDocument(req: Request, res: Response): Promise<void>
export async function deleteDocument(req: Request, res: Response): Promise<void> {
try {
await customerService.deleteDocument(parseInt(req.params.id));
const docId = parseInt(req.params.id);
const doc = await prisma.identityDocument.findUnique({ where: { id: docId }, select: { customerId: true } });
const customerId = doc?.customerId;
await customerService.deleteDocument(docId);
await logChange({
req, action: 'DELETE', resourceType: 'IdentityDocument',
resourceId: docId.toString(),
label: `Ausweis gelöscht für Kunde #${customerId}`,
customerId: customerId ?? undefined,
});
res.json({ success: true, message: 'Ausweis gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -247,7 +510,14 @@ export async function getMeters(req: Request, res: Response): Promise<void> {
export async function createMeter(req: Request, res: Response): Promise<void> {
try {
const meter = await customerService.createMeter(parseInt(req.params.customerId), req.body);
const customerId = parseInt(req.params.customerId);
const meter = await customerService.createMeter(customerId, req.body);
await logChange({
req, action: 'CREATE', resourceType: 'Meter',
resourceId: meter.id.toString(),
label: `Zähler angelegt für Kunde #${customerId}`,
customerId,
});
res.status(201).json({ success: true, data: meter } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -259,7 +529,52 @@ export async function createMeter(req: Request, res: Response): Promise<void> {
export async function updateMeter(req: Request, res: Response): Promise<void> {
try {
const meter = await customerService.updateMeter(parseInt(req.params.id), req.body);
const meterId = parseInt(req.params.id);
const data = req.body;
// Vorherigen Stand laden für Audit
const before = await prisma.meter.findUnique({ where: { id: meterId } });
const meter = await customerService.updateMeter(meterId, data);
const customerId = meter.customerId;
// Audit: Geänderte Felder ermitteln und loggen
if (before) {
const changes: Record<string, { von: unknown; nach: unknown }> = {};
const fieldLabels: Record<string, string> = {
meterNumber: 'Zählernummer', type: 'Typ', tariffModel: 'Tarifmodell',
location: 'Standort', isActive: 'Aktiv',
};
for (const [key, newVal] of Object.entries(data)) {
if (['id', 'createdAt', 'updatedAt'].includes(key)) continue;
const oldVal = (before as any)[key];
const norm = (v: unknown) => (v === null || v === undefined || v === '' ? null : v);
if (JSON.stringify(norm(oldVal)) !== JSON.stringify(norm(newVal))) {
const label = fieldLabels[key] || key;
const formatVal = (v: unknown) => {
if (v === null || v === undefined || v === '') return '-';
if (typeof v === 'boolean') return v ? 'Ja' : 'Nein';
return String(v);
};
changes[label] = { von: formatVal(oldVal), nach: formatVal(newVal) };
}
}
const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von}${c.nach}`).join(', ');
await logChange({
req, action: 'UPDATE', resourceType: 'Meter',
resourceId: meter.id.toString(),
label: changeList ? `Zähler aktualisiert: ${changeList}` : `Zähler aktualisiert`,
details: Object.keys(changes).length > 0 ? changes : undefined,
customerId,
});
} else {
await logChange({
req, action: 'UPDATE', resourceType: 'Meter',
resourceId: meter.id.toString(),
label: `Zähler aktualisiert`,
});
}
res.json({ success: true, data: meter } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -271,7 +586,13 @@ export async function updateMeter(req: Request, res: Response): Promise<void> {
export async function deleteMeter(req: Request, res: Response): Promise<void> {
try {
await customerService.deleteMeter(parseInt(req.params.id));
const meterId = parseInt(req.params.id);
await customerService.deleteMeter(meterId);
await logChange({
req, action: 'DELETE', resourceType: 'Meter',
resourceId: meterId.toString(),
label: `Zähler gelöscht`,
});
res.json({ success: true, message: 'Zähler gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -294,13 +615,30 @@ export async function getMeterReadings(req: Request, res: Response): Promise<voi
export async function addMeterReading(req: Request, res: Response): Promise<void> {
try {
const { readingDate, value, valueNt, unit, notes } = req.body;
const reading = await customerService.addMeterReading(parseInt(req.params.meterId), {
const meterId = parseInt(req.params.meterId);
const reading = await customerService.addMeterReading(meterId, {
readingDate: new Date(readingDate),
value: parseFloat(value),
valueNt: valueNt !== undefined && valueNt !== null && valueNt !== '' ? parseFloat(valueNt) : undefined,
unit,
notes,
});
// Audit: Zählerstand mit Kontext loggen
const meter = await prisma.meter.findUnique({
where: { id: meterId },
select: { meterNumber: true, customer: { select: { id: true, firstName: true, lastName: true } } },
});
if (meter) {
const ntInfo = valueNt ? ` / NT: ${parseFloat(valueNt)}` : '';
await logChange({
req, action: 'CREATE', resourceType: 'MeterReading',
label: `Zählerstand ${parseFloat(value)}${ntInfo} ${unit || 'kWh'} für Zähler ${meter.meterNumber} erfasst (${meter.customer.firstName} ${meter.customer.lastName})`,
details: { zähler: meter.meterNumber, stand: parseFloat(value), datum: readingDate },
customerId: meter.customer.id,
});
}
res.status(201).json({ success: true, data: reading } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -325,6 +663,11 @@ export async function updateMeterReading(req: Request, res: Response): Promise<v
parseInt(req.params.readingId),
updateData as any
);
await logChange({
req, action: 'UPDATE', resourceType: 'MeterReading',
resourceId: reading.id.toString(),
label: `Zählerstand aktualisiert`,
});
res.json({ success: true, data: reading } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -336,10 +679,16 @@ export async function updateMeterReading(req: Request, res: Response): Promise<v
export async function deleteMeterReading(req: Request, res: Response): Promise<void> {
try {
const readingId = parseInt(req.params.readingId);
await customerService.deleteMeterReading(
parseInt(req.params.meterId),
parseInt(req.params.readingId)
readingId
);
await logChange({
req, action: 'DELETE', resourceType: 'MeterReading',
resourceId: readingId.toString(),
label: `Zählerstand gelöscht`,
});
res.json({ success: true, data: null } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -389,6 +738,15 @@ export async function reportMeterReading(req: AuthRequest, res: Response): Promi
data: { reportedBy: user.email, status: 'REPORTED' },
});
// Audit
const meterInfo = await prisma.meter.findUnique({ where: { id: meterId }, select: { meterNumber: true } });
await logChange({
req, action: 'CREATE', resourceType: 'MeterReading',
label: `Zählerstand ${parsedValue} gemeldet (Zähler ${meterInfo?.meterNumber || meterId})`,
details: { zähler: meterInfo?.meterNumber, stand: parsedValue, datum: parsedDate.toISOString() },
customerId: user.customerId,
});
res.status(201).json({ success: true, data: reading } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -437,6 +795,12 @@ export async function markReadingTransferred(req: AuthRequest, res: Response): P
},
});
await logChange({
req, action: 'UPDATE', resourceType: 'MeterReading',
resourceId: readingId.toString(),
label: `Zählerstand als übertragen markiert`,
});
res.json({ success: true, data: reading } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -476,11 +840,58 @@ export async function getPortalSettings(req: Request, res: Response): Promise<vo
export async function updatePortalSettings(req: Request, res: Response): Promise<void> {
try {
const customerId = parseInt(req.params.customerId);
const { portalEnabled, portalEmail } = req.body;
const settings = await customerService.updatePortalSettings(parseInt(req.params.customerId), {
// Vorherigen Stand laden für Audit
const before = await prisma.customer.findUnique({
where: { id: customerId },
select: { portalEnabled: true, portalEmail: true },
});
const settings = await customerService.updatePortalSettings(customerId, {
portalEnabled,
portalEmail,
});
// Audit: Geänderte Felder ermitteln und loggen
const data: Record<string, unknown> = { portalEnabled, portalEmail };
if (before) {
const changes: Record<string, { von: unknown; nach: unknown }> = {};
const fieldLabels: Record<string, string> = {
portalEnabled: 'Portal aktiv', portalEmail: 'Portal-E-Mail',
};
for (const [key, newVal] of Object.entries(data)) {
if (newVal === undefined) continue;
const oldVal = (before as any)[key];
const norm = (v: unknown) => (v === null || v === undefined || v === '' ? null : v);
if (JSON.stringify(norm(oldVal)) !== JSON.stringify(norm(newVal))) {
const label = fieldLabels[key] || key;
const formatVal = (v: unknown) => {
if (v === null || v === undefined || v === '') return '-';
if (typeof v === 'boolean') return v ? 'Ja' : 'Nein';
return String(v);
};
changes[label] = { von: formatVal(oldVal), nach: formatVal(newVal) };
}
}
const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von}${c.nach}`).join(', ');
await logChange({
req, action: 'UPDATE', resourceType: 'PortalSettings',
resourceId: customerId.toString(),
label: changeList ? `Portal-Einstellungen aktualisiert für Kunde #${customerId}: ${changeList}` : `Portal-Einstellungen aktualisiert für Kunde #${customerId}`,
details: Object.keys(changes).length > 0 ? changes : undefined,
customerId,
});
} else {
await logChange({
req, action: 'UPDATE', resourceType: 'PortalSettings',
resourceId: customerId.toString(),
label: `Portal-Einstellungen aktualisiert für Kunde #${customerId}`,
customerId,
});
}
res.json({ success: true, data: settings } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -500,7 +911,14 @@ export async function setPortalPassword(req: Request, res: Response): Promise<vo
} as ApiResponse);
return;
}
await authService.setCustomerPortalPassword(parseInt(req.params.customerId), password);
const customerId = parseInt(req.params.customerId);
await authService.setCustomerPortalPassword(customerId, password);
await logChange({
req, action: 'UPDATE', resourceType: 'PortalSettings',
resourceId: customerId.toString(),
label: `Portal-Passwort gesetzt für Kunde #${customerId}`,
customerId,
});
res.json({ success: true, message: 'Passwort gesetzt' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -539,12 +957,19 @@ export async function getRepresentatives(req: Request, res: Response): Promise<v
export async function addRepresentative(req: Request, res: Response): Promise<void> {
try {
const customerId = parseInt(req.params.customerId);
const { representativeId, notes } = req.body;
const representative = await customerService.addRepresentative(
parseInt(req.params.customerId),
customerId,
parseInt(representativeId),
notes
);
await logChange({
req, action: 'CREATE', resourceType: 'Representative',
resourceId: representative.id.toString(),
label: `Vertreter hinzugefügt für Kunde #${customerId}`,
customerId,
});
res.status(201).json({ success: true, data: representative } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -556,10 +981,16 @@ export async function addRepresentative(req: Request, res: Response): Promise<vo
export async function removeRepresentative(req: Request, res: Response): Promise<void> {
try {
const customerId = parseInt(req.params.customerId);
await customerService.removeRepresentative(
parseInt(req.params.customerId),
customerId,
parseInt(req.params.representativeId)
);
await logChange({
req, action: 'DELETE', resourceType: 'Representative',
label: `Vertreter entfernt für Kunde #${customerId}`,
customerId,
});
res.json({ success: true, message: 'Vertreter entfernt' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -2,6 +2,7 @@
import { Request, Response } from 'express';
import * as emailProviderService from '../services/emailProvider/emailProviderService.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse } from '../types/index.js';
// ==================== CONFIG CRUD ====================
@@ -43,6 +44,11 @@ export async function getProviderConfig(req: Request, res: Response): Promise<vo
export async function createProviderConfig(req: Request, res: Response): Promise<void> {
try {
const config = await emailProviderService.createProviderConfig(req.body);
await logChange({
req, action: 'CREATE', resourceType: 'EmailProviderConfig',
resourceId: config.id.toString(),
label: `E-Mail-Provider ${config.name} angelegt`,
});
res.status(201).json({ success: true, data: config } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -56,6 +62,11 @@ export async function updateProviderConfig(req: Request, res: Response): Promise
try {
const id = parseInt(req.params.id);
const config = await emailProviderService.updateProviderConfig(id, req.body);
await logChange({
req, action: 'UPDATE', resourceType: 'EmailProviderConfig',
resourceId: id.toString(),
label: `E-Mail-Provider ${config.name} aktualisiert`,
});
res.json({ success: true, data: config } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -68,7 +79,13 @@ export async function updateProviderConfig(req: Request, res: Response): Promise
export async function deleteProviderConfig(req: Request, res: Response): Promise<void> {
try {
const id = parseInt(req.params.id);
const config = await emailProviderService.getProviderConfigById(id);
await emailProviderService.deleteProviderConfig(id);
await logChange({
req, action: 'DELETE', resourceType: 'EmailProviderConfig',
resourceId: id.toString(),
label: `E-Mail-Provider ${config?.name || id} gelöscht`,
});
res.json({ success: true, message: 'Email-Provider gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
+78 -4
View File
@@ -4,16 +4,15 @@ 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 { createAuditLog, logChange } from '../services/audit.service.js';
import { ConsentType, DeletionRequestStatus } from '@prisma/client';
import prisma from '../lib/prisma.js';
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)
*/
@@ -73,6 +72,13 @@ export async function createDeletionRequest(req: AuthRequest, res: Response) {
requestedBy: req.user?.email || 'unknown',
});
await logChange({
req, action: 'CREATE', resourceType: 'DeletionRequest',
resourceId: request.id.toString(),
label: `Löschanfrage erstellt für Kunde #${customerId}`,
customerId,
});
res.status(201).json({ success: true, data: request });
} catch (error) {
console.error('Fehler beim Erstellen der Löschanfrage:', error);
@@ -281,6 +287,13 @@ export async function updateCustomerConsent(req: AuthRequest, res: Response) {
return res.status(400).json({ success: false, error: 'Ungültiger Consent-Typ' });
}
const consentLabels: Record<string, string> = {
DATA_PROCESSING: 'Datenverarbeitung',
MARKETING_EMAIL: 'E-Mail-Marketing',
MARKETING_PHONE: 'Telefonmarketing',
DATA_SHARING_PARTNER: 'Datenweitergabe',
};
const consent = await consentService.updateConsent(customerId, consentType, {
status,
source: source || 'portal',
@@ -290,6 +303,14 @@ export async function updateCustomerConsent(req: AuthRequest, res: Response) {
createdBy: req.user?.email || 'unknown',
});
const consentName = consentLabels[consentType] || consentType;
await logChange({
req, action: 'UPDATE', resourceType: 'CustomerConsent',
label: status === 'GRANTED' ? `Einwilligung "${consentName}" erteilt` : `Einwilligung "${consentName}" widerrufen`,
details: { einwilligung: consentName, status, quelle: source || 'portal' },
customerId,
});
res.json({ success: true, data: consent });
} catch (error) {
console.error('Fehler beim Aktualisieren der Einwilligung:', error);
@@ -350,6 +371,11 @@ export async function updatePrivacyPolicy(req: AuthRequest, res: Response) {
await appSettingService.setSetting('privacyPolicyHtml', html);
await logChange({
req, action: 'UPDATE', resourceType: 'PrivacyPolicy',
label: `Datenschutzerklärung aktualisiert`,
});
res.json({ success: true, message: 'Datenschutzerklärung gespeichert' });
} catch (error) {
console.error('Fehler beim Speichern der Datenschutzerklärung:', error);
@@ -383,6 +409,11 @@ export async function updateAuthorizationTemplate(req: AuthRequest, res: Respons
await appSettingService.setSetting('authorizationTemplateHtml', html);
await logChange({
req, action: 'UPDATE', resourceType: 'AuthorizationTemplate',
label: `Vollmacht-Vorlage aktualisiert`,
});
res.json({ success: true, message: 'Vollmacht-Vorlage gespeichert' });
} catch (error) {
console.error('Fehler beim Speichern der Vollmacht-Vorlage:', error);
@@ -743,6 +774,15 @@ export async function grantAuthorization(req: AuthRequest, res: Response) {
notes,
});
const rep = await prisma.customer.findUnique({ where: { id: representativeId }, select: { firstName: true, lastName: true } });
const repName = rep ? `${rep.firstName} ${rep.lastName}` : `#${representativeId}`;
await logChange({
req, action: 'UPDATE', resourceType: 'Authorization',
resourceId: auth.id.toString(),
label: `Vollmacht für ${repName} erteilt (Admin)`,
customerId,
});
res.json({ success: true, data: auth });
} catch (error) {
console.error('Fehler beim Erteilen der Vollmacht:', error);
@@ -762,6 +802,16 @@ export async function withdrawAuthorization(req: AuthRequest, res: Response) {
const representativeId = parseInt(req.params.representativeId);
const auth = await authorizationService.withdrawAuthorization(customerId, representativeId);
const rep = await prisma.customer.findUnique({ where: { id: representativeId }, select: { firstName: true, lastName: true } });
const repName = rep ? `${rep.firstName} ${rep.lastName}` : `#${representativeId}`;
await logChange({
req, action: 'UPDATE', resourceType: 'Authorization',
resourceId: auth.id.toString(),
label: `Vollmacht für ${repName} widerrufen (Admin)`,
customerId,
});
res.json({ success: true, data: auth });
} catch (error) {
console.error('Fehler beim Widerrufen der Vollmacht:', error);
@@ -791,6 +841,13 @@ export async function uploadAuthorizationDocument(req: AuthRequest, res: Respons
documentPath
);
await logChange({
req, action: 'CREATE', resourceType: 'AuthorizationDocument',
resourceId: auth.id.toString(),
label: `Vollmacht-PDF hochgeladen für Vertreter #${representativeId}`,
customerId,
});
res.json({ success: true, data: auth });
} catch (error) {
console.error('Fehler beim Upload des Vollmacht-Dokuments:', error);
@@ -810,6 +867,14 @@ export async function deleteAuthorizationDocument(req: AuthRequest, res: Respons
const representativeId = parseInt(req.params.representativeId);
const auth = await authorizationService.deleteAuthorizationDocument(customerId, representativeId);
await logChange({
req, action: 'DELETE', resourceType: 'AuthorizationDocument',
resourceId: auth.id.toString(),
label: `Vollmacht-PDF gelöscht für Vertreter #${representativeId}`,
customerId,
});
res.json({ success: true, data: auth });
} catch (error) {
console.error('Fehler beim Löschen des Vollmacht-Dokuments:', error);
@@ -852,13 +917,22 @@ export async function toggleMyAuthorization(req: AuthRequest, res: Response) {
const representativeId = parseInt(req.params.representativeId);
const { grant } = req.body;
// Vertreter-Name laden
const representative = await prisma.customer.findUnique({
where: { id: representativeId },
select: { firstName: true, lastName: true },
});
const repName = representative ? `${representative.firstName} ${representative.lastName}` : `#${representativeId}`;
let auth;
if (grant) {
auth = await authorizationService.grantAuthorization(user.customerId, representativeId, {
source: 'portal',
});
await logChange({ req, action: 'UPDATE', resourceType: 'RepresentativeAuthorization', label: `Vollmacht für ${repName} erteilt`, details: { status: 'erteilt', vertreter: repName, quelle: 'portal' }, customerId: user.customerId });
} else {
auth = await authorizationService.withdrawAuthorization(user.customerId, representativeId);
await logChange({ req, action: 'UPDATE', resourceType: 'RepresentativeAuthorization', label: `Vollmacht für ${repName} widerrufen`, details: { status: 'widerrufen', vertreter: repName, quelle: 'portal' }, customerId: user.customerId });
}
res.json({ success: true, data: auth });
@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import * as invoiceService from '../services/invoice.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse } from '../types/index.js';
/**
@@ -69,6 +70,12 @@ export async function addInvoice(req: Request, res: Response): Promise<void> {
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);
@@ -95,6 +102,12 @@ export async function updateInvoice(req: Request, res: Response): Promise<void>
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);
@@ -115,6 +128,12 @@ export async function deleteInvoice(req: Request, res: Response): Promise<void>
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);
+19 -1
View File
@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import * as platformService from '../services/platform.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse } from '../types/index.js';
export async function getPlatforms(req: Request, res: Response): Promise<void> {
@@ -37,6 +38,11 @@ export async function getPlatform(req: Request, res: Response): Promise<void> {
export async function createPlatform(req: Request, res: Response): Promise<void> {
try {
const platform = await platformService.createPlatform(req.body);
await logChange({
req, action: 'CREATE', resourceType: 'Platform',
resourceId: platform.id.toString(),
label: `Vertriebsplattform ${platform.name} angelegt`,
});
res.status(201).json({ success: true, data: platform } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -49,6 +55,11 @@ export async function createPlatform(req: Request, res: Response): Promise<void>
export async function updatePlatform(req: Request, res: Response): Promise<void> {
try {
const platform = await platformService.updatePlatform(parseInt(req.params.id), req.body);
await logChange({
req, action: 'UPDATE', resourceType: 'Platform',
resourceId: platform.id.toString(),
label: `Vertriebsplattform ${platform.name} aktualisiert`,
});
res.json({ success: true, data: platform } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -60,7 +71,14 @@ export async function updatePlatform(req: Request, res: Response): Promise<void>
export async function deletePlatform(req: Request, res: Response): Promise<void> {
try {
await platformService.deletePlatform(parseInt(req.params.id));
const platformId = parseInt(req.params.id);
const platform = await platformService.getPlatformById(platformId);
await platformService.deletePlatform(platformId);
await logChange({
req, action: 'DELETE', resourceType: 'Platform',
resourceId: platformId.toString(),
label: `Vertriebsplattform ${platform?.name || platformId} gelöscht`,
});
res.json({ success: true, message: 'Vertriebsplattform gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
+19 -1
View File
@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import * as providerService from '../services/provider.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse } from '../types/index.js';
export async function getProviders(req: Request, res: Response): Promise<void> {
@@ -37,6 +38,11 @@ export async function getProvider(req: Request, res: Response): Promise<void> {
export async function createProvider(req: Request, res: Response): Promise<void> {
try {
const provider = await providerService.createProvider(req.body);
await logChange({
req, action: 'CREATE', resourceType: 'Provider',
resourceId: provider.id.toString(),
label: `Anbieter ${provider.name} angelegt`,
});
res.status(201).json({ success: true, data: provider } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -49,6 +55,11 @@ export async function createProvider(req: Request, res: Response): Promise<void>
export async function updateProvider(req: Request, res: Response): Promise<void> {
try {
const provider = await providerService.updateProvider(parseInt(req.params.id), req.body);
await logChange({
req, action: 'UPDATE', resourceType: 'Provider',
resourceId: provider.id.toString(),
label: `Anbieter ${provider.name} aktualisiert`,
});
res.json({ success: true, data: provider } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -60,7 +71,14 @@ export async function updateProvider(req: Request, res: Response): Promise<void>
export async function deleteProvider(req: Request, res: Response): Promise<void> {
try {
await providerService.deleteProvider(parseInt(req.params.id));
const providerId = parseInt(req.params.id);
const provider = await providerService.getProviderById(providerId);
await providerService.deleteProvider(providerId);
await logChange({
req, action: 'DELETE', resourceType: 'Provider',
resourceId: providerId.toString(),
label: `Anbieter ${provider?.name || providerId} gelöscht`,
});
res.json({ success: true, message: 'Anbieter gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import * as stressfreiEmailService from '../services/stressfreiEmail.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse } from '../types/index.js';
export async function getEmailsByCustomer(req: Request, res: Response): Promise<void> {
@@ -42,6 +43,12 @@ export async function createEmail(req: Request, res: Response): Promise<void> {
...req.body,
customerId,
});
await logChange({
req, action: 'CREATE', resourceType: 'StressfreiEmail',
resourceId: email.id.toString(),
label: `Stressfrei-Wechseln Adresse angelegt für Kunde #${customerId}`,
customerId,
});
res.status(201).json({ success: true, data: email } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -54,6 +61,11 @@ export async function createEmail(req: Request, res: Response): Promise<void> {
export async function updateEmail(req: Request, res: Response): Promise<void> {
try {
const email = await stressfreiEmailService.updateEmail(parseInt(req.params.id), req.body);
await logChange({
req, action: 'UPDATE', resourceType: 'StressfreiEmail',
resourceId: email.id.toString(),
label: `Stressfrei-Wechseln Adresse aktualisiert`,
});
res.json({ success: true, data: email } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -65,7 +77,13 @@ export async function updateEmail(req: Request, res: Response): Promise<void> {
export async function deleteEmail(req: Request, res: Response): Promise<void> {
try {
await stressfreiEmailService.deleteEmail(parseInt(req.params.id));
const emailId = parseInt(req.params.id);
await stressfreiEmailService.deleteEmail(emailId);
await logChange({
req, action: 'DELETE', resourceType: 'StressfreiEmail',
resourceId: emailId.toString(),
label: `Stressfrei-Wechseln Adresse gelöscht`,
});
res.json({ success: true, message: 'Stressfrei-Wechseln Adresse gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
+19 -1
View File
@@ -1,5 +1,6 @@
import { Request, Response } from 'express';
import * as tariffService from '../services/tariff.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse } from '../types/index.js';
export async function getTariffs(req: Request, res: Response): Promise<void> {
@@ -39,6 +40,11 @@ export async function createTariff(req: Request, res: Response): Promise<void> {
try {
const providerId = parseInt(req.params.providerId);
const tariff = await tariffService.createTariff({ ...req.body, providerId });
await logChange({
req, action: 'CREATE', resourceType: 'Tariff',
resourceId: tariff.id.toString(),
label: `Tarif ${tariff.name} angelegt`,
});
res.status(201).json({ success: true, data: tariff } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -51,6 +57,11 @@ export async function createTariff(req: Request, res: Response): Promise<void> {
export async function updateTariff(req: Request, res: Response): Promise<void> {
try {
const tariff = await tariffService.updateTariff(parseInt(req.params.id), req.body);
await logChange({
req, action: 'UPDATE', resourceType: 'Tariff',
resourceId: tariff.id.toString(),
label: `Tarif ${tariff.name} aktualisiert`,
});
res.json({ success: true, data: tariff } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -62,7 +73,14 @@ export async function updateTariff(req: Request, res: Response): Promise<void> {
export async function deleteTariff(req: Request, res: Response): Promise<void> {
try {
await tariffService.deleteTariff(parseInt(req.params.id));
const tariffId = parseInt(req.params.id);
const tariff = await tariffService.getTariffById(tariffId);
await tariffService.deleteTariff(tariffId);
await logChange({
req, action: 'DELETE', resourceType: 'Tariff',
resourceId: tariffId.toString(),
label: `Tarif ${tariff?.name || tariffId} gelöscht`,
});
res.json({ success: true, message: 'Tarif gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
+78 -3
View File
@@ -1,5 +1,7 @@
import { Request, Response } from 'express';
import prisma from '../lib/prisma.js';
import * as userService from '../services/user.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse } from '../types/index.js';
// Users
@@ -48,6 +50,11 @@ export async function getUser(req: Request, res: Response): Promise<void> {
export async function createUser(req: Request, res: Response): Promise<void> {
try {
const user = await userService.createUser(req.body);
await logChange({
req, action: 'CREATE', resourceType: 'User',
resourceId: user.id.toString(),
label: `Benutzer ${user.firstName} ${user.lastName} angelegt`,
});
res.status(201).json({ success: true, data: user } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -59,7 +66,49 @@ export async function createUser(req: Request, res: Response): Promise<void> {
export async function updateUser(req: Request, res: Response): Promise<void> {
try {
const user = await userService.updateUser(parseInt(req.params.id), req.body);
const userId = parseInt(req.params.id);
const data = req.body;
// Vorherigen Stand laden für Audit
const before = await prisma.user.findUnique({ where: { id: userId } });
const user = await userService.updateUser(userId, data);
if (user) {
// Audit: Geänderte Felder ermitteln und loggen
if (before) {
const changes: Record<string, { von: unknown; nach: unknown }> = {};
const fieldLabels: Record<string, string> = {
email: 'E-Mail', firstName: 'Vorname', lastName: 'Nachname', isActive: 'Aktiv',
};
for (const [key, newVal] of Object.entries(data)) {
if (['id', 'createdAt', 'updatedAt'].includes(key)) continue;
const oldVal = (before as any)[key];
const norm = (v: unknown) => (v === null || v === undefined || v === '' ? null : v);
if (JSON.stringify(norm(oldVal)) !== JSON.stringify(norm(newVal))) {
const label = fieldLabels[key] || key;
const formatVal = (v: unknown) => {
if (v === null || v === undefined || v === '') return '-';
if (typeof v === 'boolean') return v ? 'Ja' : 'Nein';
return String(v);
};
changes[label] = { von: formatVal(oldVal), nach: formatVal(newVal) };
}
}
const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von}${c.nach}`).join(', ');
await logChange({
req, action: 'UPDATE', resourceType: 'User',
resourceId: user.id.toString(),
label: changeList ? `Benutzer ${user.firstName} ${user.lastName} aktualisiert: ${changeList}` : `Benutzer ${user.firstName} ${user.lastName} aktualisiert`,
details: Object.keys(changes).length > 0 ? changes : undefined,
});
} else {
await logChange({
req, action: 'UPDATE', resourceType: 'User',
resourceId: user.id.toString(),
label: `Benutzer ${user.firstName} ${user.lastName} aktualisiert`,
});
}
}
res.json({ success: true, data: user } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -71,7 +120,14 @@ export async function updateUser(req: Request, res: Response): Promise<void> {
export async function deleteUser(req: Request, res: Response): Promise<void> {
try {
await userService.deleteUser(parseInt(req.params.id));
const userId = parseInt(req.params.id);
const userBefore = await userService.getUserById(userId);
await userService.deleteUser(userId);
await logChange({
req, action: 'DELETE', resourceType: 'User',
resourceId: userId.toString(),
label: `Benutzer ${userBefore?.firstName || ''} ${userBefore?.lastName || ''} gelöscht`,
});
res.json({ success: true, message: 'Benutzer gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -116,6 +172,11 @@ export async function getRole(req: Request, res: Response): Promise<void> {
export async function createRole(req: Request, res: Response): Promise<void> {
try {
const role = await userService.createRole(req.body);
await logChange({
req, action: 'CREATE', resourceType: 'Role',
resourceId: role.id.toString(),
label: `Rolle ${role.name} angelegt`,
});
res.status(201).json({ success: true, data: role } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -128,6 +189,13 @@ export async function createRole(req: Request, res: Response): Promise<void> {
export async function updateRole(req: Request, res: Response): Promise<void> {
try {
const role = await userService.updateRole(parseInt(req.params.id), req.body);
if (role) {
await logChange({
req, action: 'UPDATE', resourceType: 'Role',
resourceId: role.id.toString(),
label: `Rolle ${role.name} aktualisiert`,
});
}
res.json({ success: true, data: role } as ApiResponse);
} catch (error) {
res.status(400).json({
@@ -139,7 +207,14 @@ export async function updateRole(req: Request, res: Response): Promise<void> {
export async function deleteRole(req: Request, res: Response): Promise<void> {
try {
await userService.deleteRole(parseInt(req.params.id));
const roleId = parseInt(req.params.id);
const role = await userService.getRoleById(roleId);
await userService.deleteRole(roleId);
await logChange({
req, action: 'DELETE', resourceType: 'Role',
resourceId: roleId.toString(),
label: `Rolle ${role?.name || roleId} gelöscht`,
});
res.json({ success: true, message: 'Rolle gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
+5
View File
@@ -17,6 +17,11 @@ const AUDITED_MODELS = [
'ContractCategory',
'AppSetting',
'CustomerConsent',
'EnergyContractDetails',
'RepresentativeAuthorization',
'ContractMeter',
'EmailProviderConfig',
'ContractTask',
];
// Sensible Felder die aus dem Audit-Log gefiltert werden
+30 -3
View File
@@ -387,17 +387,44 @@ export function auditMiddleware(req: AuthRequest, res: Response, next: NextFunct
};
// Response-Ende abfangen für Logging
// Audit-Kontext hier erfassen (bevor AsyncLocalStorage den Kontext verliert)
let capturedAuditContext: ReturnType<typeof getAuditContext> | undefined;
const origEnd = res.end;
(res as any).end = function(chunk?: any, encoding?: any, cb?: any) {
// Kontext VOR dem Ende erfassen
capturedAuditContext = getAuditContext();
return origEnd.call(this, chunk, encoding, cb);
};
res.on('finish', () => {
// Async Logging - blockiert nicht die Response
setImmediate(async () => {
try {
const durationMs = Date.now() - startTime;
const action = determineAction(req.method, req.path, responseSuccess);
// READ-Aktionen nicht loggen (nur Änderungen, Logins und Exporte)
if (action === 'READ') return;
// Routen die bereits gezielt via logChange() geloggt werden → nicht doppelt loggen
const manuallyLoggedPaths = [
'/api/customers',
'/api/contracts',
'/api/meters',
'/api/gdpr',
'/api/upload',
];
// Login/Logout immer loggen
if (action !== 'LOGIN' && action !== 'LOGOUT' && action !== 'LOGIN_FAILED') {
if (manuallyLoggedPaths.some(p => req.originalUrl?.startsWith(p) || req.baseUrl?.startsWith(p))) return;
}
const resourceId = mapping.extractId?.(req);
const dataSubjectId = extractDataSubjectId(req);
// Audit-Kontext abrufen (enthält Before/After-Werte von Prisma Middleware)
const auditContext = getAuditContext();
// Audit-Kontext nutzen (wurde vor Response-Ende erfasst)
const auditContext = capturedAuditContext;
// Menschenlesbares Label generieren
const resourceLabel = generateHumanLabel(action, mapping.type, req, responseBody);
@@ -405,7 +432,7 @@ export function auditMiddleware(req: AuthRequest, res: Response, next: NextFunct
await createAuditLog({
userId: req.user?.userId,
userEmail: req.user?.email || 'anonymous',
userRole: req.user?.permissions?.join(', '),
userRole: req.user?.isCustomerPortal ? 'Kundenportal' : (req.user as any)?.roleName || 'Mitarbeiter',
customerId: req.user?.customerId,
isCustomerPortal: req.user?.isCustomerPortal,
action,
+1 -3
View File
@@ -1,10 +1,8 @@
import { Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { PrismaClient } from '@prisma/client';
import prisma from '../lib/prisma.js';
import { AuthRequest, JwtPayload } from '../types/index.js';
const prisma = new PrismaClient();
export async function authenticate(
req: AuthRequest,
res: Response,
+8 -5
View File
@@ -10,17 +10,20 @@ router.use(authenticate);
// Audit-Logs abrufen
router.get('/', requirePermission('audit:read'), auditLogController.getAuditLogs);
// Einzelnes Audit-Log abrufen
router.get('/:id', requirePermission('audit:read'), auditLogController.getAuditLogById);
// Audit-Logs exportieren (muss VOR /:id stehen!)
router.get('/export', requirePermission('audit:read'), auditLogController.exportAuditLogs);
// Audit-Logs für einen Kunden (DSGVO)
router.get('/customer/:customerId', requirePermission('audit:read'), auditLogController.getAuditLogsByCustomer);
// Audit-Logs exportieren
router.get('/export', requirePermission('audit:export'), auditLogController.exportAuditLogs);
// Einzelnes Audit-Log abrufen
router.get('/:id', requirePermission('audit:read'), auditLogController.getAuditLogById);
// Hash-Ketten-Integrität prüfen
router.post('/verify', requirePermission('audit:admin'), auditLogController.verifyIntegrity);
router.post('/verify', requirePermission('audit:read'), auditLogController.verifyIntegrity);
// Hash-Kette reparieren
router.post('/rehash', requirePermission('audit:admin'), auditLogController.rehashAll);
// Retention-Policies
router.get('/retention-policies', requirePermission('audit:admin'), auditLogController.getRetentionPolicies);
+2 -2
View File
@@ -1,10 +1,10 @@
import { Router, Response } from 'express';
import { PrismaClient, Prisma } from '@prisma/client';
import { Prisma } from '@prisma/client';
import prisma from '../lib/prisma.js';
import { authenticate, requirePermission } from '../middleware/auth.js';
import { AuthRequest } from '../types/index.js';
const router = Router();
const prisma = new PrismaClient();
// Setup-Endpunkt: Erstellt die developer:access Permission und fügt sie der Admin-Rolle hinzu
// Dieser Endpunkt erfordert keine Authentifizierung, da er nur einmalig zum Setup verwendet wird
+20 -2
View File
@@ -2,12 +2,12 @@ import { Router, Response } from 'express';
import multer from 'multer';
import path from 'path';
import fs from 'fs';
import { PrismaClient } from '@prisma/client';
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';
const router = Router();
const prisma = new PrismaClient();
// Uploads-Verzeichnis erstellen falls nicht vorhanden
const uploadsDir = path.join(process.cwd(), 'uploads');
@@ -450,6 +450,15 @@ router.post(
});
}
// 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: {
@@ -504,6 +513,15 @@ router.delete(
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);
+1 -3
View File
@@ -1,6 +1,4 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
import prisma from '../lib/prisma.js';
// Default settings
const DEFAULT_SETTINGS: Record<string, string> = {
+83 -9
View File
@@ -3,6 +3,42 @@ import crypto from 'crypto';
import { encrypt, decrypt } from '../utils/encryption.js';
import prisma from '../lib/prisma.js';
/**
* Vereinfachte Audit-Log-Funktion für gezielte Änderungsprotokolle.
* Wird direkt in Controllern aufgerufen mit aussagekräftigen Details.
*/
export async function logChange(opts: {
req: any; // Express Request (für userId, email, IP)
action: AuditAction;
resourceType: string;
resourceId?: string;
label: string; // Menschenlesbares Label z.B. "Vollmacht für Stefan Hacker widerrufen"
details?: Record<string, unknown>; // Zusätzliche Details z.B. { vorher: 'erteilt', nachher: 'widerrufen' }
customerId?: number;
}) {
try {
const user = opts.req?.user;
await createAuditLog({
userId: user?.userId,
userEmail: user?.email || 'system',
userRole: user?.isCustomerPortal ? 'Kundenportal' : 'Mitarbeiter',
customerId: user?.customerId,
isCustomerPortal: user?.isCustomerPortal,
action: opts.action,
resourceType: opts.resourceType,
resourceId: opts.resourceId,
resourceLabel: opts.label,
endpoint: opts.req?.path || '',
httpMethod: opts.req?.method || '',
ipAddress: opts.req?.socket?.remoteAddress || opts.req?.headers?.['x-forwarded-for'] || 'unknown',
dataSubjectId: opts.customerId,
changesAfter: opts.details,
});
} catch (error) {
console.error('[logChange] Fehler:', error);
}
}
export interface CreateAuditLogData {
userId?: number;
userEmail: string;
@@ -101,16 +137,11 @@ function determineSensitivity(resourceType: string): AuditSensitivity {
}
/**
* Prüft ob Änderungen verschlüsselt werden sollen
* Prüft ob Änderungen verschlüsselt werden sollen.
* Deaktiviert - sensible Felder werden bereits von der Prisma-Middleware als [REDACTED] gefiltert.
*/
function shouldEncryptChanges(resourceType: string): boolean {
const encryptedTypes = [
'BankCard',
'IdentityDocument',
'User',
'Customer', // Enthält Portal-Passwörter
];
return encryptedTypes.includes(resourceType);
function shouldEncryptChanges(_resourceType: string): boolean {
return false;
}
/**
@@ -381,6 +412,49 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
};
}
/**
* Hash-Kette komplett neu berechnen (Reparatur)
*/
export async function rehashAll(): Promise<{ rehashedCount: number }> {
const logs = await prisma.auditLog.findMany({
orderBy: { id: 'asc' },
select: {
id: true,
userEmail: true,
action: true,
resourceType: true,
resourceId: true,
endpoint: true,
createdAt: true,
},
});
let previousHash: string | null = null;
let count = 0;
for (const log of logs) {
const hash = generateHash({
userEmail: log.userEmail,
action: log.action,
resourceType: log.resourceType,
resourceId: log.resourceId,
endpoint: log.endpoint,
createdAt: log.createdAt,
previousHash,
});
await prisma.auditLog.update({
where: { id: log.id },
data: { hash, previousHash },
});
previousHash = hash;
count++;
}
return { rehashedCount: count };
}
/**
* Exportiert Audit-Logs als JSON oder CSV
*/
+1 -3
View File
@@ -1,11 +1,9 @@
import { PrismaClient } from '@prisma/client';
import prisma from '../lib/prisma.js';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { JwtPayload } from '../types/index.js';
import { encrypt, decrypt } from '../utils/encryption.js';
const prisma = new PrismaClient();
// Mitarbeiter-Login
export async function login(email: string, password: string) {
const user = await prisma.user.findUnique({
+1 -3
View File
@@ -4,15 +4,13 @@
* Ermöglicht Backup und Restore der Datenbank und Uploads über die Web-Oberfläche.
*/
import { PrismaClient } from '@prisma/client';
import prisma from '../lib/prisma.js';
import * as fs from 'fs';
import * as path from 'path';
import archiver from 'archiver';
import AdmZip from 'adm-zip';
import bcrypt from 'bcryptjs';
const prisma = new PrismaClient();
// Verzeichnisse
const BACKUPS_DIR = path.join(__dirname, '../../prisma/backups');
const UPLOADS_DIR = path.join(process.cwd(), 'uploads');
+2 -3
View File
@@ -1,13 +1,12 @@
// ==================== CACHED EMAIL SERVICE ====================
// Service für E-Mail-Caching und Vertragszuordnung
import { PrismaClient, CachedEmail, Prisma, EmailFolder } from '@prisma/client';
import { CachedEmail, Prisma, EmailFolder } from '@prisma/client';
import prisma from '../lib/prisma.js';
import { decrypt } from '../utils/encryption.js';
import { fetchEmails, ImapCredentials, FetchedEmail, moveToTrash, restoreFromTrash, permanentDelete } from './imapService.js';
import { getImapSmtpSettings } from './emailProvider/emailProviderService.js';
const prisma = new PrismaClient();
// ==================== TYPES ====================
export interface CachedEmailWithRelations extends CachedEmail {
@@ -1,6 +1,4 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
import prisma from '../lib/prisma.js';
export async function getAllCancellationPeriods(includeInactive = false) {
const where = includeInactive ? {} : { isActive: true };
+2 -2
View File
@@ -253,8 +253,8 @@ export const CONSENT_TYPE_LABELS: Record<ConsentType, { label: string; descripti
description: 'Grundlegende Verarbeitung personenbezogener Daten zur Vertragserfüllung',
},
MARKETING_EMAIL: {
label: 'E-Mail-Marketing',
description: 'Zusendung von Werbung und Angeboten per E-Mail',
label: 'Elektronisches Marketing',
description: 'Zusendung von Werbung und Angeboten über elektronische Kommunikationswege (E-Mail, Messenger etc.)',
},
MARKETING_PHONE: {
label: 'Telefonmarketing',
@@ -1,6 +1,4 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
import prisma from '../lib/prisma.js';
export async function getAllContractDurations(includeInactive = false) {
const where = includeInactive ? {} : { isActive: true };
+2 -3
View File
@@ -1,9 +1,8 @@
import { PrismaClient, ContractType, ContractStatus } from '@prisma/client';
import { ContractType, ContractStatus } from '@prisma/client';
import prisma from '../lib/prisma.js';
import { generateContractNumber, paginate, buildPaginationResponse } from '../utils/helpers.js';
import { encrypt, decrypt } from '../utils/encryption.js';
const prisma = new PrismaClient();
export interface ContractFilters {
customerId?: number;
customerIds?: number[]; // Für Kundenportal: eigene ID + vertretene Kunden
@@ -1,6 +1,4 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
import prisma from '../lib/prisma.js';
export async function getAllContractCategories(includeInactive = false) {
return prisma.contractCategory.findMany({
@@ -1,9 +1,7 @@
import { PrismaClient, ContractStatus
} from '@prisma/client';
import { ContractStatus } from '@prisma/client';
import prisma from '../lib/prisma.js';
import * as appSettingService from './appSetting.service.js';
const prisma = new PrismaClient();
// Typen für das Cockpit
export type UrgencyLevel = 'critical' | 'warning' | 'ok' | 'none';
@@ -1,6 +1,4 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
import prisma from '../lib/prisma.js';
export interface CreateHistoryEntryData {
title: string;
+2 -3
View File
@@ -1,6 +1,5 @@
import { PrismaClient, ContractTaskStatus } from '@prisma/client';
const prisma = new PrismaClient();
import { ContractTaskStatus } from '@prisma/client';
import prisma from '../lib/prisma.js';
export interface ContractTaskFilters {
contractId: number;
+2 -3
View File
@@ -1,10 +1,9 @@
import { PrismaClient, CustomerType, ContractStatus } from '@prisma/client';
import { CustomerType, ContractStatus } from '@prisma/client';
import prisma from '../lib/prisma.js';
import { generateCustomerNumber, paginate, buildPaginationResponse } from '../utils/helpers.js';
import fs from 'fs';
import path from 'path';
const prisma = new PrismaClient();
// Helper zum Löschen von Dateien
function deleteFileIfExists(filePath: string | null) {
if (!filePath) return;
@@ -1,6 +1,6 @@
// ==================== EMAIL PROVIDER SERVICE ====================
import { PrismaClient } from '@prisma/client';
import prisma from '../../lib/prisma.js';
import { decrypt } from '../../utils/encryption.js';
import {
IEmailProvider,
@@ -12,8 +12,6 @@ import {
} from './types.js';
import { PleskEmailProvider } from './pleskProvider.js';
const prisma = new PrismaClient();
// Factory-Funktion um den richtigen Provider zu erstellen
function createProvider(config: EmailProviderConfig): IEmailProvider {
switch (config.type) {
+2 -3
View File
@@ -1,9 +1,8 @@
import { PrismaClient, InvoiceType } from '@prisma/client';
import { InvoiceType } from '@prisma/client';
import prisma from '../lib/prisma.js';
import fs from 'fs';
import path from 'path';
const prisma = new PrismaClient();
export interface CreateInvoiceData {
invoiceDate: Date;
invoiceType: InvoiceType;
+1 -3
View File
@@ -1,6 +1,4 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
import prisma from '../lib/prisma.js';
export async function getAllPlatforms(includeInactive = false) {
const where = includeInactive ? {} : { isActive: true };
+1 -3
View File
@@ -1,6 +1,4 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
import prisma from '../lib/prisma.js';
export async function getAllProviders(includeInactive = false) {
const where = includeInactive ? {} : { isActive: true };
@@ -1,4 +1,4 @@
import { PrismaClient } from '@prisma/client';
import prisma from '../lib/prisma.js';
import { encrypt, decrypt } from '../utils/encryption.js';
import {
provisionEmail,
@@ -10,8 +10,6 @@ import {
} from './emailProvider/emailProviderService.js';
import { generateSecurePassword } from '../utils/passwordGenerator.js';
const prisma = new PrismaClient();
export async function getEmailsByCustomerId(customerId: number, includeInactive = false) {
const where: Record<string, unknown> = { customerId };
if (!includeInactive) {
+1 -3
View File
@@ -1,6 +1,4 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
import prisma from '../lib/prisma.js';
export async function getTariffsByProvider(providerId: number, includeInactive = false) {
const where: { providerId: number; isActive?: boolean } = { providerId };
+1 -3
View File
@@ -1,9 +1,7 @@
import { PrismaClient } from '@prisma/client';
import prisma from '../lib/prisma.js';
import bcrypt from 'bcryptjs';
import { paginate, buildPaginationResponse } from '../utils/helpers.js';
const prisma = new PrismaClient();
export interface UserFilters {
search?: string;
isActive?: boolean;