/** * Datenbank Backup Service * * Ermöglicht Backup und Restore der Datenbank und Uploads über die Web-Oberfläche. */ import { PrismaClient } from '@prisma/client'; 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'); // Stelle sicher, dass das Backup-Verzeichnis existiert if (!fs.existsSync(BACKUPS_DIR)) { fs.mkdirSync(BACKUPS_DIR, { recursive: true }); } export interface BackupInfo { name: string; timestamp: string; totalRecords: number; tables: { table: string; count: number }[]; sizeBytes: number; hasUploads: boolean; uploadSizeBytes: number; } export interface BackupResult { success: boolean; backupName?: string; error?: string; } export interface RestoreResult { success: boolean; restoredRecords?: number; restoredFiles?: number; error?: string; } // Hilfsfunktion: Datum-Strings zu Date-Objekten konvertieren function convertDates(obj: any): any { if (obj === null || obj === undefined) return obj; if (typeof obj === 'string') { if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(obj)) { return new Date(obj); } return obj; } if (Array.isArray(obj)) { return obj.map(convertDates); } if (typeof obj === 'object') { const result: any = {}; for (const [key, value] of Object.entries(obj)) { result[key] = convertDates(value); } return result; } return obj; } // Hilfsfunktion: JSON-Datei lesen function readJsonFile(filePath: string): T[] { if (!fs.existsSync(filePath)) { return []; } const content = fs.readFileSync(filePath, 'utf-8'); return JSON.parse(content); } // Hilfsfunktion: Ordnergröße berechnen (rekursiv) function getDirectorySize(dirPath: string): number { if (!fs.existsSync(dirPath)) return 0; let size = 0; const items = fs.readdirSync(dirPath); for (const item of items) { const itemPath = path.join(dirPath, item); const stats = fs.statSync(itemPath); if (stats.isFile()) { size += stats.size; } else if (stats.isDirectory()) { size += getDirectorySize(itemPath); } } return size; } // Hilfsfunktion: Ordner rekursiv kopieren function copyDirectory(src: string, dest: string): number { if (!fs.existsSync(src)) return 0; let fileCount = 0; fs.mkdirSync(dest, { recursive: true }); const items = fs.readdirSync(src); for (const item of items) { const srcPath = path.join(src, item); const destPath = path.join(dest, item); const stats = fs.statSync(srcPath); if (stats.isDirectory()) { fileCount += copyDirectory(srcPath, destPath); } else { fs.copyFileSync(srcPath, destPath); fileCount++; } } return fileCount; } // Hilfsfunktion: Ordner rekursiv löschen function deleteDirectory(dirPath: string): void { if (!fs.existsSync(dirPath)) return; const items = fs.readdirSync(dirPath); for (const item of items) { const itemPath = path.join(dirPath, item); const stats = fs.statSync(itemPath); if (stats.isDirectory()) { deleteDirectory(itemPath); } else { fs.unlinkSync(itemPath); } } fs.rmdirSync(dirPath); } /** * Liste aller verfügbaren Backups */ export async function listBackups(): Promise { const backups: BackupInfo[] = []; if (!fs.existsSync(BACKUPS_DIR)) { return backups; } const dirs = fs.readdirSync(BACKUPS_DIR) .filter(f => { const fullPath = path.join(BACKUPS_DIR, f); return fs.statSync(fullPath).isDirectory() && f !== '.gitkeep' && !f.startsWith('.'); }) .sort() .reverse(); for (const dir of dirs) { const backupDir = path.join(BACKUPS_DIR, dir); const infoPath = path.join(backupDir, '_backup-info.json'); const uploadsPath = path.join(backupDir, 'uploads'); if (fs.existsSync(infoPath)) { try { const info = JSON.parse(fs.readFileSync(infoPath, 'utf-8')); const hasUploads = fs.existsSync(uploadsPath); const uploadSize = hasUploads ? getDirectorySize(uploadsPath) : 0; backups.push({ name: dir, timestamp: info.timestamp, totalRecords: info.totalRecords, tables: info.tables, sizeBytes: getDirectorySize(backupDir), hasUploads, uploadSizeBytes: uploadSize, }); } catch { backups.push({ name: dir, timestamp: dir, totalRecords: 0, tables: [], sizeBytes: getDirectorySize(backupDir), hasUploads: fs.existsSync(uploadsPath), uploadSizeBytes: 0, }); } } } return backups; } /** * Neues Backup erstellen (inkl. Uploads) */ export async function createBackup(): Promise { try { const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); const backupDir = path.join(BACKUPS_DIR, timestamp); fs.mkdirSync(backupDir, { recursive: true }); // Tabellen in Abhängigkeitsreihenfolge const tables = [ { name: 'Permission', query: () => prisma.permission.findMany() }, { name: 'Role', query: () => prisma.role.findMany() }, { name: 'SalesPlatform', query: () => prisma.salesPlatform.findMany() }, { name: 'ContractCategory', query: () => prisma.contractCategory.findMany() }, { name: 'CancellationPeriod', query: () => prisma.cancellationPeriod.findMany() }, { name: 'ContractDuration', query: () => prisma.contractDuration.findMany() }, { name: 'AppSetting', query: () => prisma.appSetting.findMany() }, { name: 'EmailProviderConfig', query: () => prisma.emailProviderConfig.findMany() }, { name: 'Provider', query: () => prisma.provider.findMany() }, { name: 'RolePermission', query: () => prisma.rolePermission.findMany() }, { name: 'User', query: () => prisma.user.findMany() }, { name: 'Customer', query: () => prisma.customer.findMany() }, { name: 'Tariff', query: () => prisma.tariff.findMany() }, { name: 'UserRole', query: () => prisma.userRole.findMany() }, { name: 'CustomerRepresentative', query: () => prisma.customerRepresentative.findMany() }, { name: 'StressfreiEmail', query: () => prisma.stressfreiEmail.findMany() }, { name: 'Contract', query: () => prisma.contract.findMany() }, { name: 'Meter', query: () => prisma.meter.findMany() }, { name: 'CachedEmail', query: () => prisma.cachedEmail.findMany() }, { name: 'ContractTask', query: () => prisma.contractTask.findMany() }, { name: 'MeterReading', query: () => prisma.meterReading.findMany() }, { name: 'ContractTaskSubtask', query: () => prisma.contractTaskSubtask.findMany() }, { name: 'EnergyContractDetails', query: () => prisma.energyContractDetails.findMany() }, { name: 'InternetContractDetails', query: () => prisma.internetContractDetails.findMany() }, { name: 'MobileContractDetails', query: () => prisma.mobileContractDetails.findMany() }, { name: 'TvContractDetails', query: () => prisma.tvContractDetails.findMany() }, { name: 'CarInsuranceDetails', query: () => prisma.carInsuranceDetails.findMany() }, { name: 'PhoneNumber', query: () => prisma.phoneNumber.findMany() }, { name: 'SimCard', query: () => prisma.simCard.findMany() }, { name: 'Address', query: () => prisma.address.findMany() }, { name: 'BankCard', query: () => prisma.bankCard.findMany() }, { name: 'IdentityDocument', query: () => prisma.identityDocument.findMany() }, ]; let totalRecords = 0; const stats: { table: string; count: number }[] = []; for (const table of tables) { try { const data = await table.query(); const count = data.length; totalRecords += count; stats.push({ table: table.name, count }); const filePath = path.join(backupDir, `${table.name}.json`); fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); } catch { stats.push({ table: table.name, count: 0 }); } } // Uploads-Ordner kopieren (falls vorhanden) let uploadFileCount = 0; if (fs.existsSync(UPLOADS_DIR)) { const uploadsBackupDir = path.join(backupDir, 'uploads'); uploadFileCount = copyDirectory(UPLOADS_DIR, uploadsBackupDir); } // Backup-Info speichern const backupInfo = { timestamp: new Date().toISOString(), totalRecords, tables: stats, uploadFiles: uploadFileCount, }; fs.writeFileSync(path.join(backupDir, '_backup-info.json'), JSON.stringify(backupInfo, null, 2)); return { success: true, backupName: timestamp }; } catch (error: any) { return { success: false, error: error.message }; } } /** * Backup wiederherstellen (inkl. Uploads) */ export async function restoreBackup(backupName: string): Promise { const backupDir = path.join(BACKUPS_DIR, backupName); if (!fs.existsSync(backupDir)) { return { success: false, error: 'Backup nicht gefunden' }; } try { // Foreign Key Checks deaktivieren await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 0'); // WICHTIG: Alle Tabellen vor dem Restore leeren, damit keine alten Daten übrig bleiben console.log('[Restore] Lösche alle bestehenden Daten...'); // Detail-Tabellen await prisma.carInsuranceDetails.deleteMany({}); await prisma.tvContractDetails.deleteMany({}); await prisma.simCard.deleteMany({}); await prisma.mobileContractDetails.deleteMany({}); await prisma.phoneNumber.deleteMany({}); await prisma.internetContractDetails.deleteMany({}); await prisma.energyContractDetails.deleteMany({}); await prisma.meterReading.deleteMany({}); // E-Mail & Verträge await prisma.cachedEmail.deleteMany({}); await prisma.contractTaskSubtask.deleteMany({}); await prisma.contractTask.deleteMany({}); await prisma.contract.deleteMany({}); // Kunden-bezogene Daten await prisma.stressfreiEmail.deleteMany({}); await prisma.meter.deleteMany({}); await prisma.identityDocument.deleteMany({}); await prisma.bankCard.deleteMany({}); await prisma.address.deleteMany({}); await prisma.customerRepresentative.deleteMany({}); // Benutzer & Rollen await prisma.userRole.deleteMany({}); await prisma.user.deleteMany({}); await prisma.customer.deleteMany({}); // Stammdaten await prisma.tariff.deleteMany({}); await prisma.provider.deleteMany({}); await prisma.rolePermission.deleteMany({}); await prisma.role.deleteMany({}); await prisma.permission.deleteMany({}); await prisma.salesPlatform.deleteMany({}); await prisma.cancellationPeriod.deleteMany({}); await prisma.contractDuration.deleteMany({}); await prisma.contractCategory.deleteMany({}); await prisma.emailProviderConfig.deleteMany({}); await prisma.appSetting.deleteMany({}); console.log('[Restore] Alle Daten gelöscht, starte Wiederherstellung...'); // Restore-Reihenfolge const restoreOrder = [ { name: 'Permission', restore: async (data: any[]) => { for (const item of data) { await prisma.permission.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'Role', restore: async (data: any[]) => { for (const item of data) { await prisma.role.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'SalesPlatform', restore: async (data: any[]) => { for (const item of data) { await prisma.salesPlatform.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'ContractCategory', restore: async (data: any[]) => { for (const item of data) { await prisma.contractCategory.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'CancellationPeriod', restore: async (data: any[]) => { for (const item of data) { await prisma.cancellationPeriod.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'ContractDuration', restore: async (data: any[]) => { for (const item of data) { await prisma.contractDuration.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'AppSetting', restore: async (data: any[]) => { for (const item of data) { await prisma.appSetting.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'EmailProviderConfig', restore: async (data: any[]) => { for (const item of data) { await prisma.emailProviderConfig.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'Provider', restore: async (data: any[]) => { for (const item of data) { await prisma.provider.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'RolePermission', restore: async (data: any[]) => { for (const item of data) { await prisma.rolePermission.upsert({ where: { roleId_permissionId: { roleId: item.roleId, permissionId: item.permissionId } }, update: {}, create: convertDates(item), }); } }, }, { name: 'User', restore: async (data: any[]) => { for (const item of data) { await prisma.user.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'Customer', restore: async (data: any[]) => { for (const item of data) { await prisma.customer.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'Tariff', restore: async (data: any[]) => { for (const item of data) { await prisma.tariff.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'UserRole', restore: async (data: any[]) => { for (const item of data) { await prisma.userRole.upsert({ where: { userId_roleId: { userId: item.userId, roleId: item.roleId } }, update: {}, create: convertDates(item), }); } }, }, { name: 'CustomerRepresentative', restore: async (data: any[]) => { for (const item of data) { await prisma.customerRepresentative.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'StressfreiEmail', restore: async (data: any[]) => { for (const item of data) { await prisma.stressfreiEmail.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'Contract', restore: async (data: any[]) => { for (const item of data) { await prisma.contract.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'Meter', restore: async (data: any[]) => { for (const item of data) { await prisma.meter.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'CachedEmail', restore: async (data: any[]) => { for (const item of data) { await prisma.cachedEmail.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'ContractTask', restore: async (data: any[]) => { for (const item of data) { await prisma.contractTask.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'MeterReading', restore: async (data: any[]) => { for (const item of data) { await prisma.meterReading.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'ContractTaskSubtask', restore: async (data: any[]) => { for (const item of data) { await prisma.contractTaskSubtask.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'EnergyContractDetails', restore: async (data: any[]) => { for (const item of data) { await prisma.energyContractDetails.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'InternetContractDetails', restore: async (data: any[]) => { for (const item of data) { await prisma.internetContractDetails.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'MobileContractDetails', restore: async (data: any[]) => { for (const item of data) { await prisma.mobileContractDetails.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'TvContractDetails', restore: async (data: any[]) => { for (const item of data) { await prisma.tvContractDetails.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'CarInsuranceDetails', restore: async (data: any[]) => { for (const item of data) { await prisma.carInsuranceDetails.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'PhoneNumber', restore: async (data: any[]) => { for (const item of data) { await prisma.phoneNumber.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'SimCard', restore: async (data: any[]) => { for (const item of data) { await prisma.simCard.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'Address', restore: async (data: any[]) => { for (const item of data) { await prisma.address.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'BankCard', restore: async (data: any[]) => { for (const item of data) { await prisma.bankCard.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, { name: 'IdentityDocument', restore: async (data: any[]) => { for (const item of data) { await prisma.identityDocument.upsert({ where: { id: item.id }, update: convertDates(item), create: convertDates(item), }); } }, }, ]; let totalRestored = 0; for (const table of restoreOrder) { const filePath = path.join(backupDir, `${table.name}.json`); const data = readJsonFile(filePath); if (data.length === 0) continue; try { await table.restore(data); totalRestored += data.length; } catch { // Fehler bei einzelner Tabelle, weitermachen } } // Foreign Key Checks wieder aktivieren await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 1'); // Uploads wiederherstellen let restoredFiles = 0; const uploadsBackupDir = path.join(backupDir, 'uploads'); if (fs.existsSync(uploadsBackupDir)) { // Bestehenden Uploads-Ordner leeren (optional: könnte auch nur überschreiben) if (fs.existsSync(UPLOADS_DIR)) { deleteDirectory(UPLOADS_DIR); } restoredFiles = copyDirectory(uploadsBackupDir, UPLOADS_DIR); } return { success: true, restoredRecords: totalRestored, restoredFiles }; } catch (error: any) { try { await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 1'); } catch {} return { success: false, error: error.message }; } } /** * Backup löschen */ export async function deleteBackup(backupName: string): Promise<{ success: boolean; error?: string }> { const backupDir = path.join(BACKUPS_DIR, backupName); if (!fs.existsSync(backupDir)) { return { success: false, error: 'Backup nicht gefunden' }; } try { deleteDirectory(backupDir); return { success: true }; } catch (error: any) { return { success: false, error: error.message }; } } /** * Backup als ZIP-Datei erstellen und Stream zurückgeben */ export async function createBackupZip(backupName: string): Promise<{ stream: archiver.Archiver; filename: string } | { error: string }> { const backupDir = path.join(BACKUPS_DIR, backupName); if (!fs.existsSync(backupDir)) { return { error: 'Backup nicht gefunden' }; } const archive = archiver('zip', { zlib: { level: 9 } }); // Alle Dateien im Backup-Ordner hinzufügen archive.directory(backupDir, false); return { stream: archive, filename: `opencrm-backup-${backupName}.zip`, }; } /** * ZIP-Backup hochladen und extrahieren */ export async function uploadBackupZip(zipBuffer: Buffer): Promise { try { const zip = new AdmZip(zipBuffer); const entries = zip.getEntries(); // Prüfen ob gültiges Backup (muss _backup-info.json enthalten) const hasBackupInfo = entries.some(entry => entry.entryName === '_backup-info.json'); if (!hasBackupInfo) { return { success: false, error: 'Ungültiges Backup-Archiv: _backup-info.json fehlt' }; } // Backup-Info lesen um Namen zu bestimmen const infoEntry = entries.find(entry => entry.entryName === '_backup-info.json'); let backupName: string; try { const infoContent = infoEntry!.getData().toString('utf8'); const info = JSON.parse(infoContent); // Backup-Name aus Timestamp generieren backupName = new Date(info.timestamp).toISOString().replace(/[:.]/g, '-').slice(0, 19); } catch { // Fallback: Aktueller Timestamp backupName = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19) + '-uploaded'; } const backupDir = path.join(BACKUPS_DIR, backupName); // Falls Backup mit diesem Namen existiert, Suffix hinzufügen let finalBackupDir = backupDir; let suffix = 1; while (fs.existsSync(finalBackupDir)) { finalBackupDir = `${backupDir}-${suffix}`; suffix++; } const finalBackupName = path.basename(finalBackupDir); // ZIP extrahieren zip.extractAllTo(finalBackupDir, true); return { success: true, backupName: finalBackupName }; } catch (error: any) { return { success: false, error: error.message }; } } /** * Pfad zum Backup-Verzeichnis zurückgeben */ export function getBackupsDir(): string { return BACKUPS_DIR; } /** * Werkseinstellungen - Alle Daten löschen (außer Backups) * * Löscht alle Datenbankeinträge und Uploads, behält aber Backups. * Erstellt einen Admin-Benutzer und notwendige Stammdaten. */ export async function factoryReset(): Promise<{ success: boolean; error?: string }> { try { // Foreign Key Checks deaktivieren await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 0'); // Tabellen in korrekter Reihenfolge leeren (abhängige zuerst) // Detail-Tabellen await prisma.carInsuranceDetails.deleteMany({}); await prisma.tvContractDetails.deleteMany({}); await prisma.simCard.deleteMany({}); await prisma.mobileContractDetails.deleteMany({}); await prisma.phoneNumber.deleteMany({}); await prisma.internetContractDetails.deleteMany({}); await prisma.energyContractDetails.deleteMany({}); await prisma.meterReading.deleteMany({}); // E-Mail & Verträge await prisma.cachedEmail.deleteMany({}); await prisma.contractTaskSubtask.deleteMany({}); await prisma.contractTask.deleteMany({}); await prisma.contract.deleteMany({}); // Kunden-bezogene Daten await prisma.stressfreiEmail.deleteMany({}); await prisma.meter.deleteMany({}); await prisma.identityDocument.deleteMany({}); await prisma.bankCard.deleteMany({}); await prisma.address.deleteMany({}); await prisma.customerRepresentative.deleteMany({}); // Benutzer & Rollen (außer Admin) await prisma.userRole.deleteMany({}); await prisma.user.deleteMany({}); await prisma.customer.deleteMany({}); // Stammdaten await prisma.tariff.deleteMany({}); await prisma.provider.deleteMany({}); await prisma.rolePermission.deleteMany({}); await prisma.role.deleteMany({}); await prisma.permission.deleteMany({}); await prisma.salesPlatform.deleteMany({}); await prisma.cancellationPeriod.deleteMany({}); await prisma.contractDuration.deleteMany({}); await prisma.contractCategory.deleteMany({}); await prisma.emailProviderConfig.deleteMany({}); await prisma.appSetting.deleteMany({}); // Foreign Key Checks wieder aktivieren await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 1'); // Uploads-Verzeichnis leeren (aber nicht das Verzeichnis selbst) if (fs.existsSync(UPLOADS_DIR)) { const items = fs.readdirSync(UPLOADS_DIR); for (const item of items) { const itemPath = path.join(UPLOADS_DIR, item); const stats = fs.statSync(itemPath); if (stats.isDirectory()) { deleteDirectory(itemPath); } else { fs.unlinkSync(itemPath); } } } // Grundlegende Stammdaten neu anlegen (aus Seed) // Berechtigungen - muss mit seed.ts übereinstimmen! const resourcePermissions: Record = { // 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'], // Lookup-Tabellen (nur lesen) 'contract-categories': ['read'], 'cancellation-periods': ['read'], 'contract-durations': ['read'], // Einstellungen (nur lesen/ändern) settings: ['read', 'update'], // Spezial-Permissions developer: ['access'], emails: ['delete'], }; for (const [resource, actions] of Object.entries(resourcePermissions)) { for (const action of actions) { await prisma.permission.create({ data: { resource, action }, }); } } console.log('[FactoryReset] Berechtigungen erstellt'); // Admin-Rolle mit allen Berechtigungen (außer developer:access) const allPermissions = await prisma.permission.findMany(); const adminRole = await prisma.role.create({ data: { name: 'Admin', description: 'Voller Zugriff auf alle Funktionen', permissions: { create: allPermissions .filter(p => !(p.resource === 'developer' && p.action === 'access')) .map(p => ({ permissionId: p.id })), }, }, }); // Developer-Rolle - ALLE Berechtigungen inkl. developer:access await prisma.role.create({ data: { name: 'Developer', description: 'Voller Zugriff inkl. Entwickler-Tools', permissions: { create: allPermissions.map(p => ({ permissionId: p.id })), }, }, }); // Mitarbeiter-Rolle - customers, contracts + read-only auf Stammdaten const employeePermIds = allPermissions .filter(p => p.resource === 'customers' || p.resource === 'contracts' || (p.action === 'read' && ['platforms', 'providers', 'tariffs', 'contract-categories', 'cancellation-periods', 'contract-durations'].includes(p.resource)) ) .map(p => p.id); await prisma.role.create({ data: { name: 'Mitarbeiter', description: 'Kann Kunden und Verträge verwalten', permissions: { create: employeePermIds.map(id => ({ permissionId: id })), }, }, }); // Nur-Lesen Rolle const readOnlyResources = ['customers', 'contracts', 'platforms', 'providers', 'tariffs', 'contract-categories', 'cancellation-periods', 'contract-durations']; const readOnlyPermIds = allPermissions .filter(p => p.action === 'read' && readOnlyResources.includes(p.resource)) .map(p => p.id); await prisma.role.create({ data: { name: 'Mitarbeiter (Nur-Lesen)', description: 'Kann nur lesen, keine Änderungen', permissions: { create: readOnlyPermIds.map(id => ({ permissionId: id })), }, }, }); // Kunden-Rolle await prisma.role.create({ data: { name: 'Kunde', description: 'Kann nur eigene Daten lesen', permissions: { create: readOnlyPermIds.map(id => ({ permissionId: id })), }, }, }); console.log('[FactoryReset] Rollen erstellt'); // Standard Admin-Benutzer erstellen console.log('[FactoryReset] Erstelle Admin-Benutzer...'); const hashedPassword = await bcrypt.hash('admin', 10); console.log('[FactoryReset] Passwort gehasht, Admin-Rolle ID:', adminRole.id); const adminUser = await prisma.user.create({ data: { email: 'admin@admin.com', password: hashedPassword, firstName: 'Admin', lastName: 'User', roles: { create: [{ roleId: adminRole.id }], }, }, }); console.log('[FactoryReset] Admin-Benutzer erstellt mit ID:', adminUser.id); // Standard Kündigungsfristen (wie in seed.ts) 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 cp of cancellationPeriods) { await prisma.cancellationPeriod.create({ data: cp }); } console.log('[FactoryReset] Kündigungsfristen erstellt'); // Standard Vertragslaufzeiten (wie in seed.ts) 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 cd of contractDurations) { await prisma.contractDuration.create({ data: cd }); } console.log('[FactoryReset] Vertragslaufzeiten erstellt'); // Standard Vertragstypen (wie in seed.ts - WICHTIG: Diese müssen mit den Formularen übereinstimmen!) 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 cat of contractCategories) { await prisma.contractCategory.create({ data: cat }); } console.log('[FactoryReset] Vertragstypen erstellt'); // Standard Vertriebsplattformen (wie in seed.ts) const platforms = ['Moon Fachhandel', 'Verivox', 'Check24', 'Eigenvermittlung']; for (const name of platforms) { await prisma.salesPlatform.create({ data: { name, isActive: true } }); } console.log('[FactoryReset] Vertriebsplattformen erstellt'); // Standard Anbieter (wie in seed.ts) 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.create({ data: { ...provider, isActive: true } }); } console.log('[FactoryReset] Anbieter erstellt'); // Standard App-Einstellungen (wie in seed.ts) const appSettings = [ { key: 'deadlineCriticalDays', value: '14' }, { key: 'deadlineWarningDays', value: '42' }, { key: 'deadlineOkDays', value: '90' }, { key: 'companyName', value: 'OpenCRM' }, { key: 'defaultEmailDomain', value: 'stressfrei-wechseln.de' }, ]; for (const setting of appSettings) { await prisma.appSetting.create({ data: setting }); } console.log('[FactoryReset] App-Einstellungen erstellt'); return { success: true }; } catch (error: any) { try { await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 1'); } catch {} return { success: false, error: error.message }; } }