7c9d7c1223
(req as any) und @Req() req: any durch AuthenticatedRequest ersetzt in
dashboard, favorites, calendar, groups, module-grants, module-registry,
tenders, dkv, ldap, settings; @CurrentUser() in user.controller auf AuthUser.
Die abwehrenden Pruefungen ("No tenant context", "No user context") bleiben
lebendig, weil user auf dem Anfragetyp wahlfrei ist - genau das beschreibt
den Zustand auf oeffentlichen Wegen.
Nebengewinn ohne neue Zusicherungen: req.tenantId as string | undefined
(dkv, settings), file.buffer as Buffer und file.mimetype as string
(dkv, user) sind weggefallen, weil der Typ sie jetzt traegt.
BEFUND 1 (D-03, gemeldet) dashboard.controller.ts:74 alt: der Handler las
req.user?.role NACH extractContext und gab sie an getWidgets(role: Role)
weiter, das eine Rolle zwingend verlangt. Die Annahme "hier gibt es immer
einen Aufrufer" stimmt - die Pruefung "No user context" erzwingt sie -, aber
sie stand in einer anderen Methode, wo der Compiler sie nicht sehen konnte.
extractContext gibt die Rolle jetzt mit zurueck: keine neue Pruefung, kein
erfundener Wert, gleiche Reihenfolge, gleiche Meldungen.
BEFUND 2 (D-03, gemeldet) tenders.controller.ts:142: resolveRequestingTenantId
erklaerte string | undefined, liest aber req.tenantId, das TenantGuard fuer
einen SUPER_ADMIN ohne Mandanten auf null setzt. Die Erklaerung war also nie
vollstaendig. Erweitert auf string | null | undefined, und buildTenderWhere
nimmt string | null - beides nur Erklaerung, kein Verhalten: die Funktion
entscheidet seit jeher ueber Wahrheitswert und faellt bei beiden zu
(nur global sichtbare Ausschreibungen).
Fixtures in user.controller.spec.ts ergaenzt (username, mustChangePassword,
originalname, size). Testzahlen unveraendert.
noExplicitAny in apps/api/src: 137 -> 66. type-check 4/4, lint 5/5,
apps/api 72/1143, apps/web 73/531, tenant.guard.ts unveraendert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
92 lines
3.0 KiB
TypeScript
92 lines
3.0 KiB
TypeScript
import {
|
||
Body,
|
||
Controller,
|
||
Delete,
|
||
ForbiddenException,
|
||
Get,
|
||
Param,
|
||
Post,
|
||
Req,
|
||
UseGuards,
|
||
} from '@nestjs/common';
|
||
import { Role } from '@prisma/client';
|
||
import type { AuthenticatedRequest } from '../auth/types/auth-user';
|
||
import { Roles } from '../auth/decorators/roles.decorator';
|
||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||
import { CreateModuleGrantDto } from './dto/create-module-grant.dto';
|
||
import { ModuleGrantsService } from './module-grants.service';
|
||
|
||
/**
|
||
* REST-Controller für die Schreibseite der Modul-Freigaben (PERM-03).
|
||
*
|
||
* tenantId kommt ausschließlich aus dem JWT (req.tenantId ?? req.user?.tenantId),
|
||
* niemals aus Body/Params (T-03-04) — identisch zum Muster in
|
||
* GroupsController/ModuleRegistryController. Jede Route ist rollengeschützt.
|
||
*
|
||
* Statische Segmente stehen vor Parameter-Routen: `matrix` ist vor
|
||
* `users/:userId` deklariert. Dieses Projekt hat den Beschattungsfehler
|
||
* schon einmal gehabt und Unit-Tests fangen ihn nicht.
|
||
*/
|
||
@Controller('module-grants')
|
||
export class ModuleGrantsController {
|
||
constructor(private readonly moduleGrantsService: ModuleGrantsService) {}
|
||
|
||
private getTenantId(req: AuthenticatedRequest): string {
|
||
const tenantId = req.tenantId ?? req.user?.tenantId;
|
||
if (!tenantId) {
|
||
throw new ForbiddenException('No tenant context');
|
||
}
|
||
return tenantId;
|
||
}
|
||
|
||
/**
|
||
* GET /module-grants/matrix
|
||
* Module × Gruppen mit den bestehenden Gruppen-Grants (D-15).
|
||
*/
|
||
@Get('matrix')
|
||
@UseGuards(RolesGuard)
|
||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||
async matrix(@Req() req: AuthenticatedRequest) {
|
||
return this.moduleGrantsService.getMatrix(this.getTenantId(req));
|
||
}
|
||
|
||
/**
|
||
* GET /module-grants/users/:userId
|
||
* Liefert { groups, modules } (D-16). `groups` sind die tatsächlichen
|
||
* Gruppenmitgliedschaften des Benutzers (aus GroupMembership, unabhängig
|
||
* von Modul-Freigaben). `modules` ist je aktivem Modul, über welche
|
||
* Gruppen der Benutzer das Modul erbt, und ob zusätzlich ein
|
||
* Direkt-Grant besteht.
|
||
*/
|
||
@Get('users/:userId')
|
||
@UseGuards(RolesGuard)
|
||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||
async userAccess(@Param('userId') userId: string, @Req() req: AuthenticatedRequest) {
|
||
return this.moduleGrantsService.getUserAccess(this.getTenantId(req), userId);
|
||
}
|
||
|
||
/**
|
||
* POST /module-grants
|
||
* Legt einen Grant für eine Gruppe oder einen Benutzer an.
|
||
*/
|
||
@Post()
|
||
@UseGuards(RolesGuard)
|
||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||
async create(@Body() dto: CreateModuleGrantDto, @Req() req: AuthenticatedRequest) {
|
||
return this.moduleGrantsService.grant(this.getTenantId(req), dto);
|
||
}
|
||
|
||
/**
|
||
* DELETE /module-grants
|
||
* Entzieht einen Grant. Ziel im Body, weil die Kombination aus drei
|
||
* Feldern besteht und nicht sinnvoll in einen Pfadparameter passt.
|
||
*/
|
||
@Delete()
|
||
@UseGuards(RolesGuard)
|
||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||
async remove(@Body() dto: CreateModuleGrantDto, @Req() req: AuthenticatedRequest) {
|
||
await this.moduleGrantsService.revoke(this.getTenantId(req), dto);
|
||
return { success: true };
|
||
}
|
||
}
|