Beide Findings der Pentesterin waren berechtigt. Ihre Patches liessen sich nicht anwenden (Basis791711c, seitdem 42 Commits, sync-roles.ts kollidiert), und an zwei Stellen greifen sie zu kurz. R188 - ungueltige IDs im Pfad ----------------------------- Gemeldet: GET /api/users/:id gibt bei nicht-numerischer ID 500 statt 400. Ihr Fix schliesst nebenbei mehr, als sie beansprucht: parseInt('12abc') ergibt 12, also lieferte /api/users/12abc bisher Benutzer 12 aus. Es waren aber 181 ungepruefte Stellen in 19 Controllern, nicht eine. 181 Einzel-Guards waeren genau der Fehler aus R186-01 gewesen - drei Filterlisten, die dasselbe bedeuten sollten und auseinanderliefen. Stattdessen router.param(), an einer Stelle fuer alle 33 Router registriert, ueber einen mounte()-Helfer, der Pruefung und Einhaengen zusammenbindet. Antwort ist 404, nicht 400: Ein Pfadsegment, das keine ID sein kann, benennt keine Ressource. Der bestehende Praezedenzfall in provider.controller.ts (Pentest Mai 2026) hatte es genauso entschieden. Dabei eine aeltere Heuristik abgeloest (Pentest Runde 7). Ihr eigener Kommentar nannte den Grund fuer sie - "app.param() greift nicht auf in Sub-Router gemounteten Routes" - und genau das loest mounte(). Sie war zu eng (/users/abc ging durch und endete als 500) und zu weit (ein Einstellungs-Schluessel 12abc unter :key wurde geblockt, obwohl das keine ID ist), und sie antwortete 400, wo jetzt 404 steht. R189-01 - DSGVO-Rechte ohne Traeger ------------------------------------ Gemeldet: gdpr:* und audit:read/export haengen an DSGVO und Developer, die Admin-Rolle hat sie nicht, und nach einem frischen Seed war DSGVO keinem Konto zugewiesen. Auskunft nach Art. 15 und Loeschung nach Art. 17 konnte niemand ausfuehren. Seed weist admin@admin.com jetzt zusaetzlich die DSGVO-Rolle zu; die Admin-Rolle selbst bleibt ohne diese Rechte, die Trennung aus R186 bleibt also erhalten. Label ehrlich gemacht. Beim Pruefen ihres Patches ein eigener Fund: seed.ts vergab an die DSGVO-Rolle weiterhin audit:* komplett, inklusive audit:admin - die Buendelung, diefc6f39eaufgeloest hat. Ich hatte damals zwei Listen gefunden und die dritte uebersehen. Gerettet hat es nur die Reihenfolge im Containerstart; ein einzelnes `npm run db:seed` brachte sie zurueck. Der Seed hilft nur bei Neuinstallation (update: {}). Deshalb zusaetzlich eine Wache beim Start: Gibt es fuer gdpr:export, gdpr:delete oder audit:read kein aktives Konto, steht das mit Handlungsanweisung im Log - Erkennung der ABWESENHEIT einer Faehigkeit, wie beim Heartbeat. Bewusst nur melden, nicht automatisch vergeben. Getestet ueber HTTP gegen eine Wegwerf-Instanz: alle ID-Varianten quer ueber sechs Controller, Nicht-ID-Parameter unveraendert, frischer Seed, Wache mit und ohne vergebene Rechte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
583 lines
20 KiB
TypeScript
583 lines
20 KiB
TypeScript
import { PrismaClient } from '@prisma/client';
|
||
import bcrypt from 'bcryptjs';
|
||
import crypto from 'crypto';
|
||
|
||
const prisma = new PrismaClient();
|
||
|
||
async function main() {
|
||
console.log('Seeding database...');
|
||
|
||
// ==================== PERMISSIONS ====================
|
||
// Ressourcen mit ihren erlaubten Aktionen
|
||
const resourcePermissions: Record<string, string[]> = {
|
||
// Haupt-Ressourcen (CRUD)
|
||
customers: ['create', 'read', 'update', 'delete'],
|
||
contracts: ['create', 'read', 'update', 'delete'],
|
||
users: ['create', 'read', 'update', 'delete'],
|
||
platforms: ['create', 'read', 'update', 'delete'],
|
||
providers: ['create', 'read', 'update', 'delete'],
|
||
tariffs: ['create', 'read', 'update', 'delete'],
|
||
// Konfiguration (CRUD)
|
||
'cancellation-periods': ['create', 'read', 'update', 'delete'],
|
||
'contract-durations': ['create', 'read', 'update', 'delete'],
|
||
'contract-categories': ['create', 'read', 'update', 'delete'],
|
||
'email-providers': ['create', 'read', 'update', 'delete'],
|
||
// Einstellungen (nur lesen/ändern)
|
||
settings: ['read', 'update'],
|
||
// Spezial-Permissions
|
||
developer: ['access'],
|
||
emails: ['delete'],
|
||
// DSGVO & Audit
|
||
audit: ['read', 'export', 'admin'],
|
||
gdpr: ['export', 'delete', 'admin'],
|
||
};
|
||
|
||
const permissions: { resource: string; action: string }[] = [];
|
||
for (const [resource, actions] of Object.entries(resourcePermissions)) {
|
||
for (const action of actions) {
|
||
permissions.push({ resource, action });
|
||
}
|
||
}
|
||
|
||
for (const perm of permissions) {
|
||
await prisma.permission.upsert({
|
||
where: { resource_action: perm },
|
||
update: {},
|
||
create: perm,
|
||
});
|
||
}
|
||
|
||
console.log(`Permissions created (${permissions.length} total)`);
|
||
|
||
// Get all permissions
|
||
const allPermissions = await prisma.permission.findMany();
|
||
const customerReadPerm = allPermissions.find(
|
||
(p) => p.resource === 'customers' && p.action === 'read'
|
||
);
|
||
const contractReadPerm = allPermissions.find(
|
||
(p) => p.resource === 'contracts' && p.action === 'read'
|
||
);
|
||
const platformReadPerm = allPermissions.find(
|
||
(p) => p.resource === 'platforms' && p.action === 'read'
|
||
);
|
||
const providerReadPerm = allPermissions.find(
|
||
(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 and audit/gdpr (controlled separately via checkboxes)
|
||
const adminPermissions = allPermissions.filter(
|
||
(p) =>
|
||
!(p.resource === 'developer' && p.action === 'access') &&
|
||
p.resource !== 'audit' &&
|
||
p.resource !== 'gdpr'
|
||
);
|
||
const adminRole = await prisma.role.upsert({
|
||
where: { name: 'Admin' },
|
||
update: {},
|
||
create: {
|
||
name: 'Admin',
|
||
description: 'Voller Zugriff auf alle Fachfunktionen (ohne Audit & Datenschutz – dafür die separaten Rollen DSGVO und Audit-Betrieb)',
|
||
permissions: {
|
||
create: adminPermissions.map((p) => ({ permissionId: p.id })),
|
||
},
|
||
},
|
||
});
|
||
await syncRolePermissions(adminRole.id, adminPermissions.map((p) => p.id));
|
||
|
||
// Developer - ALL permissions (developer:access + alles andere)
|
||
const developerPermissions = allPermissions;
|
||
const developerRole = await prisma.role.upsert({
|
||
where: { name: 'Developer' },
|
||
update: {},
|
||
create: {
|
||
name: 'Developer',
|
||
description: 'Voller Zugriff inkl. Entwickler-Tools',
|
||
permissions: {
|
||
create: developerPermissions.map((p) => ({ permissionId: p.id })),
|
||
},
|
||
},
|
||
});
|
||
await syncRolePermissions(developerRole.id, developerPermissions.map((p) => p.id));
|
||
|
||
// DSGVO - Datenschutz-Verwaltung plus LESENDER Zugriff aufs Audit-Protokoll.
|
||
//
|
||
// Bewusst OHNE `audit:admin`: Wer das Protokoll beaufsichtigt, darf seine
|
||
// eigene Beweisgrundlage nicht ersetzen koennen (Pentest R186). Die
|
||
// eingreifenden Rechte liegen in der Rolle `Audit-Betrieb`.
|
||
//
|
||
// Diese Liste stand hier bis 09/2026 noch auf `audit:*` komplett und war
|
||
// damit die dritte Stelle, die denselben Rechtesatz beschrieb - neben
|
||
// `sync-roles.ts` und dem Notfallpfad in `user.service.ts`. Gerettet hat es
|
||
// nur die Reihenfolge im Container-Start (sync-roles laeuft danach und
|
||
// raeumt Ueberzaehliges weg); ein einzelnes `npm run db:seed` brachte die
|
||
// Buendelung zurueck.
|
||
const gdprPermissions = allPermissions.filter(
|
||
(p) =>
|
||
p.resource === 'gdpr' ||
|
||
(p.resource === 'audit' && (p.action === 'read' || p.action === 'export'))
|
||
);
|
||
const gdprRole = await prisma.role.upsert({
|
||
where: { name: 'DSGVO' },
|
||
update: {},
|
||
create: {
|
||
name: 'DSGVO',
|
||
description: 'DSGVO-Zugriff: Audit-Logs lesen 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
|
||
.filter(
|
||
(p) =>
|
||
p.resource === 'customers' ||
|
||
p.resource === 'contracts' ||
|
||
// Read-only Zugriff auf Stammdaten und Konfiguration
|
||
(p.action === 'read' && [
|
||
'platforms',
|
||
'providers',
|
||
'tariffs',
|
||
'cancellation-periods',
|
||
'contract-durations',
|
||
'contract-categories',
|
||
].includes(p.resource))
|
||
)
|
||
.map((p) => p.id);
|
||
|
||
const employeeRole = await prisma.role.upsert({
|
||
where: { name: 'Mitarbeiter' },
|
||
update: {},
|
||
create: {
|
||
name: 'Mitarbeiter',
|
||
description: 'Kann Kunden und Verträge verwalten',
|
||
permissions: {
|
||
create: employeePermIds.map((id) => ({ permissionId: id })),
|
||
},
|
||
},
|
||
});
|
||
await syncRolePermissions(employeeRole.id, employeePermIds);
|
||
|
||
// Read-only employee - read access to main entities and lookup tables
|
||
const readOnlyResources = [
|
||
'customers',
|
||
'contracts',
|
||
'platforms',
|
||
'providers',
|
||
'tariffs',
|
||
'cancellation-periods',
|
||
'contract-durations',
|
||
'contract-categories',
|
||
];
|
||
const readOnlyPermIds = allPermissions
|
||
.filter((p) => p.action === 'read' && readOnlyResources.includes(p.resource))
|
||
.map((p) => p.id);
|
||
|
||
const readOnlyRole = await prisma.role.upsert({
|
||
where: { name: 'Mitarbeiter (Nur-Lesen)' },
|
||
update: {},
|
||
create: {
|
||
name: 'Mitarbeiter (Nur-Lesen)',
|
||
description: 'Kann nur lesen, keine Änderungen',
|
||
permissions: {
|
||
create: readOnlyPermIds.map((id) => ({ permissionId: id })),
|
||
},
|
||
},
|
||
});
|
||
await syncRolePermissions(readOnlyRole.id, readOnlyPermIds);
|
||
|
||
// Customer role - read own data only (handled in middleware)
|
||
const customerRole = await prisma.role.upsert({
|
||
where: { name: 'Kunde' },
|
||
update: {},
|
||
create: {
|
||
name: 'Kunde',
|
||
description: 'Kann nur eigene Daten lesen',
|
||
permissions: {
|
||
create: readOnlyPermIds.map((id) => ({ permissionId: id })),
|
||
},
|
||
},
|
||
});
|
||
await syncRolePermissions(customerRole.id, readOnlyPermIds);
|
||
|
||
console.log('Roles created');
|
||
|
||
// Admin-User anlegen. Standard-Passwort darf NIEMALS in der Source-Repo
|
||
// landen (Pentest Runde 12: "admin" verletzt die eigene 12-Zeichen-
|
||
// Komplexitätspolicy). Stattdessen:
|
||
// - SEED_ADMIN_PASSWORD-ENV → wird verwendet (z.B. via docker-compose env)
|
||
// - sonst → zufälliges 16-Zeichen-Passwort, wird ein einziges Mal beim
|
||
// Seed in stdout ausgegeben. Wer das Log nicht sieht, muss
|
||
// Passwort-vergessen-Flow nutzen.
|
||
// Hash-Cost: 12 (OWASP 2026), nicht mehr 10.
|
||
function generateInitialPassword(): string {
|
||
const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
|
||
const lower = 'abcdefghijkmnopqrstuvwxyz';
|
||
const digits = '23456789';
|
||
const special = '!@#$%&*+=?';
|
||
const all = upper + lower + digits + special;
|
||
// Kryptografisch sichere Auswahl – Math.random() ist vorhersagbar
|
||
// und reicht für ein Initial-Admin-Passwort nicht (Pentest 2026-05-20).
|
||
const pick = (s: string) => s[crypto.randomInt(0, s.length)];
|
||
// mind. einen aus jeder Klasse + Rest zufällig
|
||
const chars = [pick(upper), pick(lower), pick(digits), pick(special)];
|
||
// 28 Zeichen → Komplexität + komfortable Marge über dem 25-Zeichen-
|
||
// Mitarbeiter-Schwellwert (Pentest Runde 13).
|
||
for (let i = chars.length; i < 28; i++) chars.push(pick(all));
|
||
// Fisher-Yates Shuffle mit kryptografisch starkem Random.
|
||
for (let i = chars.length - 1; i > 0; i--) {
|
||
const j = crypto.randomInt(0, i + 1);
|
||
[chars[i], chars[j]] = [chars[j], chars[i]];
|
||
}
|
||
return chars.join('');
|
||
}
|
||
|
||
const envPassword = process.env.SEED_ADMIN_PASSWORD;
|
||
const adminPlainPassword = envPassword && envPassword.length >= 25
|
||
? envPassword
|
||
: generateInitialPassword();
|
||
const hashedPassword = await bcrypt.hash(adminPlainPassword, 12);
|
||
|
||
const adminUser = await prisma.user.upsert({
|
||
where: { email: 'admin@admin.com' },
|
||
update: {},
|
||
create: {
|
||
email: 'admin@admin.com',
|
||
password: hashedPassword,
|
||
firstName: 'Admin',
|
||
lastName: 'User',
|
||
// Zusaetzlich die DSGVO-Rolle (Pentest R189-01).
|
||
//
|
||
// Ohne sie kann nach einem frischen Seed NIEMAND eine Auskunft nach
|
||
// Art. 15 oder eine Loeschung nach Art. 17 ausfuehren - die Rechte
|
||
// haengen an DSGVO und Developer, und beide waren keinem Konto
|
||
// zugewiesen. Ein Ausfall mit Fristwirkung, ausgeloest durch nichts
|
||
// weiter als eine Neuinstallation.
|
||
//
|
||
// Die Trennung bleibt: Die Admin-ROLLE bekommt diese Rechte weiterhin
|
||
// nicht. Nur dieses eine Bootstrap-Konto traegt beide, damit ueberhaupt
|
||
// jemand handlungsfaehig ist.
|
||
roles: {
|
||
create: [{ roleId: adminRole.id }, { roleId: gdprRole.id }],
|
||
},
|
||
},
|
||
});
|
||
|
||
console.log('========================================================');
|
||
console.log(' Admin-User: admin@admin.com');
|
||
if (envPassword && envPassword.length >= 25) {
|
||
console.log(' Passwort: aus SEED_ADMIN_PASSWORD');
|
||
} else {
|
||
if (envPassword && envPassword.length < 25) {
|
||
console.log(' ⚠️ SEED_ADMIN_PASSWORD < 25 Zeichen, wird ignoriert!');
|
||
}
|
||
console.log(` Initial-Passwort: ${adminPlainPassword}`);
|
||
console.log(' ⚠️ Dieses Passwort wird hier EINMAL ausgegeben!');
|
||
console.log(' Bitte sofort nach dem ersten Login ändern.');
|
||
}
|
||
console.log('========================================================');
|
||
|
||
// Create some sales platforms
|
||
const platforms = ['Moon Fachhandel', 'Verivox', 'Check24', 'Eigenvermittlung'];
|
||
for (const name of platforms) {
|
||
await prisma.salesPlatform.upsert({
|
||
where: { name },
|
||
update: {},
|
||
create: { name, isActive: true },
|
||
});
|
||
}
|
||
|
||
console.log('Sales platforms created');
|
||
|
||
// ==================== STANDARD PROVIDERS ====================
|
||
const providers = [
|
||
{
|
||
name: 'Vodafone',
|
||
portalUrl: 'https://www.vodafone.de/meinvodafone/account/login',
|
||
usernameFieldName: 'username',
|
||
passwordFieldName: 'password',
|
||
},
|
||
{
|
||
name: 'Klarmobil',
|
||
portalUrl: 'https://www.klarmobil.de/login',
|
||
usernameFieldName: 'username',
|
||
passwordFieldName: 'password',
|
||
},
|
||
{
|
||
name: 'Otelo',
|
||
portalUrl: 'https://www.otelo.de/mein-otelo/login',
|
||
usernameFieldName: 'username',
|
||
passwordFieldName: 'password',
|
||
},
|
||
{
|
||
name: 'Congstar',
|
||
portalUrl: 'https://www.congstar.de/login/',
|
||
usernameFieldName: 'username',
|
||
passwordFieldName: 'password',
|
||
},
|
||
{
|
||
name: 'Telekom',
|
||
portalUrl: 'https://www.telekom.de/kundencenter/startseite',
|
||
usernameFieldName: 'username',
|
||
passwordFieldName: 'password',
|
||
},
|
||
{
|
||
name: 'O2',
|
||
portalUrl: 'https://www.o2online.de/ecare/selfcare',
|
||
usernameFieldName: 'username',
|
||
passwordFieldName: 'password',
|
||
},
|
||
{
|
||
name: '1&1',
|
||
portalUrl: 'https://control-center.1und1.de/',
|
||
usernameFieldName: 'username',
|
||
passwordFieldName: 'password',
|
||
},
|
||
];
|
||
|
||
for (const provider of providers) {
|
||
await prisma.provider.upsert({
|
||
where: { name: provider.name },
|
||
update: {
|
||
portalUrl: provider.portalUrl,
|
||
usernameFieldName: provider.usernameFieldName,
|
||
passwordFieldName: provider.passwordFieldName,
|
||
},
|
||
create: { ...provider, isActive: true },
|
||
});
|
||
}
|
||
|
||
console.log('Providers created');
|
||
|
||
// Create contract categories (matching existing enum values)
|
||
const contractCategories = [
|
||
{ code: 'ELECTRICITY', name: 'Strom', icon: 'Zap', color: '#FFC107', sortOrder: 1 },
|
||
{ code: 'GAS', name: 'Gas', icon: 'Flame', color: '#FF5722', sortOrder: 2 },
|
||
{ code: 'DSL', name: 'DSL', icon: 'Wifi', color: '#2196F3', sortOrder: 3 },
|
||
{ code: 'FIBER', name: 'Glasfaser', icon: 'Cable', color: '#9C27B0', sortOrder: 4 },
|
||
{ code: 'CABLE', name: 'Kabel Internet (Coax)', icon: 'Cable', color: '#00BCD4', sortOrder: 5 },
|
||
{ code: 'MOBILE', name: 'Mobilfunk', icon: 'Smartphone', color: '#4CAF50', sortOrder: 6 },
|
||
{ code: 'TV', name: 'TV', icon: 'Tv', color: '#E91E63', sortOrder: 7 },
|
||
{ code: 'CAR_INSURANCE', name: 'KFZ-Versicherung', icon: 'Car', color: '#607D8B', sortOrder: 8 },
|
||
];
|
||
|
||
for (const category of contractCategories) {
|
||
await prisma.contractCategory.upsert({
|
||
where: { code: category.code },
|
||
update: { name: category.name, icon: category.icon, color: category.color, sortOrder: category.sortOrder },
|
||
create: category,
|
||
});
|
||
}
|
||
|
||
console.log('Contract categories created');
|
||
|
||
// ==================== CANCELLATION PERIODS ====================
|
||
const cancellationPeriods = [
|
||
{ code: '14D', description: '14 Tage' },
|
||
{ code: '1M', description: '1 Monat' },
|
||
{ code: '2M', description: '2 Monate' },
|
||
{ code: '3M', description: '3 Monate' },
|
||
{ code: '6M', description: '6 Monate' },
|
||
{ code: '12M', description: '12 Monate' },
|
||
{ code: '1W', description: '1 Woche' },
|
||
{ code: '2W', description: '2 Wochen' },
|
||
{ code: '4W', description: '4 Wochen' },
|
||
{ code: '6W', description: '6 Wochen' },
|
||
];
|
||
|
||
for (const period of cancellationPeriods) {
|
||
await prisma.cancellationPeriod.upsert({
|
||
where: { code: period.code },
|
||
update: { description: period.description },
|
||
create: period,
|
||
});
|
||
}
|
||
|
||
console.log('Cancellation periods created');
|
||
|
||
// ==================== CONTRACT DURATIONS ====================
|
||
const contractDurations = [
|
||
{ code: '1M', description: '1 Monat' },
|
||
{ code: '3M', description: '3 Monate' },
|
||
{ code: '6M', description: '6 Monate' },
|
||
{ code: '12M', description: '12 Monate' },
|
||
{ code: '24M', description: '24 Monate' },
|
||
{ code: '36M', description: '36 Monate' },
|
||
{ code: '1J', description: '1 Jahr' },
|
||
{ code: '2J', description: '2 Jahre' },
|
||
{ code: '3J', description: '3 Jahre' },
|
||
{ code: '4J', description: '4 Jahre' },
|
||
{ code: '5J', description: '5 Jahre' },
|
||
{ code: 'UNBEFRISTET', description: 'Unbefristet' },
|
||
];
|
||
|
||
for (const duration of contractDurations) {
|
||
await prisma.contractDuration.upsert({
|
||
where: { code: duration.code },
|
||
update: { description: duration.description },
|
||
create: duration,
|
||
});
|
||
}
|
||
|
||
console.log('Contract durations created');
|
||
|
||
// ==================== APP SETTINGS ====================
|
||
const appSettings = [
|
||
// Cockpit-Einstellungen (Fristen-Ampel)
|
||
{ key: 'deadlineCriticalDays', value: '14' }, // Rot: <= 14 Tage
|
||
{ key: 'deadlineWarningDays', value: '42' }, // Gelb: <= 42 Tage
|
||
{ key: 'deadlineOkDays', value: '90' }, // Grün: <= 90 Tage
|
||
// Allgemeine Einstellungen
|
||
{ key: 'companyName', value: 'OpenCRM' },
|
||
{ key: 'defaultEmailDomain', value: 'stressfrei-wechseln.de' },
|
||
];
|
||
|
||
for (const setting of appSettings) {
|
||
await prisma.appSetting.upsert({
|
||
where: { key: setting.key },
|
||
update: {}, // Bestehende Werte nicht überschreiben
|
||
create: setting,
|
||
});
|
||
}
|
||
|
||
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)',
|
||
},
|
||
{
|
||
// Stiller Token-Refresh (Routine). Ohne eigene Regel fiele diese
|
||
// Kombination in die Auffangregel `*` mit 3650 Tagen – das Rauschen
|
||
// wäre dann länger aufbewahrt als echte Logins (730 Tage).
|
||
resourceType: 'Authentication',
|
||
sensitivity: 'LOW' as const,
|
||
retentionDays: 90,
|
||
description: 'Routine-Auth (stiller Token-Refresh)',
|
||
legalBasis: 'Betriebsnotwendigkeit / Datenminimierung (DSGVO Art. 5)',
|
||
},
|
||
];
|
||
|
||
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!');
|
||
}
|
||
|
||
main()
|
||
.catch((e) => {
|
||
console.error(e);
|
||
process.exit(1);
|
||
})
|
||
.finally(async () => {
|
||
await prisma.$disconnect();
|
||
});
|