added backup and email client
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('Adding/updating permissions and Developer role...');
|
||||
|
||||
// 1. Create or get the emails:delete permission
|
||||
const emailsDeletePerm = await prisma.permission.upsert({
|
||||
where: { resource_action: { resource: 'emails', action: 'delete' } },
|
||||
update: {},
|
||||
create: { resource: 'emails', action: 'delete' },
|
||||
});
|
||||
console.log('emails:delete permission created/found');
|
||||
|
||||
// 2. Create or get the developer:access permission
|
||||
const developerAccessPerm = await prisma.permission.upsert({
|
||||
where: { resource_action: { resource: 'developer', action: 'access' } },
|
||||
update: {},
|
||||
create: { resource: 'developer', action: 'access' },
|
||||
});
|
||||
console.log('developer:access permission created/found');
|
||||
|
||||
// 3. Create Developer role if it doesn't exist
|
||||
let developerRole = await prisma.role.findUnique({
|
||||
where: { name: 'Developer' },
|
||||
});
|
||||
|
||||
if (!developerRole) {
|
||||
// Get all permissions for Developer role
|
||||
const allPermissions = await prisma.permission.findMany();
|
||||
developerRole = await prisma.role.create({
|
||||
data: {
|
||||
name: 'Developer',
|
||||
description: 'Voller Zugriff inkl. Entwickler-Tools',
|
||||
permissions: {
|
||||
create: allPermissions.map(p => ({ permissionId: p.id })),
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log('Developer role created with all permissions');
|
||||
}
|
||||
|
||||
// 4. Add emails:delete to Admin and Developer
|
||||
const rolesToUpdate = [
|
||||
{ name: 'Admin', permissions: [emailsDeletePerm] },
|
||||
{ name: 'Developer', permissions: [emailsDeletePerm, developerAccessPerm] },
|
||||
];
|
||||
|
||||
for (const roleConfig of rolesToUpdate) {
|
||||
const role = await prisma.role.findUnique({
|
||||
where: { name: roleConfig.name },
|
||||
include: { permissions: true },
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
console.log(`${roleConfig.name} role not found, skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const perm of roleConfig.permissions) {
|
||||
const hasPermission = role.permissions.some(
|
||||
(rp) => rp.permissionId === perm.id
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
await prisma.rolePermission.create({
|
||||
data: {
|
||||
roleId: role.id,
|
||||
permissionId: perm.id,
|
||||
},
|
||||
});
|
||||
console.log(`Added ${perm.resource}:${perm.action} to ${roleConfig.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Done!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Datenbank-Backup Script
|
||||
*
|
||||
* Exportiert alle Daten als JSON-Dateien für die Wiederherstellung nach Migrationen.
|
||||
*
|
||||
* Verwendung:
|
||||
* npx ts-node prisma/backup-data.ts
|
||||
*
|
||||
* Erstellt einen Ordner 'backups/YYYY-MM-DD_HH-mm-ss/' mit JSON-Dateien pro Tabelle.
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
// Backup-Ordner mit Zeitstempel erstellen
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||
const backupDir = path.join(__dirname, 'backups', timestamp);
|
||||
|
||||
if (!fs.existsSync(backupDir)) {
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
}
|
||||
|
||||
console.log(`\n📦 Starte Datenbank-Backup nach: ${backupDir}\n`);
|
||||
|
||||
// Tabellen in Abhängigkeitsreihenfolge (unabhängige zuerst)
|
||||
const tables = [
|
||||
// Level 0: Keine Abhängigkeiten
|
||||
{ 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: 'EnergyProvider', query: () => prisma.energyProvider.findMany() },
|
||||
{ name: 'TelecomProvider', query: () => prisma.telecomProvider.findMany() },
|
||||
|
||||
// Level 1: Abhängig von Level 0
|
||||
{ name: 'RolePermission', query: () => prisma.rolePermission.findMany() },
|
||||
{ name: 'User', query: () => prisma.user.findMany() },
|
||||
{ name: 'Customer', query: () => prisma.customer.findMany() },
|
||||
{ name: 'Tariff', query: () => prisma.tariff.findMany() },
|
||||
|
||||
// Level 2: Abhängig von Level 1
|
||||
{ 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() },
|
||||
|
||||
// Level 3: Abhängig von Level 2
|
||||
{ name: 'CachedEmail', query: () => prisma.cachedEmail.findMany() },
|
||||
{ name: 'ContractTask', query: () => prisma.contractTask.findMany() },
|
||||
{ name: 'MeterReading', query: () => prisma.meterReading.findMany() },
|
||||
{ name: 'ContractNote', query: () => prisma.contractNote.findMany() },
|
||||
{ name: 'ContractDocument', query: () => prisma.contractDocument.findMany() },
|
||||
|
||||
// Level 4: Abhängig von Level 3
|
||||
{ name: 'ContractTaskSubtask', query: () => prisma.contractTaskSubtask.findMany() },
|
||||
|
||||
// Vertragstyp-spezifische Details
|
||||
{ name: 'EnergyContractDetails', query: () => prisma.energyContractDetails.findMany() },
|
||||
{ name: 'TelecomContractDetails', query: () => prisma.telecomContractDetails.findMany() },
|
||||
{ name: 'CarInsuranceDetails', query: () => prisma.carInsuranceDetails.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 });
|
||||
|
||||
// JSON-Datei schreiben
|
||||
const filePath = path.join(backupDir, `${table.name}.json`);
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||
|
||||
const status = count > 0 ? '✅' : '⚪';
|
||||
console.log(`${status} ${table.name}: ${count} Einträge`);
|
||||
} catch (error: any) {
|
||||
// Tabelle existiert möglicherweise nicht (bei älteren Schema-Versionen)
|
||||
console.log(`⚠️ ${table.name}: Übersprungen (${error.message?.slice(0, 50)}...)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Backup-Info speichern
|
||||
const backupInfo = {
|
||||
timestamp: new Date().toISOString(),
|
||||
totalRecords,
|
||||
tables: stats,
|
||||
};
|
||||
fs.writeFileSync(path.join(backupDir, '_backup-info.json'), JSON.stringify(backupInfo, null, 2));
|
||||
|
||||
console.log(`\n✅ Backup abgeschlossen!`);
|
||||
console.log(` 📊 ${totalRecords} Datensätze in ${stats.filter(s => s.count > 0).length} Tabellen`);
|
||||
console.log(` 📁 Gespeichert in: ${backupDir}\n`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('❌ Backup fehlgeschlagen:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[portalEmail]` on the table `Customer` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE `Contract` ADD COLUMN `stressfreiEmailId` INTEGER NULL,
|
||||
MODIFY `status` ENUM('DRAFT', 'PENDING', 'ACTIVE', 'CANCELLED', 'EXPIRED', 'DEACTIVATED') NOT NULL DEFAULT 'DRAFT';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Customer` ADD COLUMN `portalEmail` VARCHAR(191) NULL,
|
||||
ADD COLUMN `portalEnabled` BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN `portalLastLogin` DATETIME(3) NULL,
|
||||
ADD COLUMN `portalPasswordEncrypted` VARCHAR(191) NULL,
|
||||
ADD COLUMN `portalPasswordHash` VARCHAR(191) NULL;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `AppSetting` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`key` VARCHAR(191) NOT NULL,
|
||||
`value` TEXT NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `AppSetting_key_key`(`key`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `CustomerRepresentative` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`customerId` INTEGER NOT NULL,
|
||||
`representativeId` INTEGER NOT NULL,
|
||||
`notes` VARCHAR(191) NULL,
|
||||
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `CustomerRepresentative_customerId_representativeId_key`(`customerId`, `representativeId`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `EmailProviderConfig` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`type` ENUM('PLESK', 'CPANEL', 'DIRECTADMIN') NOT NULL,
|
||||
`apiUrl` VARCHAR(191) NOT NULL,
|
||||
`apiKey` VARCHAR(191) NULL,
|
||||
`username` VARCHAR(191) NULL,
|
||||
`passwordEncrypted` VARCHAR(191) NULL,
|
||||
`domain` VARCHAR(191) NOT NULL,
|
||||
`defaultForwardEmail` VARCHAR(191) NULL,
|
||||
`imapServer` VARCHAR(191) NULL,
|
||||
`imapPort` INTEGER NULL DEFAULT 993,
|
||||
`smtpServer` VARCHAR(191) NULL,
|
||||
`smtpPort` INTEGER NULL DEFAULT 465,
|
||||
`imapEncryption` ENUM('SSL', 'STARTTLS', 'NONE') NOT NULL DEFAULT 'SSL',
|
||||
`smtpEncryption` ENUM('SSL', 'STARTTLS', 'NONE') NOT NULL DEFAULT 'SSL',
|
||||
`allowSelfSignedCerts` BOOLEAN NOT NULL DEFAULT false,
|
||||
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||
`isDefault` BOOLEAN NOT NULL DEFAULT false,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `EmailProviderConfig_name_key`(`name`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `StressfreiEmail` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`customerId` INTEGER NOT NULL,
|
||||
`email` VARCHAR(191) NOT NULL,
|
||||
`platform` VARCHAR(191) NULL,
|
||||
`notes` TEXT NULL,
|
||||
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||
`isProvisioned` BOOLEAN NOT NULL DEFAULT false,
|
||||
`provisionedAt` DATETIME(3) NULL,
|
||||
`provisionError` TEXT NULL,
|
||||
`hasMailbox` BOOLEAN NOT NULL DEFAULT false,
|
||||
`emailPasswordEncrypted` VARCHAR(191) NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `CachedEmail` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`stressfreiEmailId` INTEGER NOT NULL,
|
||||
`folder` ENUM('INBOX', 'SENT') NOT NULL DEFAULT 'INBOX',
|
||||
`messageId` VARCHAR(191) NOT NULL,
|
||||
`uid` INTEGER NOT NULL,
|
||||
`subject` VARCHAR(191) NULL,
|
||||
`fromAddress` VARCHAR(191) NOT NULL,
|
||||
`fromName` VARCHAR(191) NULL,
|
||||
`toAddresses` TEXT NOT NULL,
|
||||
`ccAddresses` TEXT NULL,
|
||||
`receivedAt` DATETIME(3) NOT NULL,
|
||||
`textBody` LONGTEXT NULL,
|
||||
`htmlBody` LONGTEXT NULL,
|
||||
`hasAttachments` BOOLEAN NOT NULL DEFAULT false,
|
||||
`attachmentNames` TEXT NULL,
|
||||
`contractId` INTEGER NULL,
|
||||
`assignedAt` DATETIME(3) NULL,
|
||||
`assignedBy` INTEGER NULL,
|
||||
`isAutoAssigned` BOOLEAN NOT NULL DEFAULT false,
|
||||
`isRead` BOOLEAN NOT NULL DEFAULT false,
|
||||
`isStarred` BOOLEAN NOT NULL DEFAULT false,
|
||||
`isDeleted` BOOLEAN NOT NULL DEFAULT false,
|
||||
`deletedAt` DATETIME(3) NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `CachedEmail_contractId_idx`(`contractId`),
|
||||
INDEX `CachedEmail_stressfreiEmailId_folder_receivedAt_idx`(`stressfreiEmailId`, `folder`, `receivedAt`),
|
||||
INDEX `CachedEmail_stressfreiEmailId_isDeleted_idx`(`stressfreiEmailId`, `isDeleted`),
|
||||
UNIQUE INDEX `CachedEmail_stressfreiEmailId_messageId_folder_key`(`stressfreiEmailId`, `messageId`, `folder`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `ContractTask` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`contractId` INTEGER NOT NULL,
|
||||
`title` VARCHAR(191) NOT NULL,
|
||||
`description` TEXT NULL,
|
||||
`status` ENUM('OPEN', 'COMPLETED') NOT NULL DEFAULT 'OPEN',
|
||||
`visibleInPortal` BOOLEAN NOT NULL DEFAULT false,
|
||||
`createdBy` VARCHAR(191) NULL,
|
||||
`completedAt` DATETIME(3) NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `ContractTaskSubtask` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`taskId` INTEGER NOT NULL,
|
||||
`title` VARCHAR(191) NOT NULL,
|
||||
`status` ENUM('OPEN', 'COMPLETED') NOT NULL DEFAULT 'OPEN',
|
||||
`createdBy` VARCHAR(191) NULL,
|
||||
`completedAt` DATETIME(3) NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX `Customer_portalEmail_key` ON `Customer`(`portalEmail`);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CustomerRepresentative` ADD CONSTRAINT `CustomerRepresentative_customerId_fkey` FOREIGN KEY (`customerId`) REFERENCES `Customer`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CustomerRepresentative` ADD CONSTRAINT `CustomerRepresentative_representativeId_fkey` FOREIGN KEY (`representativeId`) REFERENCES `Customer`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `StressfreiEmail` ADD CONSTRAINT `StressfreiEmail_customerId_fkey` FOREIGN KEY (`customerId`) REFERENCES `Customer`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CachedEmail` ADD CONSTRAINT `CachedEmail_stressfreiEmailId_fkey` FOREIGN KEY (`stressfreiEmailId`) REFERENCES `StressfreiEmail`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CachedEmail` ADD CONSTRAINT `CachedEmail_contractId_fkey` FOREIGN KEY (`contractId`) REFERENCES `Contract`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Contract` ADD CONSTRAINT `Contract_stressfreiEmailId_fkey` FOREIGN KEY (`stressfreiEmailId`) REFERENCES `StressfreiEmail`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `ContractTask` ADD CONSTRAINT `ContractTask_contractId_fkey` FOREIGN KEY (`contractId`) REFERENCES `Contract`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `ContractTaskSubtask` ADD CONSTRAINT `ContractTaskSubtask_taskId_fkey` FOREIGN KEY (`taskId`) REFERENCES `ContractTask`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `User` ADD COLUMN `tokenInvalidatedAt` DATETIME(3) NULL;
|
||||
@@ -0,0 +1,486 @@
|
||||
/**
|
||||
* Datenbank-Restore Script
|
||||
*
|
||||
* Stellt Daten aus einem JSON-Backup wieder her.
|
||||
*
|
||||
* Verwendung:
|
||||
* npx ts-node prisma/restore-data.ts [backup-ordner]
|
||||
*
|
||||
* Beispiele:
|
||||
* npx ts-node prisma/restore-data.ts # Letztes Backup
|
||||
* npx ts-node prisma/restore-data.ts 2025-01-31_14-30-00 # Bestimmtes Backup
|
||||
*
|
||||
* WICHTIG: Führe vorher 'npx prisma migrate deploy' oder 'npx prisma db push' aus!
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 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: Datum-Strings zu Date-Objekten konvertieren
|
||||
function convertDates(obj: any): any {
|
||||
if (obj === null || obj === undefined) return obj;
|
||||
if (typeof obj === 'string') {
|
||||
// ISO-Datumsformat erkennen
|
||||
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;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Backup-Ordner bestimmen
|
||||
const backupsDir = path.join(__dirname, 'backups');
|
||||
let backupName = process.argv[2];
|
||||
|
||||
if (!backupName) {
|
||||
// Neuestes Backup finden
|
||||
if (!fs.existsSync(backupsDir)) {
|
||||
console.error('❌ Kein Backup-Ordner gefunden!');
|
||||
process.exit(1);
|
||||
}
|
||||
const backups = fs.readdirSync(backupsDir)
|
||||
.filter(f => fs.statSync(path.join(backupsDir, f)).isDirectory())
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
if (backups.length === 0) {
|
||||
console.error('❌ Keine Backups gefunden!');
|
||||
process.exit(1);
|
||||
}
|
||||
backupName = backups[0];
|
||||
console.log(`📦 Verwende neuestes Backup: ${backupName}`);
|
||||
}
|
||||
|
||||
const backupDir = path.join(backupsDir, backupName);
|
||||
|
||||
if (!fs.existsSync(backupDir)) {
|
||||
console.error(`❌ Backup-Ordner nicht gefunden: ${backupDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Backup-Info lesen
|
||||
const infoPath = path.join(backupDir, '_backup-info.json');
|
||||
if (fs.existsSync(infoPath)) {
|
||||
const info = JSON.parse(fs.readFileSync(infoPath, 'utf-8'));
|
||||
console.log(`\n📅 Backup vom: ${new Date(info.timestamp).toLocaleString('de-DE')}`);
|
||||
console.log(`📊 ${info.totalRecords} Datensätze in ${info.tables.filter((t: any) => t.count > 0).length} Tabellen\n`);
|
||||
}
|
||||
|
||||
console.log(`🔄 Starte Wiederherstellung aus: ${backupDir}\n`);
|
||||
|
||||
// Foreign Key Checks deaktivieren für MySQL
|
||||
await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 0');
|
||||
|
||||
try {
|
||||
// Tabellen in Abhängigkeitsreihenfolge wiederherstellen
|
||||
const restoreOrder = [
|
||||
// Level 0: Keine Abhängigkeiten
|
||||
{
|
||||
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: 'EnergyProvider',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.energyProvider.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'TelecomProvider',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.telecomProvider.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Level 1
|
||||
{
|
||||
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),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Level 2
|
||||
{
|
||||
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),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Level 3
|
||||
{
|
||||
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: 'ContractNote',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.contractNote.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),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Level 4
|
||||
{
|
||||
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),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Vertragsdetails
|
||||
{
|
||||
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: 'TelecomContractDetails',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.telecomContractDetails.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),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let totalRestored = 0;
|
||||
|
||||
for (const table of restoreOrder) {
|
||||
const filePath = path.join(backupDir, `${table.name}.json`);
|
||||
const data = readJsonFile(filePath);
|
||||
|
||||
if (data.length === 0) {
|
||||
console.log(`⚪ ${table.name}: Keine Daten`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await table.restore(data);
|
||||
totalRestored += data.length;
|
||||
console.log(`✅ ${table.name}: ${data.length} Einträge wiederhergestellt`);
|
||||
} catch (error: any) {
|
||||
console.log(`⚠️ ${table.name}: Fehler - ${error.message?.slice(0, 80)}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ Wiederherstellung abgeschlossen!`);
|
||||
console.log(` 📊 ${totalRestored} Datensätze wiederhergestellt\n`);
|
||||
|
||||
} finally {
|
||||
// Foreign Key Checks wieder aktivieren
|
||||
await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 1');
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('❌ Wiederherstellung fehlgeschlagen:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
+108
-24
@@ -20,17 +20,18 @@ model AppSetting {
|
||||
// ==================== USERS & AUTH ====================
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
password String
|
||||
firstName String
|
||||
lastName String
|
||||
isActive Boolean @default(true)
|
||||
customerId Int? @unique
|
||||
customer Customer? @relation(fields: [customerId], references: [id])
|
||||
roles UserRole[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
password String
|
||||
firstName String
|
||||
lastName String
|
||||
isActive Boolean @default(true)
|
||||
tokenInvalidatedAt DateTime? // Zeitpunkt ab dem alle Tokens ungültig sind (für Zwangslogout bei Rechteänderung)
|
||||
customerId Int? @unique
|
||||
customer Customer? @relation(fields: [customerId], references: [id])
|
||||
roles UserRole[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Role {
|
||||
@@ -216,6 +217,13 @@ enum EmailProviderType {
|
||||
DIRECTADMIN
|
||||
}
|
||||
|
||||
// Verschlüsselungstyp für E-Mail-Verbindungen
|
||||
enum MailEncryption {
|
||||
SSL // Implicit SSL/TLS (Ports 465/993) - Verschlüsselung von Anfang an
|
||||
STARTTLS // STARTTLS (Ports 587/143) - Startet unverschlüsselt, dann Upgrade
|
||||
NONE // Keine Verschlüsselung (Ports 25/143)
|
||||
}
|
||||
|
||||
model EmailProviderConfig {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique // z.B. "Plesk Hauptserver"
|
||||
@@ -226,6 +234,18 @@ model EmailProviderConfig {
|
||||
passwordEncrypted String? // Passwort (verschlüsselt)
|
||||
domain String // Domain für E-Mails (z.B. stressfrei-wechseln.de)
|
||||
defaultForwardEmail String? // Standard-Weiterleitungsadresse (unsere eigene)
|
||||
|
||||
// IMAP/SMTP-Server für E-Mail-Client (optional, default: mail.{domain})
|
||||
imapServer String? // z.B. "mail.stressfrei-wechseln.de"
|
||||
imapPort Int? @default(993)
|
||||
smtpServer String?
|
||||
smtpPort Int? @default(465)
|
||||
|
||||
// Verschlüsselungs-Einstellungen
|
||||
imapEncryption MailEncryption @default(SSL) // SSL, STARTTLS oder NONE
|
||||
smtpEncryption MailEncryption @default(SSL) // SSL, STARTTLS oder NONE
|
||||
allowSelfSignedCerts Boolean @default(false) // Selbstsignierte Zertifikate erlauben
|
||||
|
||||
isActive Boolean @default(true)
|
||||
isDefault Boolean @default(false) // Standard-Provider
|
||||
createdAt DateTime @default(now())
|
||||
@@ -235,19 +255,82 @@ model EmailProviderConfig {
|
||||
// ==================== STRESSFREI-WECHSELN EMAIL ADDRESSES ====================
|
||||
|
||||
model StressfreiEmail {
|
||||
id Int @id @default(autoincrement())
|
||||
customerId Int
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
email String // Die Weiterleitungs-E-Mail-Adresse
|
||||
platform String? // Für welche Plattform (z.B. "Freenet", "Klarmobil")
|
||||
notes String? @db.Text // Optionale Notizen
|
||||
isActive Boolean @default(true)
|
||||
isProvisioned Boolean @default(false) // Wurde bei Provider angelegt?
|
||||
provisionedAt DateTime? // Wann wurde provisioniert?
|
||||
provisionError String? @db.Text // Fehlermeldung falls Provisionierung fehlschlug
|
||||
contracts Contract[] // Verträge die diese E-Mail als Benutzername verwenden
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id Int @id @default(autoincrement())
|
||||
customerId Int
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
email String // Die Weiterleitungs-E-Mail-Adresse
|
||||
platform String? // Für welche Plattform (z.B. "Freenet", "Klarmobil")
|
||||
notes String? @db.Text // Optionale Notizen
|
||||
isActive Boolean @default(true)
|
||||
isProvisioned Boolean @default(false) // Wurde bei Provider angelegt?
|
||||
provisionedAt DateTime? // Wann wurde provisioniert?
|
||||
provisionError String? @db.Text // Fehlermeldung falls Provisionierung fehlschlug
|
||||
|
||||
// Mailbox-Zugangsdaten (für IMAP/SMTP-Zugang)
|
||||
hasMailbox Boolean @default(false) // Hat echte Mailbox (nicht nur Weiterleitung)?
|
||||
emailPasswordEncrypted String? // Verschlüsseltes Mailbox-Passwort (AES-256-GCM)
|
||||
|
||||
contracts Contract[] // Verträge die diese E-Mail als Benutzername verwenden
|
||||
cachedEmails CachedEmail[] // Gecachte E-Mails aus dieser Mailbox
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ==================== CACHED EMAILS (E-Mail-Client) ====================
|
||||
|
||||
enum EmailFolder {
|
||||
INBOX
|
||||
SENT
|
||||
}
|
||||
|
||||
model CachedEmail {
|
||||
id Int @id @default(autoincrement())
|
||||
stressfreiEmailId Int
|
||||
stressfreiEmail StressfreiEmail @relation(fields: [stressfreiEmailId], references: [id], onDelete: Cascade)
|
||||
|
||||
// Ordner (Posteingang oder Gesendet)
|
||||
folder EmailFolder @default(INBOX)
|
||||
|
||||
// IMAP-Identifikation
|
||||
messageId String // RFC 5322 Message-ID
|
||||
uid Int // IMAP UID (für Synchronisierung, bei SENT = 0)
|
||||
|
||||
// E-Mail-Metadaten
|
||||
subject String?
|
||||
fromAddress String
|
||||
fromName String?
|
||||
toAddresses String @db.Text // JSON Array
|
||||
ccAddresses String? @db.Text // JSON Array
|
||||
receivedAt DateTime
|
||||
|
||||
// Inhalt
|
||||
textBody String? @db.LongText
|
||||
htmlBody String? @db.LongText
|
||||
hasAttachments Boolean @default(false)
|
||||
attachmentNames String? @db.Text // JSON Array
|
||||
|
||||
// Vertragszuordnung
|
||||
contractId Int?
|
||||
contract Contract? @relation(fields: [contractId], references: [id], onDelete: SetNull)
|
||||
assignedAt DateTime?
|
||||
assignedBy Int? // User ID der die Zuordnung gemacht hat
|
||||
isAutoAssigned Boolean @default(false) // true = automatisch beim Senden aus Vertrag
|
||||
|
||||
// Flags
|
||||
isRead Boolean @default(false)
|
||||
isStarred Boolean @default(false)
|
||||
|
||||
// Papierkorb
|
||||
isDeleted Boolean @default(false) // Im Papierkorb?
|
||||
deletedAt DateTime? // Wann gelöscht?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([stressfreiEmailId, messageId, folder]) // Folder hinzugefügt: gleiche MessageID kann in INBOX und SENT existieren
|
||||
@@index([contractId])
|
||||
@@index([stressfreiEmailId, folder, receivedAt])
|
||||
@@index([stressfreiEmailId, isDeleted]) // Für Papierkorb-Abfragen
|
||||
}
|
||||
|
||||
// ==================== METERS (Energy) ====================
|
||||
@@ -465,6 +548,7 @@ model Contract {
|
||||
carInsuranceDetails CarInsuranceDetails?
|
||||
|
||||
tasks ContractTask[]
|
||||
assignedEmails CachedEmail[] // Zugeordnete E-Mails aus dem E-Mail-Client
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
+195
-19
@@ -6,17 +6,31 @@ const prisma = new PrismaClient();
|
||||
async function main() {
|
||||
console.log('Seeding database...');
|
||||
|
||||
// Create permissions
|
||||
const resources = ['customers', 'contracts', 'users', 'platforms', 'providers', 'developer'];
|
||||
const actions = ['create', 'read', 'update', 'delete', 'access'];
|
||||
// ==================== PERMISSIONS ====================
|
||||
// Ressourcen mit ihren erlaubten Aktionen
|
||||
const resourcePermissions: Record<string, string[]> = {
|
||||
// Haupt-Ressourcen (CRUD)
|
||||
customers: ['create', 'read', 'update', 'delete'],
|
||||
contracts: ['create', 'read', 'update', 'delete'],
|
||||
users: ['create', 'read', 'update', 'delete'],
|
||||
platforms: ['create', 'read', 'update', 'delete'],
|
||||
providers: ['create', 'read', 'update', 'delete'],
|
||||
tariffs: ['create', 'read', 'update', 'delete'],
|
||||
// Konfiguration (CRUD)
|
||||
'cancellation-periods': ['create', 'read', 'update', 'delete'],
|
||||
'contract-durations': ['create', 'read', 'update', 'delete'],
|
||||
'contract-categories': ['create', 'read', 'update', 'delete'],
|
||||
'email-providers': ['create', 'read', 'update', 'delete'],
|
||||
// Einstellungen (nur lesen/ändern)
|
||||
settings: ['read', 'update'],
|
||||
// Spezial-Permissions
|
||||
developer: ['access'],
|
||||
emails: ['delete'],
|
||||
};
|
||||
|
||||
const permissions: { resource: string; action: string }[] = [];
|
||||
for (const resource of resources) {
|
||||
for (const [resource, actions] of Object.entries(resourcePermissions)) {
|
||||
for (const action of actions) {
|
||||
// developer nur mit 'access' action
|
||||
if (resource === 'developer' && action !== 'access') continue;
|
||||
// andere resources ohne 'access' action
|
||||
if (resource !== 'developer' && action === 'access') continue;
|
||||
permissions.push({ resource, action });
|
||||
}
|
||||
}
|
||||
@@ -29,7 +43,7 @@ async function main() {
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Permissions created');
|
||||
console.log(`Permissions created (${permissions.length} total)`);
|
||||
|
||||
// Get all permissions
|
||||
const allPermissions = await prisma.permission.findMany();
|
||||
@@ -63,14 +77,34 @@ async function main() {
|
||||
},
|
||||
});
|
||||
|
||||
// Employee - full access to customers, contracts, read platforms and providers
|
||||
// Developer - ALL permissions including developer:access
|
||||
const developerRole = await prisma.role.upsert({
|
||||
where: { name: 'Developer' },
|
||||
update: {},
|
||||
create: {
|
||||
name: 'Developer',
|
||||
description: 'Voller Zugriff inkl. Entwickler-Tools',
|
||||
permissions: {
|
||||
create: allPermissions.map((p) => ({ permissionId: p.id })),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Employee - full access to customers, contracts, read access to lookup tables
|
||||
const employeePermIds = allPermissions
|
||||
.filter(
|
||||
(p) =>
|
||||
p.resource === 'customers' ||
|
||||
p.resource === 'contracts' ||
|
||||
(p.resource === 'platforms' && p.action === 'read') ||
|
||||
(p.resource === 'providers' && p.action === 'read')
|
||||
// Read-only Zugriff auf Stammdaten und Konfiguration
|
||||
(p.action === 'read' && [
|
||||
'platforms',
|
||||
'providers',
|
||||
'tariffs',
|
||||
'cancellation-periods',
|
||||
'contract-durations',
|
||||
'contract-categories',
|
||||
].includes(p.resource))
|
||||
)
|
||||
.map((p) => p.id);
|
||||
|
||||
@@ -86,10 +120,20 @@ async function main() {
|
||||
},
|
||||
});
|
||||
|
||||
// Read-only employee
|
||||
const readOnlyPermIds = [customerReadPerm?.id, contractReadPerm?.id, platformReadPerm?.id, providerReadPerm?.id].filter(
|
||||
(id): id is number => id !== undefined
|
||||
);
|
||||
// Read-only employee - read access to main entities and lookup tables
|
||||
const readOnlyResources = [
|
||||
'customers',
|
||||
'contracts',
|
||||
'platforms',
|
||||
'providers',
|
||||
'tariffs',
|
||||
'cancellation-periods',
|
||||
'contract-durations',
|
||||
'contract-categories',
|
||||
];
|
||||
const readOnlyPermIds = allPermissions
|
||||
.filter((p) => p.action === 'read' && readOnlyResources.includes(p.resource))
|
||||
.map((p) => p.id);
|
||||
|
||||
const readOnlyRole = await prisma.role.upsert({
|
||||
where: { name: 'Mitarbeiter (Nur-Lesen)' },
|
||||
@@ -149,15 +193,76 @@ async function main() {
|
||||
|
||||
console.log('Sales platforms created');
|
||||
|
||||
// ==================== STANDARD PROVIDERS ====================
|
||||
const providers = [
|
||||
{
|
||||
name: 'Vodafone',
|
||||
portalUrl: 'https://www.vodafone.de/meinvodafone/account/login',
|
||||
usernameFieldName: 'username',
|
||||
passwordFieldName: 'password',
|
||||
},
|
||||
{
|
||||
name: 'Klarmobil',
|
||||
portalUrl: 'https://www.klarmobil.de/login',
|
||||
usernameFieldName: 'username',
|
||||
passwordFieldName: 'password',
|
||||
},
|
||||
{
|
||||
name: 'Otelo',
|
||||
portalUrl: 'https://www.otelo.de/mein-otelo/login',
|
||||
usernameFieldName: 'username',
|
||||
passwordFieldName: 'password',
|
||||
},
|
||||
{
|
||||
name: 'Congstar',
|
||||
portalUrl: 'https://www.congstar.de/login/',
|
||||
usernameFieldName: 'username',
|
||||
passwordFieldName: 'password',
|
||||
},
|
||||
{
|
||||
name: 'Telekom',
|
||||
portalUrl: 'https://www.telekom.de/kundencenter/startseite',
|
||||
usernameFieldName: 'username',
|
||||
passwordFieldName: 'password',
|
||||
},
|
||||
{
|
||||
name: 'O2',
|
||||
portalUrl: 'https://www.o2online.de/ecare/selfcare',
|
||||
usernameFieldName: 'username',
|
||||
passwordFieldName: 'password',
|
||||
},
|
||||
{
|
||||
name: '1&1',
|
||||
portalUrl: 'https://control-center.1und1.de/',
|
||||
usernameFieldName: 'username',
|
||||
passwordFieldName: 'password',
|
||||
},
|
||||
];
|
||||
|
||||
for (const provider of providers) {
|
||||
await prisma.provider.upsert({
|
||||
where: { name: provider.name },
|
||||
update: {
|
||||
portalUrl: provider.portalUrl,
|
||||
usernameFieldName: provider.usernameFieldName,
|
||||
passwordFieldName: provider.passwordFieldName,
|
||||
},
|
||||
create: { ...provider, isActive: true },
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Providers created');
|
||||
|
||||
// Create contract categories (matching existing enum values)
|
||||
const contractCategories = [
|
||||
{ code: 'ELECTRICITY', name: 'Strom', icon: 'Zap', color: '#FFC107', sortOrder: 1 },
|
||||
{ code: 'GAS', name: 'Gas', icon: 'Flame', color: '#FF5722', sortOrder: 2 },
|
||||
{ code: 'DSL', name: 'DSL', icon: 'Wifi', color: '#2196F3', sortOrder: 3 },
|
||||
{ code: 'FIBER', name: 'Glasfaser', icon: 'Cable', color: '#9C27B0', sortOrder: 4 },
|
||||
{ code: 'MOBILE', name: 'Mobilfunk', icon: 'Smartphone', color: '#4CAF50', sortOrder: 5 },
|
||||
{ code: 'TV', name: 'TV', icon: 'Tv', color: '#E91E63', sortOrder: 6 },
|
||||
{ code: 'CAR_INSURANCE', name: 'KFZ-Versicherung', icon: 'Car', color: '#607D8B', sortOrder: 7 },
|
||||
{ code: 'CABLE', name: 'Kabel Internet (Coax)', icon: 'Cable', color: '#00BCD4', sortOrder: 5 },
|
||||
{ code: 'MOBILE', name: 'Mobilfunk', icon: 'Smartphone', color: '#4CAF50', sortOrder: 6 },
|
||||
{ code: 'TV', name: 'TV', icon: 'Tv', color: '#E91E63', sortOrder: 7 },
|
||||
{ code: 'CAR_INSURANCE', name: 'KFZ-Versicherung', icon: 'Car', color: '#607D8B', sortOrder: 8 },
|
||||
];
|
||||
|
||||
for (const category of contractCategories) {
|
||||
@@ -170,6 +275,77 @@ async function main() {
|
||||
|
||||
console.log('Contract categories created');
|
||||
|
||||
// ==================== CANCELLATION PERIODS ====================
|
||||
const cancellationPeriods = [
|
||||
{ code: '14D', description: '14 Tage' },
|
||||
{ code: '1M', description: '1 Monat' },
|
||||
{ code: '2M', description: '2 Monate' },
|
||||
{ code: '3M', description: '3 Monate' },
|
||||
{ code: '6M', description: '6 Monate' },
|
||||
{ code: '12M', description: '12 Monate' },
|
||||
{ code: '1W', description: '1 Woche' },
|
||||
{ code: '2W', description: '2 Wochen' },
|
||||
{ code: '4W', description: '4 Wochen' },
|
||||
{ code: '6W', description: '6 Wochen' },
|
||||
];
|
||||
|
||||
for (const period of cancellationPeriods) {
|
||||
await prisma.cancellationPeriod.upsert({
|
||||
where: { code: period.code },
|
||||
update: { description: period.description },
|
||||
create: period,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Cancellation periods created');
|
||||
|
||||
// ==================== CONTRACT DURATIONS ====================
|
||||
const contractDurations = [
|
||||
{ code: '1M', description: '1 Monat' },
|
||||
{ code: '3M', description: '3 Monate' },
|
||||
{ code: '6M', description: '6 Monate' },
|
||||
{ code: '12M', description: '12 Monate' },
|
||||
{ code: '24M', description: '24 Monate' },
|
||||
{ code: '36M', description: '36 Monate' },
|
||||
{ code: '1J', description: '1 Jahr' },
|
||||
{ code: '2J', description: '2 Jahre' },
|
||||
{ code: '3J', description: '3 Jahre' },
|
||||
{ code: '4J', description: '4 Jahre' },
|
||||
{ code: '5J', description: '5 Jahre' },
|
||||
{ code: 'UNBEFRISTET', description: 'Unbefristet' },
|
||||
];
|
||||
|
||||
for (const duration of contractDurations) {
|
||||
await prisma.contractDuration.upsert({
|
||||
where: { code: duration.code },
|
||||
update: { description: duration.description },
|
||||
create: duration,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Contract durations created');
|
||||
|
||||
// ==================== APP SETTINGS ====================
|
||||
const appSettings = [
|
||||
// Cockpit-Einstellungen (Fristen-Ampel)
|
||||
{ key: 'deadlineCriticalDays', value: '14' }, // Rot: <= 14 Tage
|
||||
{ key: 'deadlineWarningDays', value: '42' }, // Gelb: <= 42 Tage
|
||||
{ key: 'deadlineOkDays', value: '90' }, // Grün: <= 90 Tage
|
||||
// Allgemeine Einstellungen
|
||||
{ key: 'companyName', value: 'OpenCRM' },
|
||||
{ key: 'defaultEmailDomain', value: 'stressfrei-wechseln.de' },
|
||||
];
|
||||
|
||||
for (const setting of appSettings) {
|
||||
await prisma.appSetting.upsert({
|
||||
where: { key: setting.key },
|
||||
update: {}, // Bestehende Werte nicht überschreiben
|
||||
create: setting,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('App settings created');
|
||||
|
||||
console.log('Seeding completed!');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user