/** * Bringt Rechtekatalog und Systemrollen in der Datenbank auf den Stand des * Codes. Idempotent - laeuft bei jedem Container-Start. * * Verbraucher: `prisma/sync-roles.ts` (Container-Start), `prisma/seed.ts` * (Erstinstallation) und `factoryReset` in `backup.service.ts`. Alle drei * benutzen dieselbe Definition aus `config/rechte-katalog.ts`; vorher hatte * jeder seine eigene, und die dritte wich ab. * * Was hier NICHT passiert: Stammdaten, Benutzer, Vertraege. Das Skript ist * auf einer laufenden Produktionsdatenbank sicher. */ import type { PrismaClient } from '@prisma/client'; import { RECHTE_KATALOG, SYSTEMROLLEN, alsRechtString, } from '../config/rechte-katalog.js'; type Protokoll = (zeile: string) => void; /** * Setzt die Rechte einer Rolle exakt auf `permissionIds` - fehlende kommen * dazu, ueberzaehlige fliegen raus. * * Der Vollersatz ist Absicht: Er ist der Grund, warum eine per Adminer an * einer Systemrolle vorgenommene Aenderung den naechsten Container-Start * nicht ueberlebt. */ async function synchronisiereRollenrechte( prisma: PrismaClient, roleId: number, permissionIds: number[], log: Protokoll, ): Promise { const vorhanden = await prisma.rolePermission.findMany({ where: { roleId }, select: { permissionId: true }, }); const vorhandenIds = new Set(vorhanden.map((e) => e.permissionId)); const zielIds = new Set(permissionIds); const fehlend = permissionIds.filter((id) => !vorhandenIds.has(id)); if (fehlend.length > 0) { await prisma.rolePermission.createMany({ data: fehlend.map((permissionId) => ({ roleId, permissionId })), skipDuplicates: true, }); log(` → +${fehlend.length} Rechte an Rolle #${roleId}`); } const ueberzaehlig = vorhanden .filter((e) => !zielIds.has(e.permissionId)) .map((e) => e.permissionId); if (ueberzaehlig.length > 0) { await prisma.rolePermission.deleteMany({ where: { roleId, permissionId: { in: ueberzaehlig } }, }); log(` → -${ueberzaehlig.length} Rechte von Rolle #${roleId}`); } } /** * Legt alle Rechte aus dem Katalog an und bringt die Systemrollen auf Stand. * Setzt dabei auch `isSystem` und `isHidden` - das ist die eigentliche * Absicherung gegen eine von Hand verstellte Datenbank, die Migration setzt * die Flags nur einmalig. */ export async function synchronisiereRechteUndRollen( prisma: PrismaClient, log: Protokoll = (z) => console.log(z), ): Promise { log('[rollen-sync] Rechtekatalog upserten…'); for (const recht of RECHTE_KATALOG) { await prisma.permission.upsert({ where: { resource_action: { resource: recht.resource, action: recht.action } }, update: {}, create: { resource: recht.resource, action: recht.action }, }); } const alleRechte = await prisma.permission.findMany(); log(`[rollen-sync] ${alleRechte.length} Rechte in der Datenbank`); // Auflösung Katalog → Datenbank-IDs. Rechte, die in der Datenbank stehen, // aber nicht im Katalog, bleiben unangetastet und werden auch keiner Rolle // zugeteilt: Der Katalog ist die Wahrheit, nicht der Altbestand. const idFuerRecht = new Map(); for (const p of alleRechte) idFuerRecht.set(alsRechtString(p), p.id); for (const spec of SYSTEMROLLEN) { const rechteIds = RECHTE_KATALOG.filter(spec.rechte) .map((r) => idFuerRecht.get(alsRechtString(r))) .filter((id): id is number => id !== undefined); const rolle = await prisma.role.upsert({ where: { name: spec.name }, update: { description: spec.description, isSystem: true, isHidden: spec.isHidden, }, create: { name: spec.name, description: spec.description, isSystem: true, isHidden: spec.isHidden, }, }); await synchronisiereRollenrechte(prisma, rolle.id, rechteIds, log); } log('[rollen-sync] fertig.'); }