diff --git a/apps/api/src/groups/groups.module.ts b/apps/api/src/groups/groups.module.ts index 05c37bf..8af4af4 100644 --- a/apps/api/src/groups/groups.module.ts +++ b/apps/api/src/groups/groups.module.ts @@ -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 {} diff --git a/apps/api/src/groups/module-grants.controller.ts b/apps/api/src/groups/module-grants.controller.ts new file mode 100644 index 0000000..a12eb01 --- /dev/null +++ b/apps/api/src/groups/module-grants.controller.ts @@ -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 }; + } +}