feat(quick-260910-das): Steuerungsschicht binden, Selbstloesch-Riegel schliessen
- user.controller.ts: alle sieben Zugriffe binden. ADMIN-Zweig der Benutzerliste laeuft ueber forTenant() mit weiterhin bestehender Mandantenbedingung im where; SUPER_ADMIN-Zweig ueber die neue UserService.findAllForPlatformAdmin(). Die drei Wege ueber die Kennung loesen den Zielbenutzer rollenabhaengig ueber resolveTargetUser() auf (ADMIN gebunden an eigenen Mandanten, SUPER_ADMIN uebergreifend); der Schreibzugriff bei update/delete bindet an den Mandanten des Zielbenutzers, nicht des Aufrufers, damit die uebergreifende Verwaltung durch die oberste Rolle erhalten bleibt - Selbstloesch-Riegel (Befund H) repariert: verglich bisher gegen currentUser.sub, ein Feld, das der Sitzungsnachweis nicht traegt -- der Riegel griff nie. Jetzt gegen currentUser.id. Verhaltensaenderung: ein Administrator kann sein eigenes Konto nun nicht mehr loeschen - Alle fuenf Selbstbedienungszugriffe (Bild hochladen/loeschen/ ausliefern, Akzentfarbe) binden an die Mandantenkennung aus dem Sitzungsnachweis - user.controller.spec.ts (neu): Zwei-Klienten-Nachweis fuer die vorher testlose Steuerungsschicht, 8 Testfaelle, Falsifizierungsnachweis fuer eine gebundene Stelle sowie Rot-vor-Reparatur-Nachweis fuer den Selbstloesch-Riegel (siehe SUMMARY) - docs/mandantentrennung-zugriffsklassifikation.md: alle vier handgepflegten Stellen nachgezogen (Uebersichtszeile 8/14, Summenzeile 118/124, Klassen-Verteilung 63 Paare, Hintergrunddienst-Abschnitt auf fuenf Faelle inkl. admin-seed.service.ts als erster beidseitig korrekter Fall) sowie zwei Klassenkorrekturen (user.service.ts/user und admin-seed.service.ts/user je auf "beides") - docs/mandantentrennung-etappe2-fehlerrichtung.md: Nachtrag zum user-Abschnitt mit den tatsaechlich umgesetzten Pfaden, der geschlossenen Luecke und den Falsifizierungsnachweisen - 810 Tests gruen (8 neue in user.controller.spec.ts), Typpruefung sauber, Wegwerf-Werkzeug meldet weiterhin alle 53 Pruefungen bestanden Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
import { ForbiddenException, NotFoundException } from '@nestjs/common';
|
||||
import { Role } from '@prisma/client';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { UserController } from './user.controller';
|
||||
|
||||
/**
|
||||
* UserController — Zwei-Klienten-Nachweis (260910-das, Aufgabe 3, Befund
|
||||
* C, dritte Form).
|
||||
*
|
||||
* Diese Steuerungsschicht hatte bisher KEINE Testdatei: sie kann auf gar
|
||||
* keinen Fehler rot werden, obwohl in ihr sieben der siebzehn Zugriffe des
|
||||
* Bereichs UND die gesamte Rollenlogik liegen, die entscheidet, wer wessen
|
||||
* Benutzer sehen darf. `UserService` wird hier als Attrappe gestellt — sein
|
||||
* Bindungsverhalten ist bereits in Aufgabe 2 geprueft (`user.service.spec.ts`).
|
||||
* Der direkte Prisma-Zugriff dieses Controllers (findAll-ADMIN-Zweig, die
|
||||
* vier Selbstbedienungswege) bekommt denselben Zwei-Klienten-Nachweis wie
|
||||
* in `groups.service.spec.ts`.
|
||||
*/
|
||||
vi.mock('../prisma/prisma-tenant.extension', () => ({
|
||||
forTenant: vi.fn((prisma: any, tenantId: string) => prisma.__makeBoundClient(tenantId)),
|
||||
}));
|
||||
|
||||
vi.mock('fs', () => ({
|
||||
mkdirSync: vi.fn(),
|
||||
writeFileSync: vi.fn(),
|
||||
existsSync: vi.fn(() => true),
|
||||
unlinkSync: vi.fn(),
|
||||
readFileSync: vi.fn(() => Buffer.from('fake-image-bytes')),
|
||||
}));
|
||||
|
||||
function makeFakePrisma() {
|
||||
const users = new Map<string, any>();
|
||||
const boundCallLog: { tenantId: string; model: string; method: string }[] = [];
|
||||
|
||||
function projectSelect(row: any, select: any) {
|
||||
const projected: any = {};
|
||||
for (const key of Object.keys(select)) {
|
||||
if (select[key]) projected[key] = row[key];
|
||||
}
|
||||
return projected;
|
||||
}
|
||||
|
||||
function scopedFindMany(tenantId: string, args: any) {
|
||||
let rows = Array.from(users.values()).filter((u) => u.tenantId === tenantId);
|
||||
rows = [...rows].sort((a, b) => a.username.localeCompare(b.username));
|
||||
return args?.select ? rows.map((r) => projectSelect(r, args.select)) : rows;
|
||||
}
|
||||
|
||||
function scopedFindUnique(tenantId: string, args: any) {
|
||||
const row = users.get(args.where.id);
|
||||
if (!row || row.tenantId !== tenantId) return null;
|
||||
return args.select ? projectSelect(row, args.select) : row;
|
||||
}
|
||||
|
||||
function scopedUpdate(tenantId: string, args: any) {
|
||||
const row = users.get(args.where.id);
|
||||
if (!row || row.tenantId !== tenantId) {
|
||||
const err: any = new Error('Record to update not found');
|
||||
err.code = 'P2025';
|
||||
throw err;
|
||||
}
|
||||
const updated = { ...row, ...args.data };
|
||||
users.set(args.where.id, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
const fake: any = {
|
||||
__seedUser(user: any) {
|
||||
users.set(user.id, user);
|
||||
},
|
||||
__boundCallLog: boundCallLog,
|
||||
__makeBoundClient(tenantId: string) {
|
||||
return {
|
||||
__isBoundClient: true,
|
||||
__tenantId: tenantId,
|
||||
user: {
|
||||
findMany: async (args: any) => {
|
||||
boundCallLog.push({ tenantId, model: 'user', method: 'findMany' });
|
||||
return scopedFindMany(tenantId, args);
|
||||
},
|
||||
findUnique: async (args: any) => {
|
||||
boundCallLog.push({ tenantId, model: 'user', method: 'findUnique' });
|
||||
return scopedFindUnique(tenantId, args);
|
||||
},
|
||||
update: async (args: any) => {
|
||||
boundCallLog.push({ tenantId, model: 'user', method: 'update' });
|
||||
return scopedUpdate(tenantId, args);
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return fake;
|
||||
}
|
||||
|
||||
function expectBoundCall(prisma: any, tenantId: string, model: string, method: string) {
|
||||
const found = prisma.__boundCallLog.some(
|
||||
(c: any) => c.tenantId === tenantId && c.model === model && c.method === method,
|
||||
);
|
||||
expect(
|
||||
found,
|
||||
`erwarteter gebundener Aufruf ${model}.${method}(tenant=${tenantId}) fehlt im Protokoll: ${JSON.stringify(prisma.__boundCallLog)}`,
|
||||
).toBe(true);
|
||||
}
|
||||
|
||||
function makeUserServiceMock() {
|
||||
return {
|
||||
findById: vi.fn(),
|
||||
findByIdForPlatformAdmin: vi.fn(),
|
||||
findAllForPlatformAdmin: vi.fn(),
|
||||
findByUsername: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
deactivate: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe('UserController', () => {
|
||||
let prisma: any;
|
||||
let userService: any;
|
||||
let controller: UserController;
|
||||
|
||||
beforeEach(() => {
|
||||
prisma = makeFakePrisma();
|
||||
userService = makeUserServiceMock();
|
||||
controller = new UserController(userService as any, prisma);
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('Test 1: die Benutzerliste eines Mandanten-Administrators steht gebunden im Protokoll, trägt die Mandantenkennung aus dem Sitzungsnachweis, und liefert keine Benutzer eines zweiten Mandanten', async () => {
|
||||
prisma.__seedUser({
|
||||
id: 'u-a',
|
||||
username: 'alice',
|
||||
tenantId: 't1',
|
||||
email: 'alice@x.invalid',
|
||||
displayName: null,
|
||||
role: 'USER',
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
lastLoginAt: null,
|
||||
});
|
||||
prisma.__seedUser({
|
||||
id: 'u-b',
|
||||
username: 'bob',
|
||||
tenantId: 't2',
|
||||
email: 'bob@x.invalid',
|
||||
displayName: null,
|
||||
role: 'USER',
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
lastLoginAt: null,
|
||||
});
|
||||
|
||||
const result = await controller.findAll({ role: Role.ADMIN, tenantId: 't1', id: 'admin1' });
|
||||
|
||||
expect(result.map((u: any) => u.username)).toEqual(['alice']);
|
||||
expectBoundCall(prisma, 't1', 'user', 'findMany');
|
||||
});
|
||||
|
||||
it('Test 2: die Benutzerliste des Plattform-Administrators geht über die neue, übergreifende Methode des Dienstes und liefert weiterhin die Benutzer aller Mandanten in der bisherigen Sortierung — die Rollenverzweigung bleibt erhalten', async () => {
|
||||
const expected = [
|
||||
{ id: 'u-a', username: 'alice', tenantId: 't1' },
|
||||
{ id: 'u-b', username: 'bob', tenantId: 't2' },
|
||||
];
|
||||
userService.findAllForPlatformAdmin.mockResolvedValue(expected);
|
||||
|
||||
const result = await controller.findAll({
|
||||
role: Role.SUPER_ADMIN,
|
||||
tenantId: 't1',
|
||||
id: 'super1',
|
||||
});
|
||||
|
||||
expect(result).toBe(expected);
|
||||
expect(userService.findAllForPlatformAdmin).toHaveBeenCalledTimes(1);
|
||||
expect(prisma.__boundCallLog).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Zielbenutzer-Auflösung (findOne/update/remove)', () => {
|
||||
it('Test 3: die drei Wege über die Kennung lösen den Zielbenutzer rollenabhängig auf — für einen Mandanten-Administrator gebunden an dessen eigenen Mandanten, für den Plattform-Administrator über die übergreifende Methode', async () => {
|
||||
const targetUser = { id: 'u-x', username: 'x', tenantId: 't1' };
|
||||
|
||||
userService.findById.mockResolvedValue(targetUser);
|
||||
await controller.findOne('u-x', { role: Role.ADMIN, tenantId: 't1', id: 'admin1' });
|
||||
expect(userService.findById).toHaveBeenCalledWith('t1', 'u-x');
|
||||
|
||||
userService.findByIdForPlatformAdmin.mockResolvedValue(targetUser);
|
||||
await controller.findOne('u-x', { role: Role.SUPER_ADMIN, tenantId: 't2', id: 'super1' });
|
||||
expect(userService.findByIdForPlatformAdmin).toHaveBeenCalledWith('u-x');
|
||||
});
|
||||
|
||||
it('Test 4: ein Mandanten-Administrator, der einen Benutzer eines fremden Mandanten über dessen Kennung anspricht, bekommt weiterhin eine Ablehnung — die gebundene Auflösung liefert bereits null, die Ablehnung ist eine Nicht-gefunden-Ausnahme, keine Verbotene-Ausnahme (die vorgeschaltete, ausdrückliche Mandantenprüfung bleibt trotzdem als zweite Schicht bestehen)', async () => {
|
||||
userService.findById.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
controller.findOne('u-foreign', { role: Role.ADMIN, tenantId: 't1', id: 'admin1' }),
|
||||
).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('Test 5: ein Mandanten-Administrator kann weiterhin keine oberste Rolle vergeben, und die Anlage eines Benutzers landet weiterhin im Mandanten des Aufrufers, wenn dieser nicht die oberste Rolle trägt', async () => {
|
||||
const currentUser = { role: Role.ADMIN, tenantId: 't1', id: 'admin1' };
|
||||
|
||||
await expect(
|
||||
controller.create(
|
||||
{ username: 'x', email: 'x@x.invalid', password: '12345678', role: Role.SUPER_ADMIN } as any,
|
||||
currentUser,
|
||||
),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
|
||||
userService.create.mockResolvedValue({ id: 'u-new', tenantId: 't1' });
|
||||
await controller.create(
|
||||
{ username: 'y', email: 'y@y.invalid', password: '12345678', tenantId: 't9' } as any,
|
||||
currentUser,
|
||||
);
|
||||
expect(userService.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ tenantId: 't1' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove — Selbstlöschriegel (Befund H)', () => {
|
||||
it('Test 6: der Riegel gegen das Löschen des eigenen Kontos greift', async () => {
|
||||
const currentUser = { role: Role.ADMIN, tenantId: 't1', id: 'admin1' };
|
||||
userService.findById.mockResolvedValue({ id: 'admin1', tenantId: 't1' });
|
||||
|
||||
await expect(controller.remove('admin1', currentUser)).rejects.toBeInstanceOf(
|
||||
ForbiddenException,
|
||||
);
|
||||
expect(userService.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Selbstbedienungswege (Befund G)', () => {
|
||||
const currentUser = { role: Role.USER, tenantId: 't1', id: 'me' };
|
||||
|
||||
beforeEach(() => {
|
||||
prisma.__seedUser({
|
||||
id: 'me',
|
||||
username: 'me',
|
||||
tenantId: 't1',
|
||||
avatarPath: 'user-files/avatars/me.png',
|
||||
});
|
||||
});
|
||||
|
||||
it('Test 7: alle fünf Zugriffe der vier Selbstbedienungswege stehen gebunden im Protokoll, mit der Mandantenkennung aus dem Sitzungsnachweis', async () => {
|
||||
await controller.uploadAvatar({ buffer: Buffer.from('x'), mimetype: 'image/png' }, currentUser);
|
||||
await controller.deleteAvatar(currentUser);
|
||||
await controller.updateAccentColor({ color: '#ff00aa' }, currentUser);
|
||||
|
||||
prisma.__seedUser({
|
||||
id: 'me',
|
||||
username: 'me',
|
||||
tenantId: 't1',
|
||||
avatarPath: 'user-files/avatars/me.png',
|
||||
});
|
||||
const res = { setHeader: vi.fn(), send: vi.fn() };
|
||||
await controller.getAvatar(currentUser, res as any);
|
||||
|
||||
const ownUserCalls = prisma.__boundCallLog.filter(
|
||||
(c: any) => c.tenantId === 't1' && c.model === 'user',
|
||||
);
|
||||
// uploadAvatar (1 update) + deleteAvatar (1 findUnique + 1 update) +
|
||||
// updateAccentColor (1 update) + getAvatar (1 findUnique) = 5.
|
||||
expect(ownUserCalls.length).toBe(5);
|
||||
});
|
||||
|
||||
it('Test 8: der Weg, der ein Bild ausliefert, liefert für einen Benutzer ohne hinterlegtes Bild weiterhin die vorhandene Nicht-gefunden-Ausnahme und ändert sein Verhalten nicht', async () => {
|
||||
prisma.__seedUser({ id: 'me', username: 'me', tenantId: 't1', avatarPath: null });
|
||||
const res = { setHeader: vi.fn(), send: vi.fn() };
|
||||
|
||||
await expect(controller.getAvatar(currentUser, res as any)).rejects.toBeInstanceOf(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,7 @@ import { Response } from 'express';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
@@ -40,6 +41,13 @@ function resolveAvatarsDir(): string {
|
||||
return path.resolve(__dirname, '..', '..', '..', '..', 'user-files', 'avatars');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bindung an forTenant() (WINDOWS #20 Etappe 2, 260910-das, Aufgabe 3): alle
|
||||
* sieben Zugriffe dieses Controllers laufen entweder direkt ueber einen
|
||||
* gebundenen Klienten (`tenantPrisma`, Konvention aus `ldap`, `groups`,
|
||||
* `dkv`, `auth`, `user.service.ts`) oder ueber die uebergreifenden Methoden
|
||||
* von `UserService`, deren Rumpf je Mandant gebunden ist.
|
||||
*/
|
||||
@Controller('users')
|
||||
@UseGuards(RolesGuard)
|
||||
export class UserController {
|
||||
@@ -48,6 +56,22 @@ export class UserController {
|
||||
private readonly prisma: PrismaService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Loest den Zielbenutzer rollenabhaengig auf (260910-das, Aufgabe 3): ein
|
||||
* Mandanten-Administrator sieht nur den eigenen Mandanten (gebunden ueber
|
||||
* `UserService.findById`), die oberste Rolle (SUPER_ADMIN) behaelt die
|
||||
* uebergreifende Sicht ueber `UserService.findByIdForPlatformAdmin()` --
|
||||
* diese Verzweigung ist die Stelle, an der dieser Bereich die gewollte
|
||||
* uebergreifende Sicht von der mandantengebundenen unterscheidet, und sie
|
||||
* darf nicht eingeebnet werden.
|
||||
*/
|
||||
private async resolveTargetUser(currentUser: any, id: string) {
|
||||
if (currentUser.role === Role.SUPER_ADMIN) {
|
||||
return this.userService.findByIdForPlatformAdmin(id);
|
||||
}
|
||||
return this.userService.findById(currentUser.tenantId, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /users
|
||||
* ADMIN sees own-tenant users only. SUPER_ADMIN sees all users.
|
||||
@@ -57,24 +81,19 @@ export class UserController {
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async findAll(@CurrentUser() currentUser: any) {
|
||||
if (currentUser.role === Role.SUPER_ADMIN) {
|
||||
return this.prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
displayName: true,
|
||||
role: true,
|
||||
isActive: true,
|
||||
tenantId: true,
|
||||
createdAt: true,
|
||||
lastLoginAt: true,
|
||||
},
|
||||
orderBy: { username: 'asc' },
|
||||
});
|
||||
// Plattform-Administratorsicht (Befund F): die bestehende, gewollte
|
||||
// Funktion der obersten Rolle bleibt erhalten, laeuft aber ueber die
|
||||
// Schleife-je-Mandant-gebunden aus UserService.findAllForPlatformAdmin()
|
||||
// statt ueber ein ungebundenes findMany().
|
||||
return this.userService.findAllForPlatformAdmin();
|
||||
}
|
||||
|
||||
// ADMIN: filter by own tenant
|
||||
return this.prisma.user.findMany({
|
||||
// ADMIN: gebunden an den eigenen Mandanten. Die vorhandene
|
||||
// Mandantenbedingung im where BLEIBT erhalten -- nicht entfernen mit
|
||||
// dem Argument, das mache jetzt die Datenbank; dieselbe Regel, die die
|
||||
// Bereiche `tenders` und `dkv` aufgestellt haben.
|
||||
const tenantPrisma = forTenant(this.prisma, currentUser.tenantId) as any;
|
||||
return tenantPrisma.user.findMany({
|
||||
where: { tenantId: currentUser.tenantId },
|
||||
select: {
|
||||
id: true,
|
||||
@@ -97,13 +116,7 @@ export class UserController {
|
||||
@Get(':id')
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async findOne(@Param('id') id: string, @CurrentUser() currentUser: any) {
|
||||
// 260910-das, Aufgabe 2: UserService.findById() bekommt einen
|
||||
// Pflicht-Mandanten (siehe user.service.ts). Nur die Signatur wird hier
|
||||
// nachgezogen, damit die Typpruefung sauber bleibt -- die Rollenlogik
|
||||
// (insbesondere die uebergreifende SUPER_ADMIN-Sicht ueber
|
||||
// findByIdForPlatformAdmin()) wird erst in Aufgabe 3 vollstaendig
|
||||
// verdrahtet.
|
||||
const user = await this.userService.findById(currentUser.tenantId, id);
|
||||
const user = await this.resolveTargetUser(currentUser, id);
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
@@ -162,8 +175,7 @@ export class UserController {
|
||||
@Body() dto: UpdateUserDto,
|
||||
@CurrentUser() currentUser: any,
|
||||
) {
|
||||
// 260910-das, Aufgabe 2: siehe Kommentar in findOne() oben.
|
||||
const user = await this.userService.findById(currentUser.tenantId, id);
|
||||
const user = await this.resolveTargetUser(currentUser, id);
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
@@ -181,7 +193,12 @@ export class UserController {
|
||||
throw new ForbiddenException('Cannot assign SUPER_ADMIN role');
|
||||
}
|
||||
|
||||
const updated = await this.userService.update(currentUser.tenantId, id, {
|
||||
// Der Schreibzugriff bindet an den Mandanten des ZIELBENUTZERS, wie er
|
||||
// aus der vorangegangenen Aufloesung hervorgeht — NICHT an den des
|
||||
// Aufrufers (260910-das, Aufgabe 3). Nur so bleibt die uebergreifende
|
||||
// Verwaltung durch die oberste Rolle erhalten und ist der
|
||||
// Schreibzugriff trotzdem gebunden.
|
||||
const updated = await this.userService.update(user.tenantId, id, {
|
||||
username: dto.username,
|
||||
email: dto.email,
|
||||
password: dto.password,
|
||||
@@ -201,14 +218,21 @@ export class UserController {
|
||||
@Delete(':id')
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async remove(@Param('id') id: string, @CurrentUser() currentUser: any) {
|
||||
// 260910-das, Aufgabe 2: siehe Kommentar in findOne() oben.
|
||||
const user = await this.userService.findById(currentUser.tenantId, id);
|
||||
const user = await this.resolveTargetUser(currentUser, id);
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
// Cannot delete self
|
||||
if (user.id === currentUser.sub) {
|
||||
// Cannot delete self (260910-das, Befund H): dieser Vergleich verglich
|
||||
// bisher gegen `currentUser.sub` — ein Feld, das der Sitzungsnachweis
|
||||
// GAR NICHT traegt (JwtStrategy.validate() liefert exakt { id,
|
||||
// username, role, tenantId }). Der Riegel hat deshalb NIE gegriffen:
|
||||
// ein Administrator konnte sich selbst loeschen und seinen Mandanten
|
||||
// ohne Verwaltung zuruecklassen. Die Reparatur ist eine
|
||||
// Verhaltensaenderung: ein Administrator kann sein eigenes Konto nun
|
||||
// nicht mehr loeschen — das ist die urspruengliche, im Code bereits
|
||||
// formulierte Absicht.
|
||||
if (user.id === currentUser.id) {
|
||||
throw new ForbiddenException('Cannot delete your own account');
|
||||
}
|
||||
|
||||
@@ -220,13 +244,21 @@ export class UserController {
|
||||
throw new ForbiddenException('Cannot delete users from other tenants');
|
||||
}
|
||||
|
||||
await this.userService.delete(currentUser.tenantId, id);
|
||||
// Gebunden an den Mandanten des ZIELBENUTZERS, derselbe Grund wie bei
|
||||
// update() oben.
|
||||
await this.userService.delete(user.tenantId, id);
|
||||
return { message: 'User deleted' };
|
||||
}
|
||||
|
||||
// ─── Self-service avatar endpoints (all authenticated roles) ───────────────
|
||||
// No @Roles() → RolesGuard.canActivate() returns true when requiredRoles is
|
||||
// empty (see guards/roles.guard.ts). Global JwtAuthGuard still enforces auth.
|
||||
//
|
||||
// Alle fuenf Zugriffe binden vollstaendig an die Mandantenkennung aus dem
|
||||
// Sitzungsnachweis (260910-das, Befund G, Aufgabe 3): der angemeldete
|
||||
// Benutzer liegt per Definition im Mandanten seiner eigenen Sitzung, die
|
||||
// Bindung aendert an Pfaden, Dateityp-Pruefung, Groessenbegrenzung und dem
|
||||
// Aufraeumen alter Bilddateien nichts.
|
||||
|
||||
/**
|
||||
* POST /users/me/avatar
|
||||
@@ -272,7 +304,8 @@ export class UserController {
|
||||
|
||||
// Persist relative path (relative to monorepo root)
|
||||
const relativePath = path.join('user-files', 'avatars', filename);
|
||||
await this.prisma.user.update({
|
||||
const tenantPrisma = forTenant(this.prisma, currentUser.tenantId) as any;
|
||||
await tenantPrisma.user.update({
|
||||
where: { id: currentUser.id },
|
||||
data: { avatarPath: relativePath },
|
||||
});
|
||||
@@ -286,7 +319,8 @@ export class UserController {
|
||||
*/
|
||||
@Delete('me/avatar')
|
||||
async deleteAvatar(@CurrentUser() currentUser: any) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
const tenantPrisma = forTenant(this.prisma, currentUser.tenantId) as any;
|
||||
const user = await tenantPrisma.user.findUnique({
|
||||
where: { id: currentUser.id },
|
||||
select: { avatarPath: true },
|
||||
});
|
||||
@@ -297,7 +331,7 @@ export class UserController {
|
||||
if (fs.existsSync(absolutePath)) {
|
||||
fs.unlinkSync(absolutePath);
|
||||
}
|
||||
await this.prisma.user.update({
|
||||
await tenantPrisma.user.update({
|
||||
where: { id: currentUser.id },
|
||||
data: { avatarPath: null },
|
||||
});
|
||||
@@ -320,7 +354,8 @@ export class UserController {
|
||||
throw new BadRequestException('Invalid color format. Use hex (#rrggbb).');
|
||||
}
|
||||
|
||||
await this.prisma.user.update({
|
||||
const tenantPrisma = forTenant(this.prisma, currentUser.tenantId) as any;
|
||||
await tenantPrisma.user.update({
|
||||
where: { id: currentUser.id },
|
||||
data: { accentColor: body.color ?? null },
|
||||
});
|
||||
@@ -338,7 +373,8 @@ export class UserController {
|
||||
@CurrentUser() currentUser: any,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
const tenantPrisma = forTenant(this.prisma, currentUser.tenantId) as any;
|
||||
const user = await tenantPrisma.user.findUnique({
|
||||
where: { id: currentUser.id },
|
||||
select: { avatarPath: true },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user