Files
opencrm/backend/src/services/user.service.ts
T
duffyduckandClaude Opus 5 77aeb69aeb Rechtemodell Etappe 1: toter Katalog begradigt, Selbst-Erhoehung geschlossen
Vorarbeit fuer die Rollen-Oberflaeche. Eine Checkbox-Liste ueber einem
Katalog, der nicht stimmt, waere schlimmer als gar keine.

18 von 50 Rechten bewachten nichts: tariffs:*, cancellation-periods:*,
contract-durations:* und email-providers:* standen im Katalog und waren
anhakbar - die Routen prueften in Wahrheit providers:*, platforms:* und
settings:*. Die Routen gaten jetzt granular; eine rein additive Migration
vererbt jedes neue Recht an jede Rolle, die bisher das Sammelrecht hatte.
Nachgerechnet: keine Rolle hat etwas verloren.

Der Katalog stand dreifach und divergent - seed.ts, sync-roles.ts und,
abweichend, factoryReset. Die dritte Kopie kannte weder audit:* noch
gdpr:* und legte DSGVO, Audit-Betrieb und Gegenbuch gar nicht an: Nach
einem Werksreset konnte niemand mehr eine Auskunft nach Art. 15
ausfuehren, und aufgefallen waere es erst, wenn eine Frist laeuft. Jetzt
eine Quelle: src/config/rechte-katalog.ts.

Im selben Code: factoryReset setzte das Admin-Kennwort fest auf "admin"
mit bcrypt-Cost 10 - genau das, was seed.ts seit Pentest Runde 12
verbietet. Dieselbe Haertung war nur in einer der beiden Kopien
angekommen. Jetzt zufaellig, Cost 12, einmalig im Log.

Selbst-Erhoehung geschlossen: neues Recht roles:manage, getrennt von
users:*, dazu die Teilmengenregel in rechte.service.ts - niemand kann ein
Recht weitergeben, das er selbst nicht haelt. Sie greift auf allen vier
Wegen: Rolle anlegen, Rolle aendern, Rollen zuweisen und die drei Haken.
Der Haken-Weg war der wichtigste: Er brauchte nur users:create, ein
zweites Konto mit "Entwicklerzugriff" anlegen und sich damit anmelden -
die Developer-Rolle traegt alle Rechte. Geprueft wird der Zuwachs, nicht
der Endzustand, damit Entziehen erlaubt bleibt; und die Rechte des
Handelnden kommen frisch aus der Datenbank, nicht aus dem bis zu 15
Minuten alten Token.

Systemrollen gesperrt (Role.isSystem): Admin liess sich bisher umbenennen
oder leeren - und die versteckten Rollen haengen an ihrem Namen.
Role.isHidden ersetzt die im Frontend hartkodierte Namensliste, in der
"Gegenbuch" fehlte.

Rechteaenderung wirkt sofort: updateRole/deleteRole melden alle Traeger
ab. Werksreset und Backup-Restore verlangen zusaetzlich roles:manage -
beide loeschen alle Rollen und legen ein frisches admin@admin.com an.

Rollenpflege war der einzige Eingriff in die Rechtevergabe ohne
SecurityEvent, obwohl sie viele Konten auf einmal trifft. Jetzt
PERMISSION_CHANGED - ebenso fuer die bisher stummen Haken DSGVO und
Entwicklerzugriff.

Aussperr-Ausweg: prisma/rolle-zuweisen.ts. Umgeht die Regel bewusst - wer
Shell-Zugang hat, hat ohnehin die Datenbank; ein gestohlener Web-Zugang
hat ihn nicht. Schreibt ueber die Hash-Kette, nicht roh. Die Startwache
nennt den Befehl im Klartext.

Geprueft gegen Dev-DB und frische Wegwerf-DB: 7 Eskalationswege alle 403,
2 Gegenproben erlaubt; 6 Sperrtests auf Systemrollen alle 403, eigene
Rollen weiter aenderbar; Stammdaten-Lesen unveraendert 200;
Rechteaenderung sofort 401; Migration und sync-roles dreimal identisch;
db:seed auf bestehender DB ohne Wirkung auf die Rollenmatrix; frische
Installation mit allen 8 Systemrollen und ohne Hinweise.

Beim Deploy: Die Migration beendet alle Sitzungen einmalig. Noetig, weil
die Rechte im Token stehen - sonst liefen bis zu 15 Minuten 403er.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 13:49:30 +02:00

831 lines
23 KiB
TypeScript

import prisma from '../lib/prisma.js';
import bcrypt from 'bcryptjs';
import { paginate, buildPaginationResponse } from '../utils/helpers.js';
import {
pruefeTeilmenge,
rechteVonPermissionIds,
rechteVonRollen,
rechteDerHaken,
effektiveRechte,
meldeTraegerAb,
RollenSperrError,
} from './rechte.service.js';
import { istSystemrollenName, RECHTE_KATALOG } from '../config/rechte-katalog.js';
export interface UserFilters {
search?: string;
isActive?: boolean;
roleId?: number;
page?: number;
limit?: number;
}
export async function getAllUsers(filters: UserFilters) {
const { search, isActive, roleId, page = 1, limit = 20 } = filters;
const { skip, take } = paginate(page, limit);
const where: Record<string, unknown> = {};
if (isActive !== undefined) {
where.isActive = isActive;
}
if (roleId) {
where.roles = { some: { roleId } };
}
if (search) {
where.OR = [
{ email: { contains: search } },
{ firstName: { contains: search } },
{ lastName: { contains: search } },
];
}
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
skip,
take,
orderBy: { createdAt: 'desc' },
select: {
id: true,
email: true,
firstName: true,
lastName: true,
isActive: true,
isServiceAccount: true,
customerId: true,
whatsappNumber: true,
telegramUsername: true,
signalNumber: true,
createdAt: true,
roles: {
include: {
role: {
include: {
permissions: true,
},
},
},
},
},
}),
prisma.user.count({ where }),
]);
// Get hidden role IDs
const [developerRole, gdprRole, auditOpsRole] = await Promise.all([
prisma.role.findFirst({ where: { name: 'Developer' } }),
prisma.role.findFirst({ where: { name: 'DSGVO' } }),
prisma.role.findFirst({ where: { name: AUDIT_OPS_ROLLE } }),
]);
return {
users: users.map((u) => {
const hasDeveloperAccess = developerRole
? u.roles.some((ur) => ur.roleId === developerRole.id)
: false;
const hasGdprAccess = gdprRole
? u.roles.some((ur) => ur.roleId === gdprRole.id)
: false;
const hasAuditOpsAccess = auditOpsRole
? u.roles.some((ur) => ur.roleId === auditOpsRole.id)
: false;
return {
...u,
roles: u.roles.map((r) => r.role),
hasDeveloperAccess,
hasGdprAccess,
hasAuditOpsAccess,
};
}),
pagination: buildPaginationResponse(page, limit, total),
};
}
export async function getUserById(id: number) {
const user = await prisma.user.findUnique({
where: { id },
select: {
id: true,
email: true,
firstName: true,
lastName: true,
isActive: true,
isServiceAccount: true,
customerId: true,
whatsappNumber: true,
telegramUsername: true,
signalNumber: true,
createdAt: true,
updatedAt: true,
roles: {
include: {
role: {
include: {
permissions: {
include: { permission: true },
},
},
},
},
},
},
});
if (!user) return null;
const permissions = new Set<string>();
for (const userRole of user.roles) {
for (const rolePerm of userRole.role.permissions) {
permissions.add(
`${rolePerm.permission.resource}:${rolePerm.permission.action}`
);
}
}
return {
...user,
roles: user.roles.map((r) => r.role),
permissions: Array.from(permissions),
};
}
export async function createUser(data: {
email: string;
password: string;
firstName: string;
lastName: string;
roleIds: number[];
customerId?: number;
hasDeveloperAccess?: boolean;
hasGdprAccess?: boolean;
hasAuditOpsAccess?: boolean;
whatsappNumber?: string;
telegramUsername?: string;
signalNumber?: string;
},
handelnderId: number,
) {
// Was das neue Konto koennen wird - aus den Rollen UND aus den Haken.
// `handelnderId` ist Pflichtparameter, damit kein Aufrufer die Pruefung
// vergessen kann; der Compiler erzwingt sie.
await pruefeTeilmenge(handelnderId, [
...(await rechteVonRollen(data.roleIds)),
...rechteDerHaken(data),
]);
const hashedPassword = await bcrypt.hash(data.password, 10);
const user = await prisma.user.create({
data: {
email: data.email,
password: hashedPassword,
firstName: data.firstName,
lastName: data.lastName,
customerId: data.customerId,
whatsappNumber: data.whatsappNumber || null,
telegramUsername: data.telegramUsername || null,
signalNumber: data.signalNumber || null,
roles: {
create: data.roleIds.map((roleId) => ({ roleId })),
},
},
select: {
id: true,
email: true,
firstName: true,
lastName: true,
isActive: true,
isServiceAccount: true,
customerId: true,
roles: {
include: { role: true },
},
},
});
// Entwicklerzugriff setzen falls aktiviert
if (data.hasDeveloperAccess) {
await setUserDeveloperAccess(user.id, true);
}
// DSGVO-Zugriff setzen falls aktiviert
if (data.hasGdprAccess) {
await setUserGdprAccess(user.id, true);
}
if (data.hasAuditOpsAccess) {
await setUserAuditOpsAccess(user.id, true);
}
return user;
}
export async function updateUser(
id: number,
data: {
email?: string;
password?: string;
firstName?: string;
lastName?: string;
isActive?: boolean;
isServiceAccount?: boolean;
roleIds?: number[];
customerId?: number;
hasDeveloperAccess?: boolean;
hasGdprAccess?: boolean;
hasAuditOpsAccess?: boolean;
whatsappNumber?: string;
telegramUsername?: string;
signalNumber?: string;
},
handelnderId: number,
) {
const { roleIds, password, hasDeveloperAccess, hasGdprAccess, hasAuditOpsAccess, ...userData } = data;
// Teilmengenregel auf dem ZUWACHS, nicht auf dem Endzustand.
//
// Wer nur den Nachnamen eines hoeher privilegierten Kollegen korrigiert,
// schickt das Formular unveraendert mit - inklusive gesetzter Haken. Wuerde
// hier der Endzustand geprueft, waere jede solche Korrektur ein 403.
// Geprueft wird deshalb nur, was das Zielkonto NEU dazubekommt.
{
const bisher = await effektiveRechte(id);
const zuwachs = new Set<string>();
if (roleIds !== undefined) {
for (const r of await rechteVonRollen(roleIds)) {
if (!bisher.has(r)) zuwachs.add(r);
}
}
for (const r of rechteDerHaken({ hasDeveloperAccess, hasGdprAccess, hasAuditOpsAccess })) {
if (!bisher.has(r)) zuwachs.add(r);
}
await pruefeTeilmenge(handelnderId, zuwachs);
}
// Check if this would remove the last admin
const isBeingDeactivated = userData.isActive === false;
const rolesAreBeingChanged = roleIds !== undefined;
if (isBeingDeactivated || rolesAreBeingChanged) {
// Check if user currently has admin permissions
const currentUser = await prisma.user.findUnique({
where: { id },
include: {
roles: {
include: {
role: {
include: {
permissions: {
include: { permission: true },
},
},
},
},
},
},
});
const isCurrentlyAdmin = currentUser?.roles.some((ur) =>
ur.role.permissions.some(
(rp) => rp.permission.resource === 'users' && rp.permission.action === 'delete'
)
);
if (isCurrentlyAdmin) {
// Check if user will still be admin after role change
let willStillBeAdmin = false;
if (rolesAreBeingChanged) {
const newRoles = await prisma.role.findMany({
where: { id: { in: roleIds } },
include: {
permissions: {
include: { permission: true },
},
},
});
willStillBeAdmin = newRoles.some((role) =>
role.permissions.some(
(rp) => rp.permission.resource === 'users' && rp.permission.action === 'delete'
)
);
} else {
willStillBeAdmin = true; // Roles not being changed
}
// If user is losing admin status or being deactivated, check for other admins
if (!willStillBeAdmin || isBeingDeactivated) {
const otherAdminCount = await prisma.user.count({
where: {
id: { not: id },
isActive: true,
roles: {
some: {
role: {
permissions: {
some: {
permission: {
resource: 'users',
action: 'delete',
},
},
},
},
},
},
},
});
if (otherAdminCount === 0) {
if (isBeingDeactivated) {
throw new Error(
'Dieser Benutzer ist der letzte Administrator und kann nicht deaktiviert werden'
);
} else {
throw new Error(
'Die Admin-Rolle kann nicht entfernt werden, da dies der letzte Administrator ist'
);
}
}
}
}
}
// Hash password if provided
if (password) {
(userData as Record<string, unknown>).password = await bcrypt.hash(password, 10);
}
// Prüfen ob Rollen geändert werden (für Zwangslogout)
let rolesChanged = false;
if (roleIds !== undefined) {
const currentRoles = await prisma.userRole.findMany({
where: { userId: id },
select: { roleId: true },
});
const currentRoleIds = currentRoles.map((r) => r.roleId).sort();
const newRoleIds = [...roleIds].sort();
rolesChanged =
currentRoleIds.length !== newRoleIds.length ||
!currentRoleIds.every((id, i) => id === newRoleIds[i]);
}
// Update user - bei Rollenänderung Token invalidieren
await prisma.user.update({
where: { id },
data: {
...userData,
// Token invalidieren wenn Rollen geändert werden
...(rolesChanged && { tokenInvalidatedAt: new Date() }),
},
});
// Update roles if provided
if (roleIds) {
await prisma.userRole.deleteMany({ where: { userId: id } });
await prisma.userRole.createMany({
data: roleIds.map((roleId) => ({ userId: id, roleId })),
});
}
// Handle developer access
if (hasDeveloperAccess !== undefined) {
await setUserDeveloperAccess(id, hasDeveloperAccess);
}
// Handle GDPR access
if (hasGdprAccess !== undefined) {
await setUserGdprAccess(id, hasGdprAccess);
}
if (hasAuditOpsAccess !== undefined) {
await setUserAuditOpsAccess(id, hasAuditOpsAccess);
}
return getUserById(id);
}
// Helper to set developer access for a user
async function setUserDeveloperAccess(userId: number, enabled: boolean) {
// Get or create developer:access permission
let developerPerm = await prisma.permission.findFirst({
where: { resource: 'developer', action: 'access' },
});
if (!developerPerm) {
developerPerm = await prisma.permission.create({
data: { resource: 'developer', action: 'access' },
});
}
// Get or create Developer role
let developerRole = await prisma.role.findFirst({
where: { name: 'Developer' },
});
if (!developerRole) {
developerRole = await prisma.role.create({
data: {
name: 'Developer',
description: 'Entwicklerzugriff auf Datenbanktools',
permissions: {
create: [{ permissionId: developerPerm.id }],
},
},
});
}
// Check if user already has Developer role
const hasRole = await prisma.userRole.findFirst({
where: { userId, roleId: developerRole.id },
});
if (enabled && !hasRole) {
await prisma.userRole.create({
data: { userId, roleId: developerRole.id },
});
// Token invalidieren bei Rechteänderung
await prisma.user.update({
where: { id: userId },
data: { tokenInvalidatedAt: new Date() },
});
} else if (!enabled && hasRole) {
await prisma.userRole.delete({
where: { userId_roleId: { userId, roleId: developerRole.id } },
});
// Token invalidieren bei Rechteänderung
await prisma.user.update({
where: { id: userId },
data: { tokenInvalidatedAt: new Date() },
});
}
}
/**
* Name der versteckten Rolle fuer eingreifende Audit-Rechte.
*
* Getrennt von `DSGVO`, weil Aufsicht und Eingriff nicht dieselbe Rolle sein
* duerfen: Wer das Protokoll beaufsichtigt, darf seine Beweisgrundlage nicht
* ersetzen koennen (Pentest R186). Die Rolle wird von `sync-roles.ts` beim
* Containerstart angelegt.
*/
export const AUDIT_OPS_ROLLE = 'Audit-Betrieb';
// Helper to set GDPR access for a user
async function setUserGdprAccess(userId: number, enabled: boolean) {
// Get or create DSGVO role
let gdprRole = await prisma.role.findFirst({
where: { name: 'DSGVO' },
});
if (!gdprRole) {
// Rechte-Satz identisch zu sync-roles.ts: gdpr komplett, vom Audit-
// Protokoll nur LESEN und EXPORTIEREN. Ohne `audit:admin` - sonst
// brächte dieser Notfallpfad genau die Bündelung zurück, die wir gerade
// aufgelöst haben (zwei Listen, die dasselbe bedeuten sollen, laufen
// auseinander).
const gdprPermissions = await prisma.permission.findMany({
where: {
OR: [
{ resource: 'gdpr' },
{ resource: 'audit', action: { in: ['read', 'export'] } },
],
},
});
gdprRole = await prisma.role.create({
data: {
name: 'DSGVO',
description: 'DSGVO-Zugriff: Audit-Logs lesen und Datenschutz-Verwaltung',
permissions: {
create: gdprPermissions.map((p) => ({ permissionId: p.id })),
},
},
});
}
// Check if user already has DSGVO role
const hasRole = await prisma.userRole.findFirst({
where: { userId, roleId: gdprRole.id },
});
if (enabled && !hasRole) {
await prisma.userRole.create({
data: { userId, roleId: gdprRole.id },
});
await prisma.user.update({
where: { id: userId },
data: { tokenInvalidatedAt: new Date() },
});
} else if (!enabled && hasRole) {
await prisma.userRole.delete({
where: { userId_roleId: { userId, roleId: gdprRole.id } },
});
await prisma.user.update({
where: { id: userId },
data: { tokenInvalidatedAt: new Date() },
});
}
}
/**
* Eingreifende Audit-Rechte setzen: versiegeln, neu berechnen, aufraeumen,
* Aufbewahrung aendern. Bewusst getrennt vom DSGVO-Haken (siehe
* `AUDIT_OPS_ROLLE`).
*/
async function setUserAuditOpsAccess(userId: number, enabled: boolean) {
let rolle = await prisma.role.findFirst({ where: { name: AUDIT_OPS_ROLLE } });
if (!rolle) {
// Rechte-Satz identisch zu sync-roles.ts.
const rechte = await prisma.permission.findMany({
where: { resource: 'audit', action: { in: ['read', 'admin'] } },
});
rolle = await prisma.role.create({
data: {
name: AUDIT_OPS_ROLLE,
description: 'Darf das Audit-Protokoll versiegeln, aufräumen und die Aufbewahrung ändern',
permissions: { create: rechte.map((p) => ({ permissionId: p.id })) },
},
});
}
const hatRolle = await prisma.userRole.findFirst({
where: { userId, roleId: rolle.id },
});
if (enabled && !hatRolle) {
await prisma.userRole.create({ data: { userId, roleId: rolle.id } });
// Token invalidieren bei Rechteaenderung
await prisma.user.update({
where: { id: userId },
data: { tokenInvalidatedAt: new Date() },
});
} else if (!enabled && hatRolle) {
await prisma.userRole.delete({
where: { userId_roleId: { userId, roleId: rolle.id } },
});
await prisma.user.update({
where: { id: userId },
data: { tokenInvalidatedAt: new Date() },
});
}
}
export async function deleteUser(id: number) {
// Check if user is an admin
const user = await prisma.user.findUnique({
where: { id },
include: {
roles: {
include: {
role: {
include: {
permissions: {
include: { permission: true },
},
},
},
},
},
},
});
if (!user) {
throw new Error('Benutzer nicht gefunden');
}
// Check if user has admin permissions (users:delete means admin)
const isAdmin = user.roles.some((ur) =>
ur.role.permissions.some(
(rp) => rp.permission.resource === 'users' && rp.permission.action === 'delete'
)
);
if (isAdmin) {
// Count other admins (users with users:delete permission)
const adminCount = await prisma.user.count({
where: {
id: { not: id },
isActive: true,
roles: {
some: {
role: {
permissions: {
some: {
permission: {
resource: 'users',
action: 'delete',
},
},
},
},
},
},
},
});
if (adminCount === 0) {
throw new Error(
'Dieser Benutzer ist der letzte Administrator und kann nicht gelöscht werden'
);
}
}
return prisma.user.delete({ where: { id } });
}
// Role operations
export async function getAllRoles() {
return prisma.role.findMany({
include: {
permissions: {
include: { permission: true },
},
_count: {
select: { users: true },
},
},
orderBy: { name: 'asc' },
});
}
export async function getRoleById(id: number) {
return prisma.role.findUnique({
where: { id },
include: {
permissions: {
include: { permission: true },
},
},
});
}
/**
* Legt eine Rolle an.
*
* `handelnderId` ist Pflichtparameter, nicht optional: So kann ein Controller
* die Eskalationspruefung nicht vergessen, und der Compiler erzwingt sie bei
* jedem kuenftigen Aufrufer. Bis 09/2026 ging hier `req.body` ungefiltert
* durch - wer `users:create` hatte, konnte eine Rolle mit `developer:access`
* bauen und sie sich anschliessend selbst zuweisen.
*/
export async function createRole(
data: {
name: string;
description?: string;
permissionIds: number[];
},
handelnderId: number,
) {
const name = data.name.trim();
// Kein zweites "Admin". Getrimmt und ohne Ruecksicht auf Gross-/
// Kleinschreibung verglichen, sonst liesse sich " admin " anlegen, das in
// einer Liste wie das Original aussieht.
if (istSystemrollenName(name)) {
throw new RollenSperrError(
`„${name}" ist der Name einer Systemrolle und kann nicht neu vergeben werden.`,
);
}
await pruefeTeilmenge(handelnderId, await rechteVonPermissionIds(data.permissionIds));
return prisma.role.create({
data: {
name,
description: data.description,
permissions: {
create: [...new Set(data.permissionIds)].map((permissionId) => ({ permissionId })),
},
},
include: {
permissions: {
include: { permission: true },
},
},
});
}
/**
* Aendert eine Rolle. Systemrollen sind gesperrt.
*
* Geprueft wird nur der ZUWACHS gegenueber dem bisherigen Rechtesatz der
* Rolle - wer Rechte wegnimmt, braucht sie nicht selbst zu besitzen.
*/
export async function updateRole(
id: number,
data: {
name?: string;
description?: string;
permissionIds?: number[];
},
handelnderId: number,
) {
const bestehend = await prisma.role.findUnique({
where: { id },
include: { permissions: { include: { permission: true } } },
});
if (!bestehend) return null;
if (bestehend.isSystem) {
throw new RollenSperrError(
`„${bestehend.name}" ist eine Systemrolle. Sie wird von der Anwendung ` +
'gepflegt und kann hier nicht geändert werden.',
);
}
if (data.name !== undefined && istSystemrollenName(data.name)) {
throw new RollenSperrError(
`„${data.name.trim()}" ist der Name einer Systemrolle und kann nicht vergeben werden.`,
);
}
const { permissionIds, ...roleData } = data;
if (roleData.name !== undefined) roleData.name = roleData.name.trim();
if (permissionIds) {
const bisher = new Set(
bestehend.permissions.map((rp) => `${rp.permission.resource}:${rp.permission.action}`),
);
const kuenftig = await rechteVonPermissionIds(permissionIds);
const zuwachs = [...kuenftig].filter((r) => !bisher.has(r));
await pruefeTeilmenge(handelnderId, zuwachs);
}
await prisma.role.update({
where: { id },
data: roleData,
});
if (permissionIds) {
await prisma.rolePermission.deleteMany({ where: { roleId: id } });
await prisma.rolePermission.createMany({
// Dedupliziert: Doppelte IDs im Body liefen vorher in einen
// Primaerschluesselkonflikt und kamen als HTTP 500 zurueck.
data: [...new Set(permissionIds)].map((permissionId) => ({ roleId: id, permissionId })),
skipDuplicates: true,
});
// Rechteaenderung wirkt sofort, nicht erst nach Ablauf des Tokens.
await meldeTraegerAb(id);
}
return getRoleById(id);
}
export async function deleteRole(id: number) {
const bestehend = await prisma.role.findUnique({ where: { id } });
if (!bestehend) {
throw new Error('Rolle nicht gefunden');
}
if (bestehend.isSystem) {
throw new RollenSperrError(
`„${bestehend.name}" ist eine Systemrolle und kann nicht gelöscht werden.`,
);
}
// Vor dem Loeschen abmelden - danach sind die Traeger durch den Cascade
// nicht mehr ermittelbar. Greift heute nur theoretisch, weil zugewiesene
// Rollen ohnehin abgelehnt werden; die Reihenfolge bleibt trotzdem die
// richtige, falls diese Sperre je gelockert wird.
const count = await prisma.userRole.count({ where: { roleId: id } });
if (count > 0) {
throw new Error(
`Rolle kann nicht gelöscht werden, da sie ${count} Benutzern zugewiesen ist`
);
}
await meldeTraegerAb(id);
return prisma.role.delete({ where: { id } });
}
// Permission operations
/**
* Liefert die vergebbaren Rechte - angereichert um Klartext und Gruppe.
*
* Gefiltert auf den Katalog: In der Datenbank koennen Rechte aus frueheren
* Schemata liegen (`customers:access`, `settings:create` und aehnliche), die
* nirgends geprueft werden. Ungefiltert wuerden sie in der Rollenoberflaeche
* als anhakbare Kaestchen erscheinen, die nichts bewirken - genau der
* Zustand, den dieser Umbau beseitigt. Geloescht werden sie nicht: Ein
* Lesefilter ist die kleinere Behauptung als ein DELETE, und der
* Rechte-Report meldet sie ohnehin.
*/
export async function getAllPermissions() {
const alle = await prisma.permission.findMany({
orderBy: [{ resource: 'asc' }, { action: 'asc' }],
});
const beschreibung = new Map(
RECHTE_KATALOG.map((r) => [`${r.resource}:${r.action}`, r]),
);
return alle
.filter((p) => beschreibung.has(`${p.resource}:${p.action}`))
.map((p) => {
const k = beschreibung.get(`${p.resource}:${p.action}`)!;
return { ...p, bezeichnung: k.bezeichnung, gruppe: k.gruppe };
});
}