feat(15-03): ModuleGrantsController und Einbindung in GroupsModule

- GET /module-grants/matrix, GET /module-grants/users/:userId,
  POST /module-grants, DELETE /module-grants — alle vier rollengeschützt
  (RolesGuard + Roles ADMIN/SUPER_ADMIN)
- matrix vor users/:userId deklariert (Beschattungsfehler-Vermeidung)
- GroupsModule bindet ModuleGrantsController/-Service ein; kein Import
  von ModuleRegistryModule nötig, da der Service nur PrismaService braucht
This commit is contained in:
2026-08-04 18:41:17 +02:00
parent 5e256db01d
commit 072fb7f62f
2 changed files with 99 additions and 4 deletions
+11 -4
View File
@@ -1,17 +1,24 @@
import { Module } from '@nestjs/common';
import { GroupsController } from './groups.controller';
import { GroupsService } from './groups.service';
import { ModuleGrantsController } from './module-grants.controller';
import { ModuleGrantsService } from './module-grants.service';
/**
* NestJS-Modul für die Gruppenverwaltung (PERM-01, PERM-06).
* NestJS-Modul für die Gruppenverwaltung (PERM-01, PERM-06) und die
* Schreibseite der Modul-Freigaben (PERM-03, Plan 15-03).
*
* Exportiert GroupsService, damit UserModule (Plan 15-02 Task 2) darüber
* addUserToDefaultGroup aufrufen kann — der einzige Codepfad für die
* automatische Standardgruppen-Mitgliedschaft (D-11/D-12).
*
* ModuleGrantsService injiziert ausschließlich PrismaService, deshalb ist
* kein Import von ModuleRegistryModule nötig — anders als
* ModuleAccessService (15-01/15-05), das hier nicht verwendet wird.
*/
@Module({
controllers: [GroupsController],
providers: [GroupsService],
exports: [GroupsService],
controllers: [GroupsController, ModuleGrantsController],
providers: [GroupsService, ModuleGrantsService],
exports: [GroupsService, ModuleGrantsService],
})
export class GroupsModule {}
@@ -0,0 +1,88 @@
import {
Body,
Controller,
Delete,
ForbiddenException,
Get,
Param,
Post,
Req,
UseGuards,
} from '@nestjs/common';
import { Role } from '@prisma/client';
import { Request } from 'express';
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: Request): string {
const tenantId = (req as any).tenantId ?? (req as any).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: Request) {
return this.moduleGrantsService.getMatrix(this.getTenantId(req));
}
/**
* GET /module-grants/users/:userId
* Je aktivem Modul: über welche Gruppen der Benutzer erbt, und ob ein
* Direkt-Grant besteht (D-16).
*/
@Get('users/:userId')
@UseGuards(RolesGuard)
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async userAccess(@Param('userId') userId: string, @Req() req: Request) {
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: Request) {
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: Request) {
await this.moduleGrantsService.revoke(this.getTenantId(req), dto);
return { success: true };
}
}