81f0e89058
Nach der ersten Runde habe ich parallel 3 Audit-Agents auf die Codebase angesetzt. Die fanden noch eine Menge: Zip-Slip, Mass Assignment inkl. Privilege Escalation, 13 weitere IDOR-Stellen, 2x Path-Traversal. Alles gefixt. Details + Angriffsvektoren in docs/SECURITY-REVIEW.md. 🔴 KRITISCH gefixt: 1. Zip-Slip im Backup-Upload: extractAllTo() entpackte bösartige ZIPs ohne Pfad-Validierung. Ein Angreifer mit Admin-Zugang hätte mit einem ZIP mit Entries wie ../../etc/crontab das ganze Filesystem überschreiben können. Jetzt wird jeder ZIP-Entry einzeln validiert (path.resolve, starts-with-Check). Absolute Pfade + Null-Bytes werden abgelehnt. 2. Mass Assignment bei Customer/User Controllers: - updateCustomer/createCustomer: req.body ging komplett an Prisma. Angreifer konnte portalPasswordHash, portalPasswordResetToken, consentHash, customerNumber direkt setzen. - updateUser/createUser: roleIds und isActive waren übernehmbar. **Privilege Escalation**: normaler Mitarbeiter konnte sich Admin-Rechte durch PUT /users/:id mit {"roleIds":[1]} geben, oder andere User deaktivieren. Fix: Neue Whitelist-Helper pickCustomerCreate/Update, pickUserCreate/Update in utils/sanitize.ts. Nur erlaubte Felder werden durchgelassen. 3. IDOR bei 13 weiteren Endpoints (neben denen aus Runde 1): - GET /meters/:meterId/readings - GET /emails/:emailId/attachments/:filename - GET /emails/:emailId/attachments (Liste) - GET /customers/:customerId/emails - GET /contracts/:contractId/emails - GET /emails/:id (einzelne Email) - GET /stressfrei-emails/:id (leakte emailPasswordEncrypted) - weitere… Fix: accessControl.ts ausgebaut um canAccessAddress, canAccessBankCard, canAccessIdentityDocument, canAccessMeter, canAccessStressfreiEmail, canAccessCachedEmail. In allen betroffenen Endpoints angewendet. 🟡 WICHTIG gefixt: 4. Path-Traversal bei Backup-Name (GET /settings/backup/:name/*): req.params.name wurde ohne Filter in path.join. Neuer isValidBackupName() erlaubt nur [A-Za-z0-9_-]+ ohne "..". 5. Path-Traversal bei GDPR-Proof-Download: proofDocument-Pfad aus DB wurde ohne Validation gejoined. Jetzt path.resolve + starts-with-uploads-Check. Neue/erweiterte Files: - backend/src/utils/accessControl.ts - 6 neue can-Access-Helper - backend/src/utils/sanitize.ts - 4 neue Whitelist-pick-Helper - docs/SECURITY-REVIEW.md - Runde 2 dokumentiert Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1384 lines
44 KiB
TypeScript
1384 lines
44 KiB
TypeScript
/**
|
||
* Datenbank Backup Service
|
||
*
|
||
* Ermöglicht Backup und Restore der Datenbank und Uploads über die Web-Oberfläche.
|
||
*/
|
||
|
||
import prisma from '../lib/prisma.js';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import archiver from 'archiver';
|
||
import AdmZip from 'adm-zip';
|
||
import bcrypt from 'bcryptjs';
|
||
|
||
// 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: 'Invoice', query: () => prisma.invoice.findMany() },
|
||
{ name: 'ContractHistoryEntry', query: () => prisma.contractHistoryEntry.findMany() },
|
||
{ name: 'Address', query: () => prisma.address.findMany() },
|
||
{ name: 'BankCard', query: () => prisma.bankCard.findMany() },
|
||
{ name: 'IdentityDocument', query: () => prisma.identityDocument.findMany() },
|
||
// Neue Tabellen
|
||
{ name: 'PdfTemplate', query: () => prisma.pdfTemplate.findMany() },
|
||
{ name: 'ContractMeter', query: () => prisma.contractMeter.findMany() },
|
||
{ name: 'ContractDocument', query: () => prisma.contractDocument.findMany() },
|
||
{ name: 'RepresentativeAuthorization', query: () => prisma.representativeAuthorization.findMany() },
|
||
{ name: 'CustomerConsent', query: () => prisma.customerConsent.findMany() },
|
||
{ name: 'DataDeletionRequest', query: () => prisma.dataDeletionRequest.findMany() },
|
||
{ name: 'EmailLog', query: () => prisma.emailLog.findMany() },
|
||
{ name: 'AuditRetentionPolicy', query: () => prisma.auditRetentionPolicy.findMany() },
|
||
{ name: 'AuditLog', query: () => prisma.auditLog.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...');
|
||
|
||
// Logs & Audit zuerst (hängen an allem)
|
||
await prisma.auditLog.deleteMany({});
|
||
await prisma.emailLog.deleteMany({});
|
||
|
||
// 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.invoice.deleteMany({});
|
||
await prisma.energyContractDetails.deleteMany({});
|
||
await prisma.meterReading.deleteMany({});
|
||
await prisma.contractHistoryEntry.deleteMany({});
|
||
|
||
// Neue Contract-bezogene Tabellen
|
||
await prisma.contractDocument.deleteMany({});
|
||
await prisma.contractMeter.deleteMany({});
|
||
|
||
// E-Mail & Verträge
|
||
await prisma.cachedEmail.deleteMany({});
|
||
await prisma.contractTaskSubtask.deleteMany({});
|
||
await prisma.contractTask.deleteMany({});
|
||
await prisma.contract.deleteMany({});
|
||
|
||
// DSGVO + Vollmachten (abhängig von Customer)
|
||
await prisma.representativeAuthorization.deleteMany({});
|
||
await prisma.customerConsent.deleteMany({});
|
||
await prisma.dataDeletionRequest.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 & Kataloge
|
||
await prisma.pdfTemplate.deleteMany({});
|
||
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({});
|
||
await prisma.auditRetentionPolicy.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: 'Invoice',
|
||
restore: async (data: any[]) => {
|
||
for (const item of data) {
|
||
await prisma.invoice.upsert({
|
||
where: { id: item.id },
|
||
update: convertDates(item),
|
||
create: convertDates(item),
|
||
});
|
||
}
|
||
},
|
||
},
|
||
{
|
||
name: 'ContractHistoryEntry',
|
||
restore: async (data: any[]) => {
|
||
for (const item of data) {
|
||
await prisma.contractHistoryEntry.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),
|
||
});
|
||
}
|
||
},
|
||
},
|
||
// Neue Tabellen
|
||
{
|
||
name: 'PdfTemplate',
|
||
restore: async (data: any[]) => {
|
||
for (const item of data) {
|
||
await prisma.pdfTemplate.upsert({
|
||
where: { id: item.id },
|
||
update: convertDates(item),
|
||
create: convertDates(item),
|
||
});
|
||
}
|
||
},
|
||
},
|
||
{
|
||
name: 'ContractMeter',
|
||
restore: async (data: any[]) => {
|
||
for (const item of data) {
|
||
await prisma.contractMeter.upsert({
|
||
where: { id: item.id },
|
||
update: convertDates(item),
|
||
create: convertDates(item),
|
||
});
|
||
}
|
||
},
|
||
},
|
||
{
|
||
name: 'ContractDocument',
|
||
restore: async (data: any[]) => {
|
||
for (const item of data) {
|
||
await prisma.contractDocument.upsert({
|
||
where: { id: item.id },
|
||
update: convertDates(item),
|
||
create: convertDates(item),
|
||
});
|
||
}
|
||
},
|
||
},
|
||
{
|
||
name: 'RepresentativeAuthorization',
|
||
restore: async (data: any[]) => {
|
||
for (const item of data) {
|
||
await prisma.representativeAuthorization.upsert({
|
||
where: { id: item.id },
|
||
update: convertDates(item),
|
||
create: convertDates(item),
|
||
});
|
||
}
|
||
},
|
||
},
|
||
{
|
||
name: 'CustomerConsent',
|
||
restore: async (data: any[]) => {
|
||
for (const item of data) {
|
||
await prisma.customerConsent.upsert({
|
||
where: { id: item.id },
|
||
update: convertDates(item),
|
||
create: convertDates(item),
|
||
});
|
||
}
|
||
},
|
||
},
|
||
{
|
||
name: 'DataDeletionRequest',
|
||
restore: async (data: any[]) => {
|
||
for (const item of data) {
|
||
await prisma.dataDeletionRequest.upsert({
|
||
where: { id: item.id },
|
||
update: convertDates(item),
|
||
create: convertDates(item),
|
||
});
|
||
}
|
||
},
|
||
},
|
||
{
|
||
name: 'EmailLog',
|
||
restore: async (data: any[]) => {
|
||
for (const item of data) {
|
||
await prisma.emailLog.upsert({
|
||
where: { id: item.id },
|
||
update: convertDates(item),
|
||
create: convertDates(item),
|
||
});
|
||
}
|
||
},
|
||
},
|
||
{
|
||
name: 'AuditRetentionPolicy',
|
||
restore: async (data: any[]) => {
|
||
for (const item of data) {
|
||
await prisma.auditRetentionPolicy.upsert({
|
||
where: { id: item.id },
|
||
update: convertDates(item),
|
||
create: convertDates(item),
|
||
});
|
||
}
|
||
},
|
||
},
|
||
{
|
||
name: 'AuditLog',
|
||
restore: async (data: any[]) => {
|
||
for (const item of data) {
|
||
await prisma.auditLog.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 entpacken – mit Schutz gegen Zip-Slip (../../etc/passwd Angriff).
|
||
// Jeder Eintragspfad muss innerhalb von finalBackupDir bleiben.
|
||
const absBackupDir = path.resolve(finalBackupDir);
|
||
fs.mkdirSync(absBackupDir, { recursive: true });
|
||
|
||
for (const entry of entries) {
|
||
// Pfade mit absoluten Pfaden oder Traversal ablehnen
|
||
const entryName = entry.entryName;
|
||
if (entryName.includes('\0') || path.isAbsolute(entryName)) {
|
||
return { success: false, error: `Ungültiger Eintrag im ZIP: ${entryName}` };
|
||
}
|
||
|
||
const targetPath = path.resolve(absBackupDir, entryName);
|
||
// Zip-Slip-Check: aufgelöster Pfad muss im Backup-Verzeichnis liegen
|
||
if (!targetPath.startsWith(absBackupDir + path.sep) && targetPath !== absBackupDir) {
|
||
return {
|
||
success: false,
|
||
error: `Sicherheitsverletzung im ZIP: Pfad "${entryName}" zeigt außerhalb des Backup-Verzeichnisses`,
|
||
};
|
||
}
|
||
|
||
if (entry.isDirectory) {
|
||
fs.mkdirSync(targetPath, { recursive: true });
|
||
} else {
|
||
// Zielverzeichnis sicherstellen
|
||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||
// Datei schreiben
|
||
fs.writeFileSync(targetPath, entry.getData());
|
||
}
|
||
}
|
||
|
||
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.invoice.deleteMany({});
|
||
await prisma.energyContractDetails.deleteMany({});
|
||
await prisma.meterReading.deleteMany({});
|
||
await prisma.contractHistoryEntry.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');
|
||
|
||
// Anbieter mit Tarifen (aus bestehender Datenbank)
|
||
const providersWithTariffs = [
|
||
{
|
||
provider: { name: '1&1', portalUrl: 'https://control-center.1und1.de/' },
|
||
tariffs: [],
|
||
},
|
||
{
|
||
provider: { name: 'Congstar', portalUrl: 'https://www.congstar.de/login/' },
|
||
tariffs: [],
|
||
},
|
||
{
|
||
provider: { name: 'E wie einfach', portalUrl: '' },
|
||
tariffs: ['Grün & Günstig Strom'],
|
||
},
|
||
{
|
||
provider: { name: 'EVD Energieversogung Deutschland', portalUrl: 'https://mein.ev-d.de/selfcare' },
|
||
tariffs: ['EVD Gas Bonus 12'],
|
||
},
|
||
{
|
||
provider: { name: 'Klarmobil', portalUrl: 'https://www.klarmobil.de/login' },
|
||
tariffs: ['Allnet Flat 30GB 5G (+00) Telekom'],
|
||
},
|
||
{
|
||
provider: { name: 'O2', portalUrl: 'https://www.o2online.de/ecare/selfcare' },
|
||
tariffs: [],
|
||
},
|
||
{
|
||
provider: { name: 'Otelo', portalUrl: 'https://www.otelo.de/mein-otelo/login' },
|
||
tariffs: [],
|
||
},
|
||
{
|
||
provider: { name: 'Telekom', portalUrl: 'https://www.telekom.de/kundencenter/startseite' },
|
||
tariffs: [],
|
||
},
|
||
{
|
||
provider: { name: 'Vodafone', portalUrl: 'https://www.vodafone.de/meinvodafone/account/login' },
|
||
tariffs: [],
|
||
},
|
||
];
|
||
|
||
for (const { provider, tariffs } of providersWithTariffs) {
|
||
const createdProvider = await prisma.provider.create({
|
||
data: { ...provider, isActive: true },
|
||
});
|
||
// Tarife für diesen Anbieter anlegen
|
||
for (const tariffName of tariffs) {
|
||
await prisma.tariff.create({
|
||
data: {
|
||
providerId: createdProvider.id,
|
||
name: tariffName,
|
||
isActive: true,
|
||
},
|
||
});
|
||
}
|
||
}
|
||
console.log('[FactoryReset] Anbieter mit Tarifen 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 };
|
||
}
|
||
}
|