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

This commit is contained in:
2026-03-21 11:59:53 +01:00
parent 89cf92eaf5
commit f2876f877e
1491 changed files with 265550 additions and 1292 deletions
@@ -0,0 +1,103 @@
-- CreateTable
CREATE TABLE `AuditLog` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`userId` INTEGER NULL,
`userEmail` VARCHAR(191) NOT NULL,
`userRole` VARCHAR(191) NULL,
`customerId` INTEGER NULL,
`isCustomerPortal` BOOLEAN NOT NULL DEFAULT false,
`action` ENUM('CREATE', 'READ', 'UPDATE', 'DELETE', 'EXPORT', 'ANONYMIZE', 'LOGIN', 'LOGOUT', 'LOGIN_FAILED') NOT NULL,
`sensitivity` ENUM('LOW', 'MEDIUM', 'HIGH', 'CRITICAL') NOT NULL DEFAULT 'MEDIUM',
`resourceType` VARCHAR(191) NOT NULL,
`resourceId` VARCHAR(191) NULL,
`resourceLabel` VARCHAR(191) NULL,
`endpoint` VARCHAR(191) NOT NULL,
`httpMethod` VARCHAR(191) NOT NULL,
`ipAddress` VARCHAR(191) NOT NULL,
`userAgent` TEXT NULL,
`changesBefore` LONGTEXT NULL,
`changesAfter` LONGTEXT NULL,
`changesEncrypted` BOOLEAN NOT NULL DEFAULT false,
`dataSubjectId` INTEGER NULL,
`legalBasis` VARCHAR(191) NULL,
`success` BOOLEAN NOT NULL DEFAULT true,
`errorMessage` TEXT NULL,
`durationMs` INTEGER NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`hash` VARCHAR(191) NULL,
`previousHash` VARCHAR(191) NULL,
INDEX `AuditLog_userId_idx`(`userId`),
INDEX `AuditLog_customerId_idx`(`customerId`),
INDEX `AuditLog_resourceType_resourceId_idx`(`resourceType`, `resourceId`),
INDEX `AuditLog_dataSubjectId_idx`(`dataSubjectId`),
INDEX `AuditLog_action_idx`(`action`),
INDEX `AuditLog_createdAt_idx`(`createdAt`),
INDEX `AuditLog_sensitivity_idx`(`sensitivity`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `CustomerConsent` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`customerId` INTEGER NOT NULL,
`consentType` ENUM('DATA_PROCESSING', 'MARKETING_EMAIL', 'MARKETING_PHONE', 'DATA_SHARING_PARTNER') NOT NULL,
`status` ENUM('GRANTED', 'WITHDRAWN', 'PENDING') NOT NULL DEFAULT 'PENDING',
`grantedAt` DATETIME(3) NULL,
`withdrawnAt` DATETIME(3) NULL,
`source` VARCHAR(191) NULL,
`documentPath` VARCHAR(191) NULL,
`version` VARCHAR(191) NULL,
`ipAddress` VARCHAR(191) NULL,
`createdBy` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
INDEX `CustomerConsent_customerId_idx`(`customerId`),
INDEX `CustomerConsent_consentType_idx`(`consentType`),
INDEX `CustomerConsent_status_idx`(`status`),
UNIQUE INDEX `CustomerConsent_customerId_consentType_key`(`customerId`, `consentType`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `DataDeletionRequest` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`customerId` INTEGER NOT NULL,
`status` ENUM('PENDING', 'IN_PROGRESS', 'COMPLETED', 'PARTIALLY_COMPLETED', 'REJECTED') NOT NULL DEFAULT 'PENDING',
`requestedAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`requestSource` VARCHAR(191) NOT NULL,
`requestedBy` VARCHAR(191) NOT NULL,
`processedAt` DATETIME(3) NULL,
`processedBy` VARCHAR(191) NULL,
`deletedData` LONGTEXT NULL,
`retainedData` LONGTEXT NULL,
`retentionReason` TEXT NULL,
`proofDocument` VARCHAR(191) NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
INDEX `DataDeletionRequest_customerId_idx`(`customerId`),
INDEX `DataDeletionRequest_status_idx`(`status`),
INDEX `DataDeletionRequest_requestedAt_idx`(`requestedAt`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `AuditRetentionPolicy` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`resourceType` VARCHAR(191) NOT NULL,
`sensitivity` ENUM('LOW', 'MEDIUM', 'HIGH', 'CRITICAL') NULL,
`retentionDays` INTEGER NOT NULL,
`description` VARCHAR(191) NULL,
`legalBasis` VARCHAR(191) NULL,
`isActive` BOOLEAN NOT NULL DEFAULT true,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `AuditRetentionPolicy_resourceType_sensitivity_key`(`resourceType`, `sensitivity`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `CustomerConsent` ADD CONSTRAINT `CustomerConsent_customerId_fkey` FOREIGN KEY (`customerId`) REFERENCES `Customer`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,10 @@
-- AlterTable
ALTER TABLE `Customer` ADD COLUMN `consentHash` VARCHAR(191) NULL;
-- AlterTable
ALTER TABLE `User` ADD COLUMN `whatsappNumber` VARCHAR(191) NULL,
ADD COLUMN `telegramUsername` VARCHAR(191) NULL,
ADD COLUMN `signalNumber` VARCHAR(191) NULL;
-- CreateIndex
CREATE UNIQUE INDEX `Customer_consentHash_key` ON `Customer`(`consentHash`);
+252 -4
View File
@@ -7,6 +7,36 @@ datasource db {
url = env("DATABASE_URL")
}
// ==================== EMAIL LOG ====================
model EmailLog {
id Int @id @default(autoincrement())
// Absender & Empfänger
fromAddress String // Absender-E-Mail
toAddress String // Empfänger-E-Mail
subject String // Betreff
// Versand-Kontext
context String // z.B. "consent-link", "authorization-request", "customer-email"
customerId Int? // Zugehöriger Kunde (falls vorhanden)
triggeredBy String? // Wer hat den Versand ausgelöst (User-Email)
// SMTP-Details
smtpServer String // SMTP-Server
smtpPort Int // SMTP-Port
smtpEncryption String // SSL, STARTTLS, NONE
smtpUser String // SMTP-Benutzername
// Ergebnis
success Boolean // Erfolgreich?
messageId String? // Message-ID aus SMTP-Antwort
errorMessage String? @db.Text // Fehlermeldung bei Fehler
smtpResponse String? @db.Text // SMTP-Server-Antwort
// Zeitstempel
sentAt DateTime @default(now())
@@index([sentAt])
@@index([customerId])
@@index([success])
}
// ==================== APP SETTINGS ====================
model AppSetting {
@@ -27,6 +57,12 @@ model User {
lastName String
isActive Boolean @default(true)
tokenInvalidatedAt DateTime? // Zeitpunkt ab dem alle Tokens ungültig sind (für Zwangslogout bei Rechteänderung)
// Messaging-Kanäle (für Datenschutz-Link-Versand)
whatsappNumber String?
telegramUsername String?
signalNumber String?
customerId Int? @unique
customer Customer? @relation(fields: [customerId], references: [id])
roles UserRole[]
@@ -97,6 +133,7 @@ model Customer {
commercialRegisterPath String? // PDF-Pfad zum Handelsregisterauszug
commercialRegisterNumber String? // Handelsregisternummer (Text)
privacyPolicyPath String? // PDF-Pfad zur Datenschutzerklärung (für alle Kunden)
consentHash String? @unique // Permanenter Hash für öffentlichen Einwilligungslink /datenschutz/<hash>
notes String? @db.Text
// ===== Portal-Zugangsdaten =====
@@ -118,6 +155,13 @@ model Customer {
representingFor CustomerRepresentative[] @relation("RepresentativeCustomer")
representedBy CustomerRepresentative[] @relation("RepresentedCustomer")
// Vollmachten
authorizationsGiven RepresentativeAuthorization[] @relation("AuthorizationCustomer")
authorizationsReceived RepresentativeAuthorization[] @relation("AuthorizationRepresentative")
// DSGVO: Einwilligungen
consents CustomerConsent[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
@@ -140,6 +184,28 @@ model CustomerRepresentative {
@@unique([customerId, representativeId]) // Keine doppelten Einträge
}
// ==================== VOLLMACHTEN ====================
// Vollmacht: Kunde B erteilt Kunde A die Vollmacht, seine Daten einzusehen
// Ohne Vollmacht kann der Vertreter die Verträge des Kunden NICHT sehen
model RepresentativeAuthorization {
id Int @id @default(autoincrement())
customerId Int // Der Kunde, der die Vollmacht erteilt (z.B. Mutter)
customer Customer @relation("AuthorizationCustomer", fields: [customerId], references: [id], onDelete: Cascade)
representativeId Int // Der Vertreter, der Zugriff bekommt (z.B. Sohn)
representative Customer @relation("AuthorizationRepresentative", fields: [representativeId], references: [id], onDelete: Cascade)
isGranted Boolean @default(false) // Vollmacht erteilt?
grantedAt DateTime? // Wann erteilt
withdrawnAt DateTime? // Wann widerrufen
source String? // Quelle: 'portal', 'papier', 'crm-backend'
documentPath String? // PDF-Upload der unterschriebenen Vollmacht
notes String? @db.Text // Notizen
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([customerId, representativeId]) // Eine Vollmacht pro Paar
}
// ==================== ADDRESSES ====================
enum AddressType {
@@ -247,6 +313,10 @@ model EmailProviderConfig {
smtpEncryption MailEncryption @default(SSL) // SSL, STARTTLS oder NONE
allowSelfSignedCerts Boolean @default(false) // Selbstsignierte Zertifikate erlauben
// System-E-Mail für automatisierte Nachrichten (z.B. DSGVO Consent-Links)
systemEmailAddress String? // z.B. "info@stressfrei-wechseln.de"
systemEmailPasswordEncrypted String? // Passwort (verschlüsselt)
isActive Boolean @default(true)
isDefault Boolean @default(false) // Standard-Provider
createdAt DateTime @default(now())
@@ -356,14 +426,25 @@ model Meter {
}
model MeterReading {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
meterId Int
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
readingDate DateTime
value Float
unit String @default("kWh")
unit String @default("kWh")
notes String?
createdAt DateTime @default(now())
// Meldung & Übertragung
reportedBy String? // Wer hat gemeldet? (E-Mail des Portal-Kunden oder Mitarbeiter)
status MeterReadingStatus @default(RECORDED)
transferredAt DateTime? // Wann wurde der Stand an den Anbieter übertragen?
transferredBy String? // Wer hat übertragen?
createdAt DateTime @default(now())
}
enum MeterReadingStatus {
RECORDED // Erfasst (vom Mitarbeiter)
REPORTED // Vom Kunden gemeldet (Portal)
TRANSFERRED // An Anbieter übertragen
}
// ==================== SALES PLATFORMS ====================
@@ -759,3 +840,170 @@ model CarInsuranceDetails {
policyNumber String?
previousInsurer String?
}
// ==================== AUDIT LOGGING (DSGVO) ====================
enum AuditAction {
CREATE
READ
UPDATE
DELETE
EXPORT // DSGVO-Datenexport
ANONYMIZE // Recht auf Vergessenwerden
LOGIN
LOGOUT
LOGIN_FAILED
}
enum AuditSensitivity {
LOW // Einstellungen, Plattformen
MEDIUM // Verträge, Tarife
HIGH // Kundendaten, Bankdaten
CRITICAL // Authentifizierung, Ausweisdokumente
}
model AuditLog {
id Int @id @default(autoincrement())
// Wer
userId Int? // Staff User (null bei Kundenportal/System)
userEmail String
userRole String? @db.Text // Rolle zum Zeitpunkt der Aktion
customerId Int? // Bei Kundenportal-Zugriff
isCustomerPortal Boolean @default(false)
// Was
action AuditAction
sensitivity AuditSensitivity @default(MEDIUM)
// Welche Ressource
resourceType String // Prisma Model Name
resourceId String? // ID des Datensatzes
resourceLabel String? // Lesbare Bezeichnung
// Kontext
endpoint String // API-Pfad
httpMethod String // GET, POST, PUT, DELETE
ipAddress String
userAgent String? @db.Text
// Änderungen (JSON, bei sensiblen Daten verschlüsselt)
changesBefore String? @db.LongText
changesAfter String? @db.LongText
changesEncrypted Boolean @default(false)
// DSGVO
dataSubjectId Int? // Betroffene Person (für Reports)
legalBasis String? // Rechtsgrundlage
// Status
success Boolean @default(true)
errorMessage String? @db.Text
durationMs Int?
// Unveränderlichkeit (Hash-Kette)
createdAt DateTime @default(now())
hash String? // SHA-256 Hash des Eintrags
previousHash String? // Hash des vorherigen Eintrags
@@index([userId])
@@index([customerId])
@@index([resourceType, resourceId])
@@index([dataSubjectId])
@@index([action])
@@index([createdAt])
@@index([sensitivity])
}
// ==================== CONSENT MANAGEMENT (DSGVO) ====================
enum ConsentType {
DATA_PROCESSING // Grundlegende Datenverarbeitung
MARKETING_EMAIL // E-Mail-Marketing
MARKETING_PHONE // Telefon-Marketing
DATA_SHARING_PARTNER // Weitergabe an Partner
}
enum ConsentStatus {
GRANTED
WITHDRAWN
PENDING
}
model CustomerConsent {
id Int @id @default(autoincrement())
customerId Int
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
consentType ConsentType
status ConsentStatus @default(PENDING)
grantedAt DateTime?
withdrawnAt DateTime?
source String? // "portal", "telefon", "papier", "email"
documentPath String? // Unterschriebenes Dokument
version String? // Version der Datenschutzerklärung
ipAddress String?
createdBy String // User der die Einwilligung erfasst hat
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([customerId, consentType])
@@index([customerId])
@@index([consentType])
@@index([status])
}
// ==================== DATA DELETION REQUESTS (DSGVO) ====================
enum DeletionRequestStatus {
PENDING // Anfrage eingegangen
IN_PROGRESS // Wird bearbeitet
COMPLETED // Abgeschlossen
PARTIALLY_COMPLETED // Teildaten behalten (rechtliche Gründe)
REJECTED // Abgelehnt
}
model DataDeletionRequest {
id Int @id @default(autoincrement())
customerId Int
status DeletionRequestStatus @default(PENDING)
requestedAt DateTime @default(now())
requestSource String // "email", "portal", "brief"
requestedBy String // Wer hat angefragt
processedAt DateTime?
processedBy String? // Mitarbeiter der bearbeitet hat
deletedData String? @db.LongText // JSON: Was wurde gelöscht
retainedData String? @db.LongText // JSON: Was wurde behalten + Grund
retentionReason String? @db.Text // Begründung für Aufbewahrung
proofDocument String? // Pfad zum Löschnachweis-PDF
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([customerId])
@@index([status])
@@index([requestedAt])
}
// ==================== AUDIT RETENTION POLICIES ====================
model AuditRetentionPolicy {
id Int @id @default(autoincrement())
resourceType String // "*" für Standard, oder spezifischer Model-Name
sensitivity AuditSensitivity?
retentionDays Int // Aufbewahrungsfrist in Tagen (z.B. 3650 = 10 Jahre)
description String?
legalBasis String? // Gesetzliche Grundlage (z.B. "AO §147", "HGB §257")
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([resourceType, sensitivity])
}
+148 -4
View File
@@ -1,5 +1,6 @@
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
const prisma = new PrismaClient();
@@ -26,6 +27,9 @@ async function main() {
// Spezial-Permissions
developer: ['access'],
emails: ['delete'],
// DSGVO & Audit
audit: ['read', 'export', 'admin'],
gdpr: ['export', 'delete', 'admin'],
};
const permissions: { resource: string; action: string }[] = [];
@@ -60,10 +64,42 @@ async function main() {
(p) => p.resource === 'providers' && p.action === 'read'
);
// Helper: Sync permissions for a role (adds missing, removes excess)
async function syncRolePermissions(roleId: number, permissionIds: number[]) {
const existing = await prisma.rolePermission.findMany({
where: { roleId },
select: { permissionId: true },
});
const existingIds = new Set(existing.map((e) => e.permissionId));
const targetIds = new Set(permissionIds);
// Add missing permissions
const missing = permissionIds.filter((id) => !existingIds.has(id));
if (missing.length > 0) {
await prisma.rolePermission.createMany({
data: missing.map((permissionId) => ({ roleId, permissionId })),
skipDuplicates: true,
});
console.log(`${missing.length} Permissions hinzugefügt für Rolle #${roleId}`);
}
// Remove excess permissions
const excess = existing.filter((e) => !targetIds.has(e.permissionId)).map((e) => e.permissionId);
if (excess.length > 0) {
await prisma.rolePermission.deleteMany({
where: { roleId, permissionId: { in: excess } },
});
console.log(`${excess.length} Permissions entfernt für Rolle #${roleId}`);
}
}
// Create roles
// Admin - all permissions EXCEPT developer:access (that's controlled separately)
// Admin - all permissions EXCEPT developer:access and audit/gdpr (controlled separately via checkboxes)
const adminPermissions = allPermissions.filter(
(p) => !(p.resource === 'developer' && p.action === 'access')
(p) =>
!(p.resource === 'developer' && p.action === 'access') &&
p.resource !== 'audit' &&
p.resource !== 'gdpr'
);
const adminRole = await prisma.role.upsert({
where: { name: 'Admin' },
@@ -76,8 +112,10 @@ async function main() {
},
},
});
await syncRolePermissions(adminRole.id, adminPermissions.map((p) => p.id));
// Developer - ALL permissions including developer:access
// Developer - ALL permissions (developer:access + alles andere)
const developerPermissions = allPermissions;
const developerRole = await prisma.role.upsert({
where: { name: 'Developer' },
update: {},
@@ -85,10 +123,28 @@ async function main() {
name: 'Developer',
description: 'Voller Zugriff inkl. Entwickler-Tools',
permissions: {
create: allPermissions.map((p) => ({ permissionId: p.id })),
create: developerPermissions.map((p) => ({ permissionId: p.id })),
},
},
});
await syncRolePermissions(developerRole.id, developerPermissions.map((p) => p.id));
// DSGVO - audit and gdpr permissions (hidden role, controlled via hasGdprAccess)
const gdprPermissions = allPermissions.filter(
(p) => p.resource === 'audit' || p.resource === 'gdpr'
);
const gdprRole = await prisma.role.upsert({
where: { name: 'DSGVO' },
update: {},
create: {
name: 'DSGVO',
description: 'DSGVO-Zugriff: Audit-Logs und Datenschutz-Verwaltung',
permissions: {
create: gdprPermissions.map((p) => ({ permissionId: p.id })),
},
},
});
await syncRolePermissions(gdprRole.id, gdprPermissions.map((p) => p.id));
// Employee - full access to customers, contracts, read access to lookup tables
const employeePermIds = allPermissions
@@ -119,6 +175,7 @@ async function main() {
},
},
});
await syncRolePermissions(employeeRole.id, employeePermIds);
// Read-only employee - read access to main entities and lookup tables
const readOnlyResources = [
@@ -146,6 +203,7 @@ async function main() {
},
},
});
await syncRolePermissions(readOnlyRole.id, readOnlyPermIds);
// Customer role - read own data only (handled in middleware)
const customerRole = await prisma.role.upsert({
@@ -159,6 +217,7 @@ async function main() {
},
},
});
await syncRolePermissions(customerRole.id, readOnlyPermIds);
console.log('Roles created');
@@ -346,6 +405,91 @@ async function main() {
console.log('App settings created');
// ==================== AUDIT RETENTION POLICIES (DSGVO) ====================
// Standard-Policy (ohne Sensitivity)
const existingDefault = await prisma.auditRetentionPolicy.findFirst({
where: { resourceType: '*', sensitivity: null },
});
if (!existingDefault) {
await prisma.auditRetentionPolicy.create({
data: {
resourceType: '*',
sensitivity: null,
retentionDays: 3650, // 10 Jahre
description: 'Standard-Aufbewahrungsfrist',
legalBasis: 'AO §147, HGB §257',
},
});
}
// Spezifische Policies mit Sensitivity
const specificPolicies = [
{
resourceType: 'Authentication',
sensitivity: 'CRITICAL' as const,
retentionDays: 730, // 2 Jahre
description: 'Login-Versuche und Authentifizierung',
legalBasis: 'Sicherheitsanforderungen',
},
{
resourceType: 'Customer',
sensitivity: 'HIGH' as const,
retentionDays: 3650, // 10 Jahre
description: 'Kundendaten-Zugriffe',
legalBasis: 'Steuerrecht (AO §147)',
},
{
resourceType: 'Contract',
sensitivity: 'MEDIUM' as const,
retentionDays: 3650, // 10 Jahre
description: 'Vertragsdaten-Zugriffe',
legalBasis: 'Steuerrecht (AO §147)',
},
{
resourceType: 'AppSetting',
sensitivity: 'LOW' as const,
retentionDays: 1095, // 3 Jahre
description: 'Allgemeine Einstellungen',
legalBasis: 'Verjährungsfrist (BGB §195)',
},
];
for (const policy of specificPolicies) {
await prisma.auditRetentionPolicy.upsert({
where: {
resourceType_sensitivity: {
resourceType: policy.resourceType,
sensitivity: policy.sensitivity,
},
},
update: {
retentionDays: policy.retentionDays,
description: policy.description,
legalBasis: policy.legalBasis,
},
create: policy,
});
}
console.log('Audit retention policies created');
// ==================== CONSENT HASH FÜR BESTEHENDE KUNDEN ====================
const customersWithoutHash = await prisma.customer.findMany({
where: { consentHash: null },
select: { id: true },
});
for (const c of customersWithoutHash) {
await prisma.customer.update({
where: { id: c.id },
data: { consentHash: crypto.randomUUID() },
});
}
if (customersWithoutHash.length > 0) {
console.log(`ConsentHash für ${customersWithoutHash.length} Kunden generiert`);
}
console.log('Seeding completed!');
}