R191-01: Ein PUT, eine Klammer
Befund der Pentesterin nach dem R190-01-Fix. Nur der Rollentausch lag in
der Transaktion, die drei Haken liefen danach in eigenen Schreibvorgaengen.
Ein Multi-Feld-PUT war damit nicht gesamt-atomar: Bricht ein Haken-Write
ab, steht der Rollenstand schon und die Haken halb. Kein Verlust, keine
Eskalation, aber ein Zwischenstand, den niemand angefordert hat. Ein PUT
ist ein Vorgang, also gehoert er in eine Klammer.
Die gesamte Schreibphase von updateUser liegt jetzt in einer Transaktion -
Benutzerdaten, Rollentausch, alle drei Haken. createUser ebenso.
Beim Umbau mitgefunden: Die drei Haken-Helfer legten die Rolle bei Bedarf
selbst an, falls sie fehlte. Das war eine vierte Stelle, die definierte,
was "Developer" bedeutet - mit einem anderen Rechtesatz als der Katalog
(nur developer:access statt allem) und ohne isSystem. Eine so entstandene
Rolle waere ueber die Rollenverwaltung aenderbar gewesen. Der Notfallpfad
ist weg: sync-roles legt die Rollen bei jedem Start an, die Startwache
meldet ihr Fehlen, und fehlt sie hier doch, bricht der Vorgang mit klarer
Ansage ab. Drei fast gleiche Funktionen wurden dabei eine.
int4-Ueberlauf (kosmetischer Nebenbefund): IDs jenseits des INT-Bereichs
werden nicht mehr abgefragt, sondern als unbekannt gemeldet.
createUser hasht jetzt mit Cost 12 statt 10, wie seed.ts. Wieder eine
Haertung, die nur in einer von zwei Kopien angekommen war.
Nachgeprueft: Rollback bewiesen, indem die DSGVO-Rolle voruebergehend
umbenannt und {roleIds:[23], hasGdprAccess:true} geschickt wurde - 400 mit
Klartext, Rollen unveraendert. Typvektoren "22", 4.5, 1e3, true, null,
Riesenzahl alle 400 ohne Wipe. R190-01-Regression erneut gruen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,9 @@
|
||||
*/
|
||||
|
||||
import prisma from '../lib/prisma.js';
|
||||
|
||||
/** Groesste Zahl, die in eine INT-Spalte passt. Darueber gibt es keine ID. */
|
||||
const INT_MAX = 2147483647;
|
||||
import {
|
||||
ROLLE_DEVELOPER,
|
||||
ROLLE_DSGVO,
|
||||
@@ -111,10 +114,15 @@ export async function rechteVonRollen(roleIds: number[]): Promise<Set<string>> {
|
||||
export async function rechteVonPermissionIds(ids: number[]): Promise<Set<string>> {
|
||||
const rechte = new Set<string>();
|
||||
if (ids.length === 0) return rechte;
|
||||
const gefunden = await prisma.permission.findMany({ where: { id: { in: ids } } });
|
||||
if (gefunden.length !== new Set(ids).size) {
|
||||
const eindeutig = [...new Set(ids)];
|
||||
const zuGross = eindeutig.filter((id) => id > INT_MAX);
|
||||
const abfragbar = eindeutig.filter((id) => id <= INT_MAX);
|
||||
const gefunden = abfragbar.length
|
||||
? await prisma.permission.findMany({ where: { id: { in: abfragbar } } })
|
||||
: [];
|
||||
if (gefunden.length !== abfragbar.length || zuGross.length > 0) {
|
||||
const bekannt = new Set(gefunden.map((p) => p.id));
|
||||
const unbekannt = [...new Set(ids)].filter((id) => !bekannt.has(id));
|
||||
const unbekannt = [...abfragbar.filter((id) => !bekannt.has(id)), ...zuGross];
|
||||
throw new UngueltigeEingabeError(`Unbekannte Rechte-ID: ${unbekannt.join(', ')}`);
|
||||
}
|
||||
for (const p of gefunden) rechte.add(`${p.resource}:${p.action}`);
|
||||
@@ -213,13 +221,22 @@ export async function normalisiereRollenIds(roleIds: number[]): Promise<number[]
|
||||
if (!eindeutig.every((id) => Number.isInteger(id) && id >= 1)) {
|
||||
throw new UngueltigeEingabeError('roleIds darf nur positive ganze Zahlen enthalten');
|
||||
}
|
||||
const gefunden = await prisma.role.findMany({
|
||||
where: { id: { in: eindeutig } },
|
||||
select: { id: true },
|
||||
});
|
||||
if (gefunden.length !== eindeutig.length) {
|
||||
|
||||
// Zahlen jenseits des INT-Bereichs gar nicht erst abfragen: Prisma bricht
|
||||
// dort mit einem Fremdfehler ab, und der kam als allgemeines "Fehler beim
|
||||
// Aktualisieren" zurueck - eine andere Antwort auf dieselbe Eingabeklasse
|
||||
// (Pentest R191, kosmetisch). Eine ID, die nicht in die Spalte passt,
|
||||
// benennt keine Rolle; sie ist schlicht unbekannt.
|
||||
const zuGross = eindeutig.filter((id) => id > INT_MAX);
|
||||
const abfragbar = eindeutig.filter((id) => id <= INT_MAX);
|
||||
|
||||
const gefunden = abfragbar.length
|
||||
? await prisma.role.findMany({ where: { id: { in: abfragbar } }, select: { id: true } })
|
||||
: [];
|
||||
|
||||
if (gefunden.length !== abfragbar.length || zuGross.length > 0) {
|
||||
const bekannt = new Set(gefunden.map((r) => r.id));
|
||||
const unbekannt = eindeutig.filter((id) => !bekannt.has(id));
|
||||
const unbekannt = [...abfragbar.filter((id) => !bekannt.has(id)), ...zuGross];
|
||||
throw new UngueltigeEingabeError(`Unbekannte Rollen-ID: ${unbekannt.join(', ')}`);
|
||||
}
|
||||
return eindeutig;
|
||||
|
||||
@@ -11,7 +11,20 @@ import {
|
||||
meldeTraegerAb,
|
||||
RollenSperrError,
|
||||
} from './rechte.service.js';
|
||||
import { istSystemrollenName, RECHTE_KATALOG } from '../config/rechte-katalog.js';
|
||||
import {
|
||||
istSystemrollenName,
|
||||
RECHTE_KATALOG,
|
||||
ROLLE_DEVELOPER,
|
||||
ROLLE_DSGVO,
|
||||
ROLLE_AUDIT_BETRIEB,
|
||||
} from '../config/rechte-katalog.js';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
/**
|
||||
* Re-Export aus dem Katalog. Der Name ist an mehreren Stellen im Umlauf;
|
||||
* die Wahrheit steht jetzt an einer Stelle.
|
||||
*/
|
||||
export const AUDIT_OPS_ROLLE = ROLLE_AUDIT_BETRIEB;
|
||||
|
||||
export interface UserFilters {
|
||||
search?: string;
|
||||
@@ -182,50 +195,49 @@ export async function createUser(data: {
|
||||
...rechteDerHaken(data),
|
||||
]);
|
||||
|
||||
const hashedPassword = await bcrypt.hash(data.password, 10);
|
||||
// Cost 12 wie in prisma/seed.ts (OWASP 2026). Hier stand 10 - dieselbe
|
||||
// Haertung, die im Seed laengst galt, war an dieser Stelle nie
|
||||
// angekommen. Bestehende Kennwoerter bleiben pruefbar, der Cost steckt im
|
||||
// Hash.
|
||||
const hashedPassword = await bcrypt.hash(data.password, 12);
|
||||
|
||||
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: rollenIds.map((roleId) => ({ roleId })),
|
||||
const user = await prisma.$transaction(async (tx) => {
|
||||
const angelegt = await tx.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: rollenIds.map((roleId) => ({ roleId })),
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
isActive: true,
|
||||
isServiceAccount: true,
|
||||
customerId: true,
|
||||
roles: {
|
||||
include: { role: true },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
isActive: true,
|
||||
isServiceAccount: true,
|
||||
customerId: true,
|
||||
roles: {
|
||||
include: { role: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Die Haken in derselben Transaktion wie das Konto (Pentest R191-01).
|
||||
// Sonst koennte ein halb ausgestattetes Konto zurueckbleiben: angelegt,
|
||||
// aber ohne die zugesagte versteckte Rolle.
|
||||
await setzeHaken(tx, angelegt.id, data);
|
||||
|
||||
return angelegt;
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -385,217 +397,120 @@ export async function updateUser(
|
||||
!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() }),
|
||||
},
|
||||
});
|
||||
|
||||
// Rollentausch in EINER Transaktion.
|
||||
// Die gesamte Schreibphase in EINER Transaktion.
|
||||
//
|
||||
// Vorher standen deleteMany und createMany nackt nebeneinander: Scheiterte
|
||||
// das Anlegen, war das Loeschen schon passiert und das Konto hatte gar
|
||||
// keine Rolle mehr - bei einer Antwort, die wie "abgelehnt, nichts
|
||||
// geschehen" aussah. `updateRole` hatte diese Haertung laengst; sie war
|
||||
// nur nicht zum Geschwister mitgewandert (Pentest R190-01).
|
||||
if (gepruefteRollenIds !== undefined) {
|
||||
await prisma.$transaction([
|
||||
prisma.userRole.deleteMany({ where: { userId: id } }),
|
||||
prisma.userRole.createMany({
|
||||
// Zwei Stufen, beide aus dem Pentest:
|
||||
//
|
||||
// R190-01: deleteMany und createMany standen nackt nebeneinander.
|
||||
// Scheiterte das Anlegen, war das Loeschen schon passiert - das Konto
|
||||
// hatte gar keine Rolle mehr, bei einer Antwort, die wie "abgelehnt,
|
||||
// nichts geschehen" aussah.
|
||||
//
|
||||
// R191-01: Danach lag zwar der Rollentausch in einer Transaktion, die
|
||||
// drei Haken aber liefen als eigene Schreibvorgaenge hinterher. Ein
|
||||
// Datenbankfehler dort hinterliess den Rollenstand gesetzt und die Haken
|
||||
// halb - kein Verlust und keine Rechteerhoehung, aber ein Zwischenstand,
|
||||
// den niemand angefordert hat. Ein PUT ist ein Vorgang, also gehoert er
|
||||
// in eine Klammer.
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.user.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...userData,
|
||||
// Token invalidieren wenn Rollen geändert werden
|
||||
...(rolesChanged && { tokenInvalidatedAt: new Date() }),
|
||||
},
|
||||
});
|
||||
|
||||
if (gepruefteRollenIds !== undefined) {
|
||||
await tx.userRole.deleteMany({ where: { userId: id } });
|
||||
await tx.userRole.createMany({
|
||||
data: gepruefteRollenIds.map((roleId) => ({ userId: id, roleId })),
|
||||
skipDuplicates: true,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
// Nach dem Rollentausch: Der loescht ALLE Zuordnungen, auch die
|
||||
// versteckten Rollen. Die Haken setzen sie anschliessend wieder.
|
||||
await setzeHaken(tx, id, { hasDeveloperAccess, hasGdprAccess, 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.
|
||||
* Setzt oder entfernt eine der drei versteckten Rollen (DSGVO, Developer,
|
||||
* Audit-Betrieb) - innerhalb der uebergebenen Transaktion.
|
||||
*
|
||||
* 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.
|
||||
* Ersetzt drei fast gleiche Funktionen, die jede fuer sich die Rolle bei
|
||||
* Bedarf ANLEGTEN, falls sie fehlte. Das war als Notfallpfad gedacht und
|
||||
* zugleich eine vierte Stelle, die festlegte, was "Developer" bedeutet -
|
||||
* mit einem anderen Rechtesatz als der Katalog (nur `developer:access`
|
||||
* statt allem) und ohne `isSystem`. Eine so entstandene Rolle waere ueber
|
||||
* die Rollenverwaltung aenderbar gewesen.
|
||||
*
|
||||
* Den Notfallpfad braucht es nicht mehr: `synchronisiereRechteUndRollen`
|
||||
* legt die Rollen bei jedem Containerstart an, und die Startwache meldet,
|
||||
* wenn eine fehlt. Fehlt sie hier trotzdem, ist Abbrechen mit klarer Ansage
|
||||
* ehrlicher, als stillschweigend etwas Aehnliches zu erfinden.
|
||||
*/
|
||||
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 } });
|
||||
|
||||
async function setzeVersteckteRolle(
|
||||
tx: Prisma.TransactionClient,
|
||||
userId: number,
|
||||
rollenName: string,
|
||||
aktiv: boolean,
|
||||
): Promise<void> {
|
||||
const rolle = await tx.role.findUnique({ where: { name: rollenName } });
|
||||
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 })) },
|
||||
},
|
||||
});
|
||||
throw new Error(
|
||||
`Die Rolle „${rollenName}" fehlt in der Datenbank. Sie wird beim ` +
|
||||
'Containerstart angelegt – einmalig nachholen mit: npx tsx prisma/sync-roles.ts',
|
||||
);
|
||||
}
|
||||
|
||||
const hatRolle = await prisma.userRole.findFirst({
|
||||
where: { userId, roleId: rolle.id },
|
||||
const vorhanden = await tx.userRole.findUnique({
|
||||
where: { userId_roleId: { 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({
|
||||
if (aktiv && !vorhanden) {
|
||||
await tx.userRole.create({ data: { userId, roleId: rolle.id } });
|
||||
} else if (!aktiv && vorhanden) {
|
||||
await tx.userRole.delete({
|
||||
where: { userId_roleId: { userId, roleId: rolle.id } },
|
||||
});
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { tokenInvalidatedAt: new Date() },
|
||||
});
|
||||
} else {
|
||||
return; // nichts geaendert - dann auch niemanden abmelden
|
||||
}
|
||||
|
||||
// Rechteaenderung wirkt sofort, nicht erst nach Ablauf des Tokens.
|
||||
await tx.user.update({
|
||||
where: { id: userId },
|
||||
data: { tokenInvalidatedAt: new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
/** Die drei Haken auf ihre versteckten Rollen abbilden. */
|
||||
async function setzeHaken(
|
||||
tx: Prisma.TransactionClient,
|
||||
userId: number,
|
||||
haken: {
|
||||
hasDeveloperAccess?: boolean;
|
||||
hasGdprAccess?: boolean;
|
||||
hasAuditOpsAccess?: boolean;
|
||||
},
|
||||
): Promise<void> {
|
||||
if (haken.hasDeveloperAccess !== undefined) {
|
||||
await setzeVersteckteRolle(tx, userId, ROLLE_DEVELOPER, haken.hasDeveloperAccess);
|
||||
}
|
||||
if (haken.hasGdprAccess !== undefined) {
|
||||
await setzeVersteckteRolle(tx, userId, ROLLE_DSGVO, haken.hasGdprAccess);
|
||||
}
|
||||
if (haken.hasAuditOpsAccess !== undefined) {
|
||||
await setzeVersteckteRolle(tx, userId, AUDIT_OPS_ROLLE, haken.hasAuditOpsAccess);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function deleteUser(id: number) {
|
||||
// Check if user is an admin
|
||||
const user = await prisma.user.findUnique({
|
||||
|
||||
Reference in New Issue
Block a user