opencrm/backend/src/services/backup.service.ts

1187 lines
37 KiB
TypeScript

/**
* 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<T>(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<BackupInfo[]> {
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<BackupResult> {
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<RestoreResult> {
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<BackupResult> {
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<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'],
// 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 };
}
}