- App-Shell mit Seitennavigation (alle 12 Module, kommende ausgegraut), shadcn/ui-Setup (Base-UI-Variante) mit hellem Theme - Asset-Inventar: filterbare Liste (Suche/Typ/Status), Anlegen/Bearbeiten/ Löschen, Detailansicht mit Schutzbedarf (C/I/A 1–4), Abhängigkeiten (beide Richtungen, hinzufügen/entfernen), zugeordneten Prozessen mit Primär-/Sekundär-Rolle und Platzhalter für zugeordnete Risiken - Prozesse & BIA: Liste mit Kritikalität/RTO/MTD, Prozess-Detail mit Asset-Zuordnung nach Rolle (primär/sekundär), BIA-Formular (RTO/RPO/MTD, Schadenshöhe je Schutzziel, Kritikalität nach Max-Prinzip) - Server-Actions mit Zod-Validierung, requirePermission und Audit-Log für jede schreibende Aktion; Tenant-Guard um upsert-Injektion erweitert - Prisma: Asset, AssetRelation, Process, ProcessAsset, BiaEntry inkl. RLS-Policies; Seed mit Beispiel-Assets, Prozessen und BIA - Im Browser verifiziert: CRUD, Relationen, BIA-Speichern, Audit-Einträge Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
207 lines
7.1 KiB
TypeScript
207 lines
7.1 KiB
TypeScript
import "dotenv/config";
|
|
import { PrismaClient } from "@prisma/client";
|
|
import { PrismaPg } from "@prisma/adapter-pg";
|
|
import { hash } from "@node-rs/argon2";
|
|
import { PERMISSIONS, ROLE_DEFS } from "../src/server/rbac";
|
|
|
|
/**
|
|
* 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!";
|
|
|
|
async function main() {
|
|
// 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.",
|
|
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.",
|
|
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");
|
|
}
|
|
}
|
|
|
|
main()
|
|
.then(() => prisma.$disconnect())
|
|
.catch(async (e) => {
|
|
console.error(e);
|
|
await prisma.$disconnect();
|
|
process.exit(1);
|
|
});
|