Rollen+Permissions-Sync beim Container-Start
Folge-Fix für die DSGVO-Menü-Sache. Settings.tsx hatte ich auf audit:read || gdpr:admin erweitert, aber auf bestehenden Installationen läuft der prisma-Seed nicht (nur auf leeren DBs). Wer das System früher installiert hat, hat die DSGVO-Rolle ohne audit:read in der DB – das JWT enthielt die Perm dann nie, und der neue Settings.tsx-Check blieb wirkungslos. Neues Skript prisma/sync-roles.ts läuft idempotent bei jedem Container-Start: upserts Permissions-Katalog + syncRolePermissions für Admin, Developer, DSGVO, Mitarbeiter (R/W + R/O), Kunde. Stammdaten, User und Verträge werden NICHT angefasst – sicher auf prod. Live-verifiziert: nach `DELETE audit:read FROM RolePermission` liefert der nächste Lauf "+1 Permissions an Rolle #27", DSGVO ist wieder komplett. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Idempotenter Permissions+Rollen-Sync für den Container-Start.
|
||||
*
|
||||
* Hintergrund: seed.ts läuft nur auf leeren DBs (USER_COUNT=0). Wer das
|
||||
* System schon installiert hat, bekommt nachträglich hinzugefügte
|
||||
* Permissions oder neue Rollenzuordnungen NICHT — die DSGVO-Rolle kann
|
||||
* dann z.B. ohne audit:read landen, obwohl Settings.tsx das voraussetzt.
|
||||
*
|
||||
* Dieses Skript synchronisiert ausschließlich:
|
||||
* - Permission-Katalog (resource/action-Paare aus dem Code)
|
||||
* - Roll-Zuordnungen (Admin, Developer, DSGVO, Mitarbeiter,
|
||||
* Mitarbeiter (Nur-Lesen), Kunde)
|
||||
*
|
||||
* KEINE Stammdaten, KEINE User, KEINE Verträge — das Skript ist auf
|
||||
* laufenden Prod-DBs sicher.
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const RESOURCE_PERMISSIONS: Record<string, string[]> = {
|
||||
customers: ['create', 'read', 'update', 'delete'],
|
||||
contracts: ['create', 'read', 'update', 'delete'],
|
||||
users: ['create', 'read', 'update', 'delete'],
|
||||
platforms: ['create', 'read', 'update', 'delete'],
|
||||
providers: ['create', 'read', 'update', 'delete'],
|
||||
tariffs: ['create', 'read', 'update', 'delete'],
|
||||
'cancellation-periods': ['create', 'read', 'update', 'delete'],
|
||||
'contract-durations': ['create', 'read', 'update', 'delete'],
|
||||
'contract-categories': ['create', 'read', 'update', 'delete'],
|
||||
'email-providers': ['create', 'read', 'update', 'delete'],
|
||||
settings: ['read', 'update'],
|
||||
developer: ['access'],
|
||||
emails: ['delete'],
|
||||
audit: ['read', 'export', 'admin'],
|
||||
gdpr: ['export', 'delete', 'admin'],
|
||||
};
|
||||
|
||||
async function syncRolePermissions(roleId: number, permissionIds: number[]) {
|
||||
const existing = await prisma.rolePermission.findMany({
|
||||
where: { roleId },
|
||||
select: { permissionId: true },
|
||||
});
|
||||
const existingIds = new Set(existing.map((e) => e.permissionId));
|
||||
const targetIds = new Set(permissionIds);
|
||||
|
||||
const missing = permissionIds.filter((id) => !existingIds.has(id));
|
||||
if (missing.length > 0) {
|
||||
await prisma.rolePermission.createMany({
|
||||
data: missing.map((permissionId) => ({ roleId, permissionId })),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
console.log(` → +${missing.length} Permissions an Rolle #${roleId}`);
|
||||
}
|
||||
|
||||
const excess = existing
|
||||
.filter((e) => !targetIds.has(e.permissionId))
|
||||
.map((e) => e.permissionId);
|
||||
if (excess.length > 0) {
|
||||
await prisma.rolePermission.deleteMany({
|
||||
where: { roleId, permissionId: { in: excess } },
|
||||
});
|
||||
console.log(` → -${excess.length} Permissions von Rolle #${roleId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('[sync-roles] Permissions-Katalog upserten…');
|
||||
for (const [resource, actions] of Object.entries(RESOURCE_PERMISSIONS)) {
|
||||
for (const action of actions) {
|
||||
await prisma.permission.upsert({
|
||||
where: { resource_action: { resource, action } },
|
||||
update: {},
|
||||
create: { resource, action },
|
||||
});
|
||||
}
|
||||
}
|
||||
const allPermissions = await prisma.permission.findMany();
|
||||
console.log(`[sync-roles] ${allPermissions.length} Permissions vorhanden`);
|
||||
|
||||
// Admin: alles AUSSER developer:access und audit/gdpr (DSGVO + Developer
|
||||
// sind separate hidden roles, über Checkboxen zugewiesen)
|
||||
const adminPermIds = allPermissions
|
||||
.filter(
|
||||
(p) =>
|
||||
!(p.resource === 'developer' && p.action === 'access') &&
|
||||
p.resource !== 'audit' &&
|
||||
p.resource !== 'gdpr'
|
||||
)
|
||||
.map((p) => p.id);
|
||||
|
||||
// Developer: alles
|
||||
const developerPermIds = allPermissions.map((p) => p.id);
|
||||
|
||||
// DSGVO: audit + gdpr komplett
|
||||
const gdprPermIds = allPermissions
|
||||
.filter((p) => p.resource === 'audit' || p.resource === 'gdpr')
|
||||
.map((p) => p.id);
|
||||
|
||||
// Mitarbeiter: customers + contracts + read auf Stammdaten
|
||||
const employeePermIds = allPermissions
|
||||
.filter(
|
||||
(p) =>
|
||||
p.resource === 'customers' ||
|
||||
p.resource === 'contracts' ||
|
||||
(p.action === 'read' &&
|
||||
[
|
||||
'platforms',
|
||||
'providers',
|
||||
'tariffs',
|
||||
'cancellation-periods',
|
||||
'contract-durations',
|
||||
'contract-categories',
|
||||
].includes(p.resource))
|
||||
)
|
||||
.map((p) => p.id);
|
||||
|
||||
// Read-only Mitarbeiter + Kunde: nur read auf Haupt-Entities + Stammdaten
|
||||
const readOnlyResources = [
|
||||
'customers',
|
||||
'contracts',
|
||||
'platforms',
|
||||
'providers',
|
||||
'tariffs',
|
||||
'cancellation-periods',
|
||||
'contract-durations',
|
||||
'contract-categories',
|
||||
];
|
||||
const readOnlyPermIds = allPermissions
|
||||
.filter((p) => p.action === 'read' && readOnlyResources.includes(p.resource))
|
||||
.map((p) => p.id);
|
||||
|
||||
const rolesSpec: Array<{ name: string; description: string; permIds: number[] }> = [
|
||||
{ name: 'Admin', description: 'Voller Zugriff auf alle Funktionen', permIds: adminPermIds },
|
||||
{ name: 'Developer', description: 'Voller Zugriff inkl. Entwickler-Tools', permIds: developerPermIds },
|
||||
{ name: 'DSGVO', description: 'DSGVO-Zugriff: Audit-Logs und Datenschutz-Verwaltung', permIds: gdprPermIds },
|
||||
{ name: 'Mitarbeiter', description: 'Kann Kunden und Verträge verwalten', permIds: employeePermIds },
|
||||
{ name: 'Mitarbeiter (Nur-Lesen)', description: 'Kann nur lesen, keine Änderungen', permIds: readOnlyPermIds },
|
||||
{ name: 'Kunde', description: 'Kann nur eigene Daten lesen', permIds: readOnlyPermIds },
|
||||
];
|
||||
|
||||
for (const r of rolesSpec) {
|
||||
const role = await prisma.role.upsert({
|
||||
where: { name: r.name },
|
||||
update: { description: r.description },
|
||||
create: { name: r.name, description: r.description },
|
||||
});
|
||||
await syncRolePermissions(role.id, r.permIds);
|
||||
}
|
||||
|
||||
console.log('[sync-roles] fertig.');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('[sync-roles] Fehler:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
Reference in New Issue
Block a user