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:
@@ -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`);
|
||||
@@ -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
@@ -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!');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user