/** * Bestandsaufnahme des Rechtesystems. Liest nur, aendert nichts. * * npx tsx prisma/rechte-report.ts → Bericht auf stdout * npx tsx prisma/rechte-report.ts > vorher.txt * * Zweck: Vor und nach dem Rechte-Umbau denselben Bericht erzeugen und * vergleichen. Die Zusage lautet "niemand verliert Zugriff" - und eine * Zusage, die niemand nachrechnen kann, ist keine. * * Der Bericht ist bewusst zeilenweise und sortiert, damit `diff` darauf * etwas Lesbares ausgibt. */ // Ueber src/lib/prisma.js, NICHT ueber einen eigenen PrismaClient: Dort wird // DATABASE_URL aus den DB_*-Teilen zusammengesetzt, falls sie fehlt. Bei // `docker compose exec npx tsx prisma/.ts` ist genau das der // Fall - der Entrypoint exportiert sie nur in den Serverprozess, eine neue // Shell erbt sie nicht. Ein eigener Client scheitert dort mit // "Environment variable not found: DATABASE_URL". import prisma from '../src/lib/prisma.js'; import { ALLE_RECHTE, SYSTEMROLLEN_NAMEN } from '../src/config/rechte-katalog.js'; /** * Liest isSystem/isHidden, sofern die Spalten existieren. * * Der Bericht muss VOR und NACH der Migration laufen - das ist sein Zweck. * Vorher gibt es die Spalten noch nicht, und ein Absturz an dieser Stelle * haette genau die Baseline verhindert, gegen die spaeter verglichen wird. */ async function leseKennzeichen(): Promise> { const karte = new Map(); try { const zeilen = await prisma.$queryRawUnsafe< Array<{ id: number; isSystem: number | boolean; isHidden: number | boolean }> >('SELECT id, isSystem, isHidden FROM `Role`'); for (const z of zeilen) { karte.set(Number(z.id), { isSystem: Boolean(z.isSystem), isHidden: Boolean(z.isHidden) }); } } catch { // Spalten noch nicht vorhanden - das ist der Zustand vor der Migration. } return karte; } async function main(): Promise { const kennzeichen = await leseKennzeichen(); const rollen = await prisma.role.findMany({ orderBy: { name: 'asc' }, select: { id: true, name: true, permissions: { include: { permission: true } }, _count: { select: { users: true } }, }, }); console.log('=== ROLLEN ==='); for (const r of rollen) { const rechte = r.permissions .map((rp) => `${rp.permission.resource}:${rp.permission.action}`) .sort(); const k = kennzeichen.get(r.id); const etikett = [k?.isSystem ? 'System' : null, k?.isHidden ? 'versteckt' : null] .filter(Boolean) .join(', '); console.log( `\n[${r.name}] #${r.id}` + (etikett ? ` (${etikett})` : '') + ` – ${r._count.users} Konten, ${rechte.length} Rechte`, ); for (const recht of rechte) console.log(` ${recht}`); } console.log('\n=== KONTEN (aktiv) ==='); const konten = await prisma.user.findMany({ where: { isActive: true }, orderBy: { email: 'asc' }, select: { email: true, roles: { select: { role: { // Ausdrueckliche Spaltenauswahl, damit der Bericht auch auf einer // noch nicht migrierten Datenbank laeuft. select: { name: true, permissions: { include: { permission: true } }, }, }, }, }, }, }); for (const k of konten) { const rechte = new Set(); for (const ur of k.roles) { for (const rp of ur.role.permissions) { rechte.add(`${rp.permission.resource}:${rp.permission.action}`); } } const rollenNamen = k.roles.map((ur) => ur.role.name).sort().join(', ') || '(keine)'; console.log(`\n[${k.email}] Rollen: ${rollenNamen} – ${rechte.size} Rechte`); for (const recht of [...rechte].sort()) console.log(` ${recht}`); } // --- Auffaelligkeiten ----------------------------------------------------- // Nicht als Alarm gemeint, sondern als das, was man vor einem Deploy // wissen will. Eine umbenannte Systemrolle etwa wird vom Backfill der // Migration nicht getroffen. console.log('\n=== HINWEISE ==='); const hinweise: string[] = []; const vorhandeneNamen = new Set(rollen.map((r) => r.name)); for (const erwartet of SYSTEMROLLEN_NAMEN) { if (!vorhandeneNamen.has(erwartet)) { hinweise.push(`Systemrolle "${erwartet}" fehlt in der Datenbank.`); } } for (const r of rollen) { const k = kennzeichen.get(r.id); // Vor der Migration gibt es die Spalte nicht - dann ist "nicht // gekennzeichnet" kein Befund, sondern der erwartete Zustand. if (kennzeichen.size === 0) continue; if (SYSTEMROLLEN_NAMEN.includes(r.name) && !k?.isSystem) { hinweise.push(`Rolle "${r.name}" ist eine Systemrolle, aber isSystem ist nicht gesetzt.`); } if (!SYSTEMROLLEN_NAMEN.includes(r.name) && k?.isSystem) { hinweise.push(`Rolle "${r.name}" traegt isSystem, gehoert aber nicht zum Katalog.`); } } const alleRechteDb = await prisma.permission.findMany(); const imKatalog = new Set(ALLE_RECHTE); for (const p of alleRechteDb) { const s = `${p.resource}:${p.action}`; if (!imKatalog.has(s)) hinweise.push(`Recht "${s}" steht in der Datenbank, aber nicht im Katalog.`); } const inDb = new Set(alleRechteDb.map((p) => `${p.resource}:${p.action}`)); for (const s of ALLE_RECHTE) { if (!inDb.has(s)) hinweise.push(`Recht "${s}" steht im Katalog, aber nicht in der Datenbank.`); } // Konten ohne jede Rolle koennen sich anmelden und sehen nichts - eine // haeufige Ursache fuer "bei mir ist alles leer". for (const k of konten) { if (k.roles.length === 0) hinweise.push(`Konto "${k.email}" hat keine Rolle.`); } if (hinweise.length === 0) console.log(' keine'); else for (const h of hinweise) console.log(` ! ${h}`); } main() .catch((e) => { console.error('[rechte-report] Fehler:', e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });