import { PrismaClient } from '@prisma/client'; import bcrypt from 'bcryptjs'; import crypto from 'crypto'; import { synchronisiereRechteUndRollen } from '../src/services/rollen-sync.service.js'; import { ROLLE_ADMIN, ROLLE_DSGVO } from '../src/config/rechte-katalog.js'; const prisma = new PrismaClient(); async function main() { console.log('Seeding database...'); // ==================== RECHTE UND ROLLEN ==================== // Katalog und Systemrollen kommen aus einer einzigen Definition // (src/config/rechte-katalog.ts), aufgeloest von rollen-sync.service.ts. // // Bis 09/2026 stand hier eine zweite, eigene Kopie - und sie wich ab: Die // DSGVO-Rolle bekam `audit:*` komplett, also auch `audit:admin`. Gerettet // hat das nur die Reihenfolge im Container-Start (sync-roles lief danach // und raeumte Ueberzaehliges weg); ein einzelnes `npm run db:seed` brachte // die Buendelung zurueck. Drei Beschreibungen desselben Sachverhalts sind // zwei zu viel. await synchronisiereRechteUndRollen(prisma); const adminRole = await prisma.role.findUniqueOrThrow({ where: { name: ROLLE_ADMIN } }); const gdprRole = await prisma.role.findUniqueOrThrow({ where: { name: ROLLE_DSGVO } }); // 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(); });