Files
certvia/prisma/seed.ts
T
msolarczekandClaude Opus 4.8 8e4040d1da Separater Superadmin-Store + eigener Login + MFA-Pflicht (Phase-1-Härtung Paket 2)
Plattform-Administratoren sind nicht länger isPlatformAdmin-Nutzer innerhalb eines
Mandanten, sondern ein getrennter Store mit eigener Auth-Domäne — Voraussetzung
für den sicheren Betrieb beim ersten echten Kunden.

Store & Migration
- Neues Modell PlatformAdmin (kein tenant_id): Argon2id-Hash, TOTP-Secret,
  Recovery-Codes (nur SHA-256-Hashes), Fehlversuchszähler + Sperre, lastLogin.
- AuditLog.tenant_id nullable → mandantenlose Plattform-Ereignisse (scope=platform).
- Datenmigration: bestehende isPlatformAdmin-Nutzer in den neuen Store übernommen
  (gleicher Hash → Login sofort möglich), Flag mandantenweit auf false gesetzt.

Getrennter Login + MFA
- Zweite NextAuth-Instanz (server/platform-auth.ts) mit eigenem Cookie und
  eigenem basePath /api/platform-auth; Session trägt bewusst KEINEN tenantId.
- TOTP-MFA (otplib): Enrollment beim ersten Login (/platform/enroll-mfa, QR +
  Klartext-Secret), danach bei jedem Login erzwungen; 10 einmalige Recovery-Codes.
- Härtung: Konto-Sperre nach 5 Fehlversuchen (15 min), Audit aller Anmeldungen,
  Fehlversuche und Sperren (scope=platform).

Autorisierung / Trennung
- Admin-Konsole nach (platform)/admin verschoben; (platform)/layout.tsx erzwingt
  Plattform-Session + aktivierte MFA. Mandanten-Session hat KEINEN Zugriff auf /admin.
- Mandanten-Shell zeigt keinen /admin-Link mehr; admin-Actions prüfen die
  Plattform-Session statt des abgelösten Flags.
- provision/seed setzen isPlatformAdmin nicht mehr; Seed legt den Demo-Plattform-
  Admin (admin@demo.example) im getrennten Store an.

Browser-verifiziert: Plattform-Login → erzwungenes MFA-Enrollment → Recovery-Codes →
/admin; Login ohne Code scheitert (?error=1); Mandanten-Session auf /admin wird auf
/platform/login umgeleitet. tsc + lint + build + Guard-Check grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:55:12 +02:00

574 lines
23 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 "dotenv/config";
import { PrismaClient } from "@prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import { hash } from "@node-rs/argon2";
import { join } from "node:path";
import { PERMISSIONS, ROLE_DEFS } from "../src/server/rbac";
import { importPolicies } from "./import-policies";
import { importManaged } from "./import-managed";
import { MODULE_KEYS } from "../src/lib/modules";
/**
* Seed: global permission catalog + demo tenant with roles and users.
* Idempotent (upserts) — safe to run repeatedly.
*/
const prisma = new PrismaClient({
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
});
const DEMO_PASSWORD = "Demo1234!";
const THREATS = [
"Schadsoftware / Ransomware",
"Phishing / Social Engineering",
"Innentäter / Missbrauch von Berechtigungen",
"Diebstahl oder Verlust von Geräten",
"Ausfall von IT-Systemen oder Diensten",
"Ausfall eines Dienstleisters / Lieferanten",
"Stromausfall / Infrastrukturausfall",
"Feuer / Wasser / Elementarschäden",
"Unbefugter physischer Zutritt",
"Denial-of-Service-Angriff",
"Datenabfluss / Industriespionage",
"Fehlbedienung durch Mitarbeitende",
"Softwarefehler / fehlerhafte Updates",
"Kompromittierte Zugangsdaten",
"Rechtliche / regulatorische Verstöße",
];
const VULNERABILITIES = [
"Fehlende oder veraltete Backups",
"Fehlende Netzwerksegmentierung",
"Unzureichendes Patch-Management",
"Schwache oder wiederverwendete Passwörter",
"Fehlende Multi-Faktor-Authentifizierung",
"Übermäßige Berechtigungen / fehlendes Least-Privilege",
"Fehlende Awareness / Schulungen",
"Unverschlüsselte Datenträger oder Übertragungen",
"Kein Notfallkonzept / ungetesteter Wiederanlauf",
"Single Point of Failure (Technik oder Person)",
"Unzureichende Protokollierung / Überwachung",
"Veraltete oder nicht mehr unterstützte Software",
"Fehlende Vertrags-/AV-Regelungen mit Dienstleistern",
"Offene, ungenutzte Dienste und Ports",
"Unklare Verantwortlichkeiten",
];
async function main() {
// 0. Globale Kataloge Bedrohungen/Schwachstellen (Vorschläge, Freitext bleibt möglich)
for (const name of THREATS) {
await prisma.threat.upsert({ where: { name }, update: {}, create: { name } });
}
for (const name of VULNERABILITIES) {
await prisma.vulnerability.upsert({ where: { name }, update: {}, create: { name } });
}
console.log(`✔ Kataloge: ${THREATS.length} Bedrohungen, ${VULNERABILITIES.length} Schwachstellen`);
// 1. Global permission catalog
for (const key of PERMISSIONS) {
await prisma.permission.upsert({ where: { key }, update: {}, create: { key } });
}
console.log(`✔ ${PERMISSIONS.length} Permissions`);
// 2. Demo tenant
const tenant = await prisma.tenant.upsert({
where: { slug: "demo" },
update: {},
create: { name: "Demo GmbH", slug: "demo" },
});
// 3. Roles from blueprints, with permission bundles
for (const [key, def] of Object.entries(ROLE_DEFS)) {
const role = await prisma.role.upsert({
where: { tenantId_key: { tenantId: tenant.id, key } },
update: { name: def.name },
create: { tenantId: tenant.id, key, name: def.name },
});
const perms = await prisma.permission.findMany({
where: { key: { in: [...def.permissions] } },
});
for (const p of perms) {
await prisma.rolePermission.upsert({
where: { roleId_permissionId: { roleId: role.id, permissionId: p.id } },
update: {},
create: { roleId: role.id, permissionId: p.id },
});
}
}
console.log(`✔ Mandant "demo" mit ${Object.keys(ROLE_DEFS).length} Rollen`);
// 4. Demo users
const passwordHash = await hash(DEMO_PASSWORD);
const demoUsers: { email: string; name: string; roles: string[] }[] = [
{ email: "admin@demo.example", name: "Anna Admin", roles: ["tenant-admin", "isb"] },
{ email: "auditor@demo.example", name: "Axel Auditor", roles: ["auditor"] },
{ email: "owner@demo.example", name: "Oskar Owner", roles: ["owner"] },
{ email: "user@demo.example", name: "Ulla User", roles: ["user"] },
];
for (const u of demoUsers) {
const user = await prisma.user.upsert({
where: { tenantId_email: { tenantId: tenant.id, email: u.email } },
update: {},
create: {
tenantId: tenant.id,
email: u.email,
name: u.name,
passwordHash,
},
});
const roles = await prisma.role.findMany({
where: { tenantId: tenant.id, key: { in: u.roles } },
});
for (const r of roles) {
await prisma.userRole.upsert({
where: { userId_roleId: { userId: user.id, roleId: r.id } },
update: {},
create: { userId: user.id, roleId: r.id },
});
}
}
console.log(`✔ ${demoUsers.length} Demo-Nutzer (Passwort: ${DEMO_PASSWORD})`);
// 5. Beispiel-Assets, -Prozesse und BIA (nur wenn noch keine Assets existieren)
const assetCount = await prisma.asset.count({ where: { tenantId: tenant.id } });
if (assetCount === 0) {
const isb = await prisma.user.findUniqueOrThrow({
where: { tenantId_email: { tenantId: tenant.id, email: "admin@demo.example" } },
});
const owner = await prisma.user.findUniqueOrThrow({
where: { tenantId_email: { tenantId: tenant.id, email: "owner@demo.example" } },
});
const mk = (data: {
name: string;
type: "INFORMATION" | "SYSTEM" | "APPLICATION" | "LOCATION" | "SUPPLIER" | "PERSON" | "DATA";
c: number;
i: number;
a: number;
ownerId?: string;
location?: string;
tags?: string[];
description?: string;
}) =>
prisma.asset.create({
data: {
tenantId: tenant.id,
name: data.name,
type: data.type,
confidentiality: data.c,
integrity: data.i,
availability: data.a,
ownerId: data.ownerId,
location: data.location,
tags: data.tags ?? [],
description: data.description,
},
});
const erp = await mk({ name: "ERP-System", type: "SYSTEM", c: 3, i: 4, a: 3, ownerId: owner.id, location: "RZ Frankfurt", tags: ["kritisch", "kern"] });
const crm = await mk({ name: "Kundendatenbank", type: "DATA", c: 4, i: 3, a: 2, ownerId: isb.id, tags: ["dsgvo"] });
const auftraege = await mk({ name: "Auftragsdaten", type: "INFORMATION", c: 3, i: 4, a: 3, ownerId: owner.id });
const hoster = await mk({ name: "Cloud-Hoster (IaaS)", type: "SUPPLIER", c: 2, i: 3, a: 4, description: "Betreibt das Rechenzentrum für ERP und CRM." });
const buero = await mk({ name: "Bürostandort München", type: "LOCATION", c: 2, i: 2, a: 2 });
const itTeam = await mk({ name: "IT-Administration", type: "PERSON", c: 3, i: 3, a: 3 });
await prisma.assetRelation.createMany({
data: [
{ tenantId: tenant.id, assetId: erp.id, relatedAssetId: hoster.id, type: "depends_on" },
{ tenantId: tenant.id, assetId: crm.id, relatedAssetId: hoster.id, type: "depends_on" },
{ tenantId: tenant.id, assetId: erp.id, relatedAssetId: itTeam.id, type: "depends_on" },
],
});
const auftrag = await prisma.process.create({
data: {
tenantId: tenant.id,
name: "Auftragsabwicklung",
description: "Vom Kundenauftrag bis zur Auslieferung.",
category: "CORE",
ownerId: owner.id,
},
});
await prisma.processAsset.createMany({
data: [
{ tenantId: tenant.id, processId: auftrag.id, assetId: auftraege.id, role: "PRIMARY" },
{ tenantId: tenant.id, processId: auftrag.id, assetId: erp.id, role: "SECONDARY" },
{ tenantId: tenant.id, processId: auftrag.id, assetId: crm.id, role: "SECONDARY" },
{ tenantId: tenant.id, processId: auftrag.id, assetId: itTeam.id, role: "SECONDARY" },
],
});
await prisma.biaEntry.create({
data: {
tenantId: tenant.id,
processId: auftrag.id,
rtoHours: 8,
rpoHours: 4,
mtdHours: 24,
impactC: 2,
impactI: 4,
impactA: 3,
criticality: 4,
notes: "Ausfall > 1 Tag führt zu Lieferverzug und Vertragsstrafen.",
},
});
const vertrieb = await prisma.process.create({
data: {
tenantId: tenant.id,
name: "Kundenbetreuung",
description: "Anfragen, Angebote, Support.",
category: "SUPPORT",
ownerId: isb.id,
},
});
await prisma.processAsset.createMany({
data: [
{ tenantId: tenant.id, processId: vertrieb.id, assetId: crm.id, role: "PRIMARY" },
{ tenantId: tenant.id, processId: vertrieb.id, assetId: buero.id, role: "SECONDARY" },
],
});
await prisma.biaEntry.create({
data: {
tenantId: tenant.id,
processId: vertrieb.id,
rtoHours: 24,
rpoHours: 24,
mtdHours: 72,
impactC: 3,
impactI: 2,
impactA: 2,
criticality: 3,
notes: "Kundenkommunikation kann kurzfristig über Ausweichkanäle laufen.",
},
});
console.log("✔ Beispiel-Assets, -Prozesse und BIA angelegt");
}
// 6. Beispiel-Risiken (nur wenn noch keine existieren)
const riskCount = await prisma.risk.count({ where: { tenantId: tenant.id } });
if (riskCount === 0) {
const isb = await prisma.user.findUniqueOrThrow({
where: { tenantId_email: { tenantId: tenant.id, email: "admin@demo.example" } },
});
const byName = async (name: string) =>
prisma.asset.findFirstOrThrow({ where: { tenantId: tenant.id, name } });
const erp = await byName("ERP-System");
const crm = await byName("Kundendatenbank");
const hoster = await byName("Cloud-Hoster (IaaS)");
const auftrag = await prisma.process.findFirstOrThrow({
where: { tenantId: tenant.id, name: "Auftragsabwicklung" },
});
const risks: {
title: string;
threat: string;
vulnerability: string;
likelihood: number;
impact: number;
residual?: [number, number];
treatment: "AVOID" | "MITIGATE" | "TRANSFER" | "ACCEPT";
status: "OPEN" | "IN_TREATMENT" | "ACCEPTED" | "CLOSED";
assets: string[];
processId?: string;
description?: string;
}[] = [
{
title: "Ransomware auf ERP",
threat: "Schadsoftware / Verschlüsselungstrojaner",
vulnerability: "Fehlende Offline-Backups, Makro-Ausführung erlaubt",
likelihood: 4,
impact: 5,
residual: [2, 4],
treatment: "MITIGATE",
status: "IN_TREATMENT",
assets: [erp.id, crm.id],
processId: auftrag.id,
description: "Verschlüsselung der ERP-Datenbank würde die Auftragsabwicklung stoppen.",
},
{
title: "Ausfall Cloud-Hoster",
threat: "Ausfall des Rechenzentrums / Insolvenz Dienstleister",
vulnerability: "Kein Ausweich-Standort, Single Provider",
likelihood: 4,
impact: 4,
treatment: "TRANSFER",
status: "OPEN",
assets: [hoster.id, erp.id],
processId: auftrag.id,
},
{
title: "Phishing / Social Engineering",
threat: "Gezielte Phishing-Kampagnen",
vulnerability: "Fehlende Awareness-Schulungen",
likelihood: 3,
impact: 3,
residual: [2, 3],
treatment: "MITIGATE",
status: "IN_TREATMENT",
assets: [crm.id],
},
{
title: "Verlust mobiler Geräte",
threat: "Diebstahl/Verlust von Notebooks",
vulnerability: "Unvollständige Festplattenverschlüsselung",
likelihood: 2,
impact: 3,
treatment: "ACCEPT",
status: "ACCEPTED",
assets: [],
},
];
let refNo = 1;
for (const r of risks) {
const risk = await prisma.risk.create({
data: {
tenantId: tenant.id,
refNo: refNo++,
title: r.title,
description: r.description,
threat: r.threat,
vulnerability: r.vulnerability,
likelihood: r.likelihood,
impact: r.impact,
score: r.likelihood * r.impact,
residualLikelihood: r.residual?.[0],
residualImpact: r.residual?.[1],
residualScore: r.residual ? r.residual[0] * r.residual[1] : undefined,
treatment: r.treatment,
status: r.status,
ownerId: isb.id,
processId: r.processId,
},
});
for (const assetId of r.assets) {
await prisma.riskAsset.create({
data: { tenantId: tenant.id, riskId: risk.id, assetId },
});
}
}
console.log(`✔ ${risks.length} Beispiel-Risiken angelegt`);
}
// 7. Beispiel-Maßnahmen (nur wenn noch keine existieren) — bestimmen das Rest-Risiko
const measureCount = await prisma.measure.count({ where: { tenantId: tenant.id } });
if (measureCount === 0) {
const owner = await prisma.user.findUniqueOrThrow({
where: { tenantId_email: { tenantId: tenant.id, email: "owner@demo.example" } },
});
const riskByTitle = (title: string) =>
prisma.risk.findFirst({ where: { tenantId: tenant.id, title } });
const measures: {
title: string;
status: "OPEN" | "IN_PROGRESS" | "DONE";
priority: "LOW" | "MEDIUM" | "HIGH";
dueInDays?: number;
riskTitle?: string;
reduction?: [number, number]; // [Wahrscheinlichkeit, Schaden]
}[] = [
{
title: "Offline-Backups einführen (3-2-1-Regel)",
status: "IN_PROGRESS",
priority: "HIGH",
dueInDays: 14,
riskTitle: "Ransomware auf ERP",
reduction: [1, 1],
},
{
title: "Makro-Ausführung per GPO einschränken",
status: "OPEN",
priority: "HIGH",
dueInDays: 30,
riskTitle: "Ransomware auf ERP",
reduction: [1, 0],
},
{
title: "Awareness-Schulung Phishing (alle Mitarbeitenden)",
status: "IN_PROGRESS",
priority: "MEDIUM",
dueInDays: 45,
riskTitle: "Phishing / Social Engineering",
reduction: [1, 0],
},
{
title: "Notfallhandbuch aktualisieren",
status: "DONE",
priority: "MEDIUM",
},
];
let mRef = 1;
for (const m of measures) {
const measure = await prisma.measure.create({
data: {
tenantId: tenant.id,
refNo: mRef++,
title: m.title,
status: m.status,
priority: m.priority,
ownerId: owner.id,
dueDate: m.dueInDays
? new Date(Date.now() + m.dueInDays * 24 * 3600 * 1000)
: undefined,
},
});
if (m.riskTitle && m.reduction) {
const risk = await riskByTitle(m.riskTitle);
if (risk) {
await prisma.riskMeasure.create({
data: {
tenantId: tenant.id,
riskId: risk.id,
measureId: measure.id,
reductionLikelihood: m.reduction[0],
reductionImpact: m.reduction[1],
},
});
}
}
}
// Rest-Risiken aus den Maßnahmen ableiten (manuelle Alt-Werte ersetzen)
const allRisks = await prisma.risk.findMany({
where: { tenantId: tenant.id },
include: { riskMeasures: true },
});
for (const r of allRisks) {
if (r.riskMeasures.length === 0) {
await prisma.risk.update({
where: { id: r.id },
data: { residualLikelihood: null, residualImpact: null, residualScore: null },
});
} else {
const redL = r.riskMeasures.reduce((s, x) => s + x.reductionLikelihood, 0);
const redI = r.riskMeasures.reduce((s, x) => s + x.reductionImpact, 0);
const rl = Math.max(1, r.likelihood - redL);
const ri = Math.max(1, r.impact - redI);
await prisma.risk.update({
where: { id: r.id },
data: { residualLikelihood: rl, residualImpact: ri, residualScore: rl * ri },
});
}
}
console.log(`✔ ${measures.length} Beispiel-Maßnahmen angelegt, Rest-Risiken berechnet`);
}
// 8. Demo-Lieferanten als Assets (type SUPPLIER) mit Profil, Nachweisen, Verträgen
const supCount = await prisma.supplierProfile.count({ where: { tenantId: tenant.id } });
if (supCount === 0) {
const hoster = await prisma.asset.findFirst({ where: { tenantId: tenant.id, name: "Cloud-Hoster (IaaS)" } });
const a1 = await prisma.asset.create({
data: { tenantId: tenant.id, name: "Cloud-Hoster GmbH", type: "SUPPLIER", confidentiality: 3, integrity: 3, availability: 4 },
});
await prisma.supplierProfile.create({
data: { tenantId: tenant.id, assetId: a1.id, refNo: 1, sector: "IT-Dienstleistung / IaaS", serviceDesc: "Rechenzentrum, Virtualisierung, Backup", criticality: 4, dataCategories: ["Kundendaten", "Auftragsdaten"], nis2Relevant: true, lifecycle: "ACTIVE", contact: "security@cloud-hoster.example", nextReview: new Date(Date.now() + 90 * 24 * 3600 * 1000) },
});
if (hoster) await prisma.assetRelation.create({ data: { tenantId: tenant.id, assetId: a1.id, relatedAssetId: hoster.id, type: "provides" } }).catch(() => {});
await prisma.supplierEvidence.create({ data: { tenantId: tenant.id, assetId: a1.id, kind: "TISAX_LABEL", name: "TISAX AL3 (info high)", protectsCia: "C,I,A", validTo: new Date(Date.now() + 200 * 24 * 3600 * 1000), adequacyChecked: true } });
await prisma.contract.create({ data: { tenantId: tenant.id, assetId: a1.id, type: "av_dpa", avDpa: true, securityClauses: true, flowdown: true, customerRequirementsPassed: true, validTo: new Date(Date.now() + 400 * 24 * 3600 * 1000), reference: "AV-2025-014" } });
await prisma.nda.create({ data: { tenantId: tenant.id, assetId: a1.id, subject: "Betrieb der ERP-/CRM-Infrastruktur", parties: "Demo GmbH / Cloud-Hoster GmbH", validTo: new Date(Date.now() + 60 * 24 * 3600 * 1000), extensionStatus: "offen" } });
await prisma.supplierAssessment.create({ data: { tenantId: tenant.id, assetId: a1.id, type: "SELF_ASSESSMENT", score: 82, date: new Date(Date.now() - 120 * 24 * 3600 * 1000), nextReview: new Date(Date.now() + 245 * 24 * 3600 * 1000), result: "angemessen" } });
const a2 = await prisma.asset.create({
data: { tenantId: tenant.id, name: "WebAgentur X", type: "SUPPLIER", confidentiality: 2, integrity: 2, availability: 2 },
});
await prisma.supplierProfile.create({
data: { tenantId: tenant.id, assetId: a2.id, refNo: 2, sector: "Webentwicklung", serviceDesc: "Website-Betrieb", criticality: 2, dataCategories: ["Marketingdaten"], nis2Relevant: false, lifecycle: "UNDER_REVIEW" },
});
console.log("✔ 2 Demo-Lieferanten als Assets angelegt");
}
// 9. Demo-IT-Service (type IT_SERVICE) mit RACI-Matrix über den ISA-Katalog
const svcCount = await prisma.iTServiceProfile.count({ where: { tenantId: tenant.id } });
if (svcCount === 0) {
const provider = await prisma.asset.findFirst({ where: { tenantId: tenant.id, name: "Cloud-Hoster GmbH", type: "SUPPLIER" } });
const svc = await prisma.asset.create({
data: { tenantId: tenant.id, name: "Managed ERP-Hosting", type: "IT_SERVICE", confidentiality: 3, integrity: 3, availability: 4 },
});
await prisma.iTServiceProfile.create({
data: { tenantId: tenant.id, assetId: svc.id, refNo: 1, criticality: 4, internal: false, providerAssetId: provider?.id ?? null, notes: "Betrieb & Wartung der ERP-Plattform beim Cloud-Hoster (Shared Responsibility)." },
});
const raci: { controlRef: string; title: string; responsibility: "PROVIDER" | "US" | "SHARED"; evidenceRef?: string }[] = [
{ controlRef: "4.1.2", title: "Authentisierung / MFA", responsibility: "SHARED", evidenceRef: "IAM-Konzept v2" },
{ controlRef: "5.2.4", title: "Schwachstellen-/Patch-Management", responsibility: "PROVIDER", evidenceRef: "Patch-SLA" },
{ controlRef: "5.2.5", title: "Datensicherung / Backup", responsibility: "PROVIDER", evidenceRef: "Backup-Report Q2" },
{ controlRef: "4.1.3", title: "Zugriffsrechte-Verwaltung", responsibility: "US" },
{ controlRef: "1.6.1", title: "Informationssicherheits-Vorfälle", responsibility: "SHARED", evidenceRef: "IR-Runbook" },
];
for (const r of raci) {
await prisma.serviceControlResponsibility.create({
data: { tenantId: tenant.id, assetId: svc.id, controlRef: r.controlRef, title: r.title, applicable: true, responsibility: r.responsibility, evidenceRef: r.evidenceRef ?? null },
});
}
console.log("✔ 1 Demo-IT-Service mit RACI-Matrix angelegt");
}
// 10. Richtlinien & Verfahren aus dem VDA-ISA-2027-Vorlagenpaket importieren
const seedDir = join(__dirname, "..", "seed", "isms-vorlagenpaket-v2");
const pc = await importPolicies(prisma, tenant.id, seedDir);
// Demo-Werte für Variablen ohne Schema-Default (damit die Doku vollständig wirkt)
const demoVars: Record<string, string> = {
ORG_NAME: "GEFIM Demo GmbH",
ORG_SHORT: "GEFIM",
ISMS_SCOPE: "IT-Betrieb & Softwareentwicklung",
ISMS_SCOPE_DESCRIPTION: "IT-Betrieb, Softwareentwicklung und zugehörige Unterstützungsprozesse am Standort Zentrale",
DOC_DATE: "2026-07-07",
};
for (const [key, value] of Object.entries(demoVars)) {
await prisma.policyVariable.updateMany({ where: { tenantId: tenant.id, key }, data: { value } });
}
console.log(`✔ Richtlinienpaket importiert: ${pc.documents} Dokumente, ${pc.requirements} Anforderungen, ${pc.variables} Variablen, ${pc.baseline} Baseline-Parameter`);
const mc = await importManaged(prisma, tenant.id);
console.log(`✔ Verwaltete Register: Krypto ${mc.crypto}, Klassifizierung ${mc.classes}×${mc.aspects}, Risikomatrix (${mc.damage} Schadensdim.), Handbuch ${mc.handbook} Themen`);
// 11. Admin-Konsole: Mandanten-Einstellungen, Module, Plattform-Admin (getrennter Store)
// Demo-Plattform-Admin: gleiche Adresse wie der Mandanten-Admin, damit /login (Mandant)
// und /platform/login (Betrieb) mit denselben Demo-Zugangsdaten funktionieren. MFA wird
// beim ersten Plattform-Login eingerichtet.
await prisma.platformAdmin.upsert({
where: { email: "admin@demo.example" },
update: { name: "Anna Admin", status: "ACTIVE" },
create: { email: "admin@demo.example", name: "Anna Admin", passwordHash: await hash("Demo1234!"), status: "ACTIVE" },
});
await prisma.tenantSettings.upsert({
where: { tenantId: tenant.id },
update: {},
create: {
tenantId: tenant.id,
orgName: "GEFIM Demo GmbH",
orgShort: "GEFIM",
sector: "IT-Dienstleistung",
ismsScope: "IT-Betrieb & Softwareentwicklung",
ismsScopeDescription: "IT-Betrieb, Softwareentwicklung und zugehörige Unterstützungsprozesse am Standort Zentrale",
roleManagement: "Geschäftsführung",
roleIsb: "Informationssicherheitsbeauftragte(r) (ISB)",
roleItLead: "IT-Leitung",
roleDpo: "Datenschutzbeauftragte(r) (DSB)",
tisaxLevel: "AL2",
},
});
for (const key of MODULE_KEYS) {
await prisma.tenantModule.upsert({
where: { tenantId_moduleKey: { tenantId: tenant.id, moduleKey: key } },
update: {},
create: { tenantId: tenant.id, moduleKey: key, enabled: true },
});
}
console.log(`✔ Admin-Konsole: Einstellungen, ${MODULE_KEYS.length} Module aktiv; Plattform-Admin admin@demo.example (getrennter Login /platform/login, MFA beim ersten Login)`);
}
main()
.then(() => prisma.$disconnect())
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});