Die drei neuen Wartungsskripte bauten sich einen eigenen PrismaClient und gingen damit an src/lib/prisma.ts vorbei. Dort wird DATABASE_URL aus den DB_*-Teilen zusammengesetzt, falls sie fehlt - und genau das ist bei `docker compose exec <dienst> npx tsx prisma/<skript>.ts` der Fall: Der Entrypoint exportiert sie nur in den Serverprozess, eine neue Shell erbt sie nicht. Ergebnis war "Environment variable not found: DATABASE_URL". Der Kommentar in src/lib/prisma.ts beschreibt genau diesen Fall; die Loesung war da, ich habe sie nur umgangen. Betroffen war auch rolle-zuweisen.ts. Das ist der Weg, den die Startwache im Klartext nennt, wenn kein Konto mehr die Pflichtrechte haelt - er haette in dem Moment versagt, fuer den er gebaut ist. Geprueft mit entzogener DATABASE_URL und nur DB_*-Teilen in der Umgebung: rechte-report, rolle-zuweisen --liste und sync-roles laufen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
163 lines
5.9 KiB
TypeScript
163 lines
5.9 KiB
TypeScript
/**
|
||
* 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 <dienst> npx tsx prisma/<skript>.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<Map<number, { isSystem: boolean; isHidden: boolean }>> {
|
||
const karte = new Map<number, { isSystem: boolean; isHidden: boolean }>();
|
||
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<void> {
|
||
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<string>();
|
||
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();
|
||
});
|