Files
opencrm/backend/prisma/seed.ts
T
duffyduckandClaude Opus 5 c7d6b6de7e Audit-Pruefung: "manipuliert" von "Luecke" getrennt + Retention fuer Routine-Auth
Problem 1 (Deutbarkeit): verifyIntegrity warf zwei voellig unterschiedliche
Befunde in einen Topf und meldete beides als "N manipulierte Eintraege". Eine
harmlose Verkettungsluecke sah damit aus wie ein Angriff - die Meldung war im
Alltag nicht deutbar und dadurch wertlos, dasselbe Muster wie beim
Refresh-Rauschen.

Fix: Rueckgabe um tamperedEntries (Inhalt nachtraeglich veraendert, ernst) und
chainGaps (Verkettung unterbrochen durch parallele Schreibvorgaenge oder
geloeschte Zeilen, meist harmlos) erweitert. invalidEntries bleibt als Summe
erhalten. Controller formuliert die Meldung eindeutig, Frontend-API-Typ
nachgezogen.

Problem 2 (Aufbewahrung): Token-Refreshes landen seit der Entrauschung als
Authentication/LOW. Diese Kombination traf auf keine spezifische Regel und fiel
in die Auffangregel * mit 3650 Tagen - das Rauschen waere 10 Jahre aufbewahrt
worden, echte Logins nur 2. Fix: Regel Authentication/LOW mit 90 Tagen, als
idempotente Migration und im Seed.

Sensitivitaet steuert die Aufbewahrung und ist keine Alarmstufe - normale
Logins und Zugriffe auf Bankdaten/Ausweise bleiben bewusst CRITICAL, ein
Herabstufen wuerde still die Aufbewahrungsfrist verlaengern.

Verifiziert: Live-Test gegen Dev-DB - echte Manipulation einer Zeile wird als
manipuliert erkannt und nicht mit Luecken verwechselt, Ketten-Luecken bleiben
bei 7, Originalzustand exakt wiederhergestellt. tsc + vite build gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 11:30:36 +02:00

559 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
const prisma = new PrismaClient();
async function main() {
console.log('Seeding database...');
// ==================== 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'],
// DSGVO & Audit
audit: ['read', 'export', 'admin'],
gdpr: ['export', 'delete', 'admin'],
};
const permissions: { resource: string; action: string }[] = [];
for (const [resource, actions] of Object.entries(resourcePermissions)) {
for (const action of actions) {
permissions.push({ resource, action });
}
}
for (const perm of permissions) {
await prisma.permission.upsert({
where: { resource_action: perm },
update: {},
create: perm,
});
}
console.log(`Permissions created (${permissions.length} total)`);
// Get all permissions
const allPermissions = await prisma.permission.findMany();
const customerReadPerm = allPermissions.find(
(p) => p.resource === 'customers' && p.action === 'read'
);
const contractReadPerm = allPermissions.find(
(p) => p.resource === 'contracts' && p.action === 'read'
);
const platformReadPerm = allPermissions.find(
(p) => p.resource === 'platforms' && p.action === 'read'
);
const providerReadPerm = allPermissions.find(
(p) => p.resource === 'providers' && p.action === 'read'
);
// Helper: Sync permissions for a role (adds missing, removes excess)
async function syncRolePermissions(roleId: number, permissionIds: number[]) {
const existing = await prisma.rolePermission.findMany({
where: { roleId },
select: { permissionId: true },
});
const existingIds = new Set(existing.map((e) => e.permissionId));
const targetIds = new Set(permissionIds);
// Add missing permissions
const missing = permissionIds.filter((id) => !existingIds.has(id));
if (missing.length > 0) {
await prisma.rolePermission.createMany({
data: missing.map((permissionId) => ({ roleId, permissionId })),
skipDuplicates: true,
});
console.log(` → ${missing.length} Permissions hinzugefügt für Rolle #${roleId}`);
}
// Remove excess permissions
const excess = existing.filter((e) => !targetIds.has(e.permissionId)).map((e) => e.permissionId);
if (excess.length > 0) {
await prisma.rolePermission.deleteMany({
where: { roleId, permissionId: { in: excess } },
});
console.log(` → ${excess.length} Permissions entfernt für Rolle #${roleId}`);
}
}
// Create roles
// Admin - all permissions EXCEPT developer:access and audit/gdpr (controlled separately via checkboxes)
const adminPermissions = allPermissions.filter(
(p) =>
!(p.resource === 'developer' && p.action === 'access') &&
p.resource !== 'audit' &&
p.resource !== 'gdpr'
);
const adminRole = await prisma.role.upsert({
where: { name: 'Admin' },
update: {},
create: {
name: 'Admin',
description: 'Voller Zugriff auf alle Funktionen',
permissions: {
create: adminPermissions.map((p) => ({ permissionId: p.id })),
},
},
});
await syncRolePermissions(adminRole.id, adminPermissions.map((p) => p.id));
// Developer - ALL permissions (developer:access + alles andere)
const developerPermissions = allPermissions;
const developerRole = await prisma.role.upsert({
where: { name: 'Developer' },
update: {},
create: {
name: 'Developer',
description: 'Voller Zugriff inkl. Entwickler-Tools',
permissions: {
create: developerPermissions.map((p) => ({ permissionId: p.id })),
},
},
});
await syncRolePermissions(developerRole.id, developerPermissions.map((p) => p.id));
// DSGVO - audit and gdpr permissions (hidden role, controlled via hasGdprAccess)
const gdprPermissions = allPermissions.filter(
(p) => p.resource === 'audit' || p.resource === 'gdpr'
);
const gdprRole = await prisma.role.upsert({
where: { name: 'DSGVO' },
update: {},
create: {
name: 'DSGVO',
description: 'DSGVO-Zugriff: Audit-Logs und Datenschutz-Verwaltung',
permissions: {
create: gdprPermissions.map((p) => ({ permissionId: p.id })),
},
},
});
await syncRolePermissions(gdprRole.id, gdprPermissions.map((p) => p.id));
// Employee - full access to customers, contracts, read access to lookup tables
const employeePermIds = allPermissions
.filter(
(p) =>
p.resource === 'customers' ||
p.resource === 'contracts' ||
// 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);
const employeeRole = await prisma.role.upsert({
where: { name: 'Mitarbeiter' },
update: {},
create: {
name: 'Mitarbeiter',
description: 'Kann Kunden und Verträge verwalten',
permissions: {
create: employeePermIds.map((id) => ({ permissionId: id })),
},
},
});
await syncRolePermissions(employeeRole.id, employeePermIds);
// 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)' },
update: {},
create: {
name: 'Mitarbeiter (Nur-Lesen)',
description: 'Kann nur lesen, keine Änderungen',
permissions: {
create: readOnlyPermIds.map((id) => ({ permissionId: id })),
},
},
});
await syncRolePermissions(readOnlyRole.id, readOnlyPermIds);
// Customer role - read own data only (handled in middleware)
const customerRole = await prisma.role.upsert({
where: { name: 'Kunde' },
update: {},
create: {
name: 'Kunde',
description: 'Kann nur eigene Daten lesen',
permissions: {
create: readOnlyPermIds.map((id) => ({ permissionId: id })),
},
},
});
await syncRolePermissions(customerRole.id, readOnlyPermIds);
console.log('Roles created');
// Admin-User anlegen. Standard-Passwort darf NIEMALS in der Source-Repo
// landen (Pentest Runde 12: "admin" verletzt die eigene 12-Zeichen-
// Komplexitätspolicy). Stattdessen:
// - SEED_ADMIN_PASSWORD-ENV → wird verwendet (z.B. via docker-compose env)
// - sonst → zufälliges 16-Zeichen-Passwort, wird ein einziges Mal beim
// Seed in stdout ausgegeben. Wer das Log nicht sieht, muss
// Passwort-vergessen-Flow nutzen.
// Hash-Cost: 12 (OWASP 2026), nicht mehr 10.
function generateInitialPassword(): string {
const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
const lower = 'abcdefghijkmnopqrstuvwxyz';
const digits = '23456789';
const special = '!@#$%&*+=?';
const all = upper + lower + digits + special;
// Kryptografisch sichere Auswahl Math.random() ist vorhersagbar
// und reicht für ein Initial-Admin-Passwort nicht (Pentest 2026-05-20).
const pick = (s: string) => s[crypto.randomInt(0, s.length)];
// mind. einen aus jeder Klasse + Rest zufällig
const chars = [pick(upper), pick(lower), pick(digits), pick(special)];
// 28 Zeichen → Komplexität + komfortable Marge über dem 25-Zeichen-
// Mitarbeiter-Schwellwert (Pentest Runde 13).
for (let i = chars.length; i < 28; i++) chars.push(pick(all));
// Fisher-Yates Shuffle mit kryptografisch starkem Random.
for (let i = chars.length - 1; i > 0; i--) {
const j = crypto.randomInt(0, i + 1);
[chars[i], chars[j]] = [chars[j], chars[i]];
}
return chars.join('');
}
const envPassword = process.env.SEED_ADMIN_PASSWORD;
const adminPlainPassword = envPassword && envPassword.length >= 25
? envPassword
: generateInitialPassword();
const hashedPassword = await bcrypt.hash(adminPlainPassword, 12);
const adminUser = await prisma.user.upsert({
where: { email: 'admin@admin.com' },
update: {},
create: {
email: 'admin@admin.com',
password: hashedPassword,
firstName: 'Admin',
lastName: 'User',
roles: {
create: [{ roleId: adminRole.id }],
},
},
});
console.log('========================================================');
console.log(' Admin-User: admin@admin.com');
if (envPassword && envPassword.length >= 25) {
console.log(' Passwort: aus SEED_ADMIN_PASSWORD');
} else {
if (envPassword && envPassword.length < 25) {
console.log(' ⚠️ SEED_ADMIN_PASSWORD < 25 Zeichen, wird ignoriert!');
}
console.log(` Initial-Passwort: ${adminPlainPassword}`);
console.log(' ⚠️ Dieses Passwort wird hier EINMAL ausgegeben!');
console.log(' Bitte sofort nach dem ersten Login ändern.');
}
console.log('========================================================');
// Create some sales platforms
const platforms = ['Moon Fachhandel', 'Verivox', 'Check24', 'Eigenvermittlung'];
for (const name of platforms) {
await prisma.salesPlatform.upsert({
where: { name },
update: {},
create: { name, isActive: true },
});
}
console.log('Sales platforms created');
// ==================== STANDARD PROVIDERS ====================
const providers = [
{
name: 'Vodafone',
portalUrl: 'https://www.vodafone.de/meinvodafone/account/login',
usernameFieldName: 'username',
passwordFieldName: 'password',
},
{
name: 'Klarmobil',
portalUrl: 'https://www.klarmobil.de/login',
usernameFieldName: 'username',
passwordFieldName: 'password',
},
{
name: 'Otelo',
portalUrl: 'https://www.otelo.de/mein-otelo/login',
usernameFieldName: 'username',
passwordFieldName: 'password',
},
{
name: 'Congstar',
portalUrl: 'https://www.congstar.de/login/',
usernameFieldName: 'username',
passwordFieldName: 'password',
},
{
name: 'Telekom',
portalUrl: 'https://www.telekom.de/kundencenter/startseite',
usernameFieldName: 'username',
passwordFieldName: 'password',
},
{
name: 'O2',
portalUrl: 'https://www.o2online.de/ecare/selfcare',
usernameFieldName: 'username',
passwordFieldName: 'password',
},
{
name: '1&1',
portalUrl: 'https://control-center.1und1.de/',
usernameFieldName: 'username',
passwordFieldName: 'password',
},
];
for (const provider of providers) {
await prisma.provider.upsert({
where: { name: provider.name },
update: {
portalUrl: provider.portalUrl,
usernameFieldName: provider.usernameFieldName,
passwordFieldName: provider.passwordFieldName,
},
create: { ...provider, isActive: true },
});
}
console.log('Providers created');
// Create contract categories (matching existing enum values)
const contractCategories = [
{ code: 'ELECTRICITY', name: 'Strom', icon: 'Zap', color: '#FFC107', sortOrder: 1 },
{ code: 'GAS', name: 'Gas', icon: 'Flame', color: '#FF5722', sortOrder: 2 },
{ code: 'DSL', name: 'DSL', icon: 'Wifi', color: '#2196F3', sortOrder: 3 },
{ code: 'FIBER', name: 'Glasfaser', icon: 'Cable', color: '#9C27B0', sortOrder: 4 },
{ code: 'CABLE', name: 'Kabel Internet (Coax)', icon: 'Cable', color: '#00BCD4', sortOrder: 5 },
{ code: 'MOBILE', name: 'Mobilfunk', icon: 'Smartphone', color: '#4CAF50', sortOrder: 6 },
{ code: 'TV', name: 'TV', icon: 'Tv', color: '#E91E63', sortOrder: 7 },
{ code: 'CAR_INSURANCE', name: 'KFZ-Versicherung', icon: 'Car', color: '#607D8B', sortOrder: 8 },
];
for (const category of contractCategories) {
await prisma.contractCategory.upsert({
where: { code: category.code },
update: { name: category.name, icon: category.icon, color: category.color, sortOrder: category.sortOrder },
create: category,
});
}
console.log('Contract categories created');
// ==================== CANCELLATION PERIODS ====================
const cancellationPeriods = [
{ code: '14D', description: '14 Tage' },
{ code: '1M', description: '1 Monat' },
{ code: '2M', description: '2 Monate' },
{ code: '3M', description: '3 Monate' },
{ code: '6M', description: '6 Monate' },
{ code: '12M', description: '12 Monate' },
{ code: '1W', description: '1 Woche' },
{ code: '2W', description: '2 Wochen' },
{ code: '4W', description: '4 Wochen' },
{ code: '6W', description: '6 Wochen' },
];
for (const period of cancellationPeriods) {
await prisma.cancellationPeriod.upsert({
where: { code: period.code },
update: { description: period.description },
create: period,
});
}
console.log('Cancellation periods created');
// ==================== CONTRACT DURATIONS ====================
const contractDurations = [
{ code: '1M', description: '1 Monat' },
{ code: '3M', description: '3 Monate' },
{ code: '6M', description: '6 Monate' },
{ code: '12M', description: '12 Monate' },
{ code: '24M', description: '24 Monate' },
{ code: '36M', description: '36 Monate' },
{ code: '1J', description: '1 Jahr' },
{ code: '2J', description: '2 Jahre' },
{ code: '3J', description: '3 Jahre' },
{ code: '4J', description: '4 Jahre' },
{ code: '5J', description: '5 Jahre' },
{ code: 'UNBEFRISTET', description: 'Unbefristet' },
];
for (const duration of contractDurations) {
await prisma.contractDuration.upsert({
where: { code: duration.code },
update: { description: duration.description },
create: duration,
});
}
console.log('Contract durations created');
// ==================== APP SETTINGS ====================
const appSettings = [
// Cockpit-Einstellungen (Fristen-Ampel)
{ key: 'deadlineCriticalDays', value: '14' }, // Rot: <= 14 Tage
{ key: 'deadlineWarningDays', value: '42' }, // Gelb: <= 42 Tage
{ key: 'deadlineOkDays', value: '90' }, // Grün: <= 90 Tage
// Allgemeine Einstellungen
{ key: 'companyName', value: 'OpenCRM' },
{ key: 'defaultEmailDomain', value: 'stressfrei-wechseln.de' },
];
for (const setting of appSettings) {
await prisma.appSetting.upsert({
where: { key: setting.key },
update: {}, // Bestehende Werte nicht überschreiben
create: setting,
});
}
console.log('App settings created');
// ==================== AUDIT RETENTION POLICIES (DSGVO) ====================
// Standard-Policy (ohne Sensitivity)
const existingDefault = await prisma.auditRetentionPolicy.findFirst({
where: { resourceType: '*', sensitivity: null },
});
if (!existingDefault) {
await prisma.auditRetentionPolicy.create({
data: {
resourceType: '*',
sensitivity: null,
retentionDays: 3650, // 10 Jahre
description: 'Standard-Aufbewahrungsfrist',
legalBasis: 'AO §147, HGB §257',
},
});
}
// Spezifische Policies mit Sensitivity
const specificPolicies = [
{
resourceType: 'Authentication',
sensitivity: 'CRITICAL' as const,
retentionDays: 730, // 2 Jahre
description: 'Login-Versuche und Authentifizierung',
legalBasis: 'Sicherheitsanforderungen',
},
{
resourceType: 'Customer',
sensitivity: 'HIGH' as const,
retentionDays: 3650, // 10 Jahre
description: 'Kundendaten-Zugriffe',
legalBasis: 'Steuerrecht (AO §147)',
},
{
resourceType: 'Contract',
sensitivity: 'MEDIUM' as const,
retentionDays: 3650, // 10 Jahre
description: 'Vertragsdaten-Zugriffe',
legalBasis: 'Steuerrecht (AO §147)',
},
{
resourceType: 'AppSetting',
sensitivity: 'LOW' as const,
retentionDays: 1095, // 3 Jahre
description: 'Allgemeine Einstellungen',
legalBasis: 'Verjährungsfrist (BGB §195)',
},
{
// Stiller Token-Refresh (Routine). Ohne eigene Regel fiele diese
// Kombination in die Auffangregel `*` mit 3650 Tagen das Rauschen
// wäre dann länger aufbewahrt als echte Logins (730 Tage).
resourceType: 'Authentication',
sensitivity: 'LOW' as const,
retentionDays: 90,
description: 'Routine-Auth (stiller Token-Refresh)',
legalBasis: 'Betriebsnotwendigkeit / Datenminimierung (DSGVO Art. 5)',
},
];
for (const policy of specificPolicies) {
await prisma.auditRetentionPolicy.upsert({
where: {
resourceType_sensitivity: {
resourceType: policy.resourceType,
sensitivity: policy.sensitivity,
},
},
update: {
retentionDays: policy.retentionDays,
description: policy.description,
legalBasis: policy.legalBasis,
},
create: policy,
});
}
console.log('Audit retention policies created');
// ==================== CONSENT HASH FÜR BESTEHENDE KUNDEN ====================
const customersWithoutHash = await prisma.customer.findMany({
where: { consentHash: null },
select: { id: true },
});
for (const c of customersWithoutHash) {
await prisma.customer.update({
where: { id: c.id },
data: { consentHash: crypto.randomUUID() },
});
}
if (customersWithoutHash.length > 0) {
console.log(`ConsentHash für ${customersWithoutHash.length} Kunden generiert`);
}
console.log('Seeding completed!');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});