feat(15-02): GroupsModule — CRUD für Gruppen, Mitgliedschaften und Löschauswirkung
- GroupsService: listForTenant/create/update/remove/getImpact/listMembers/addMembers/removeMember/addUserToDefaultGroup, jede Query tenantId-gescoped (T-15-02/T-15-12)
- isDefault:true läuft in einer Transaktion (updateMany+update), D-13
- getImpact liefert { memberCount, grantCount } für den Löschdialog (D-17)
- removeMember beschränkt sich auf source:MANUAL (D-19)
- GroupsController: 8 rollengeschützte Routen unter /groups
- 19 Tests in groups.service.spec.ts, hand-rolled In-Memory-Fake
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
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 { AddGroupMembersDto } from './dto/add-group-members.dto';
|
||||
import { CreateGroupDto } from './dto/create-group.dto';
|
||||
import { UpdateGroupDto } from './dto/update-group.dto';
|
||||
import { GroupsService } from './groups.service';
|
||||
|
||||
/**
|
||||
* REST-Controller für die Gruppenverwaltung (PERM-01, D-14).
|
||||
*
|
||||
* tenantId kommt ausschließlich aus dem JWT (req.tenantId ?? req.user?.tenantId),
|
||||
* niemals aus Body/Params (T-03-04). Jede Route ist rollengeschützt —
|
||||
* identisch zu den Modul-Aktivierungsrouten in ModuleRegistryController.
|
||||
*/
|
||||
@Controller('groups')
|
||||
export class GroupsController {
|
||||
constructor(private readonly groupsService: GroupsService) {}
|
||||
|
||||
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 /groups
|
||||
* Liste aller Gruppen des Mandanten, alphabetisch, mit Mitgliederzahl.
|
||||
*/
|
||||
@Get()
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async list(@Req() req: Request) {
|
||||
return this.groupsService.listForTenant(this.getTenantId(req));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /groups
|
||||
* Legt eine neue Gruppe im Mandanten an.
|
||||
*/
|
||||
@Post()
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async create(@Body() dto: CreateGroupDto, @Req() req: Request) {
|
||||
return this.groupsService.create(this.getTenantId(req), dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /groups/:id
|
||||
* Umbenennen, Standardmarkierung setzen/entfernen, AD-Bindung setzen/lösen.
|
||||
*/
|
||||
@Patch(':id')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateGroupDto,
|
||||
@Req() req: Request,
|
||||
) {
|
||||
return this.groupsService.update(this.getTenantId(req), id, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /groups/:id
|
||||
* Löscht die Gruppe; Mitgliedschaften und Grants folgen per Cascade (D-17).
|
||||
*/
|
||||
@Delete(':id')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async remove(@Param('id') id: string, @Req() req: Request) {
|
||||
return this.groupsService.remove(this.getTenantId(req), id);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /groups/:id/impact
|
||||
* Zahlenmaterial für den Löschdialog (D-17): { memberCount, grantCount }.
|
||||
*/
|
||||
@Get(':id/impact')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async impact(@Param('id') id: string, @Req() req: Request) {
|
||||
return this.groupsService.getImpact(this.getTenantId(req), id);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /groups/:id/members
|
||||
* Mitgliederliste inkl. Kern-Benutzerdaten.
|
||||
*/
|
||||
@Get(':id/members')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async members(@Param('id') id: string, @Req() req: Request) {
|
||||
return this.groupsService.listMembers(this.getTenantId(req), id);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /groups/:id/members
|
||||
* Fügt Mitglieder manuell hinzu (source: MANUAL).
|
||||
*/
|
||||
@Post(':id/members')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async addMembers(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: AddGroupMembersDto,
|
||||
@Req() req: Request,
|
||||
) {
|
||||
return this.groupsService.addMembers(this.getTenantId(req), id, dto.userIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /groups/:id/members/:userId
|
||||
* Entfernt ein manuell hinzugefügtes Mitglied (LDAP-Mitgliedschaften
|
||||
* bleiben unberührt, D-19).
|
||||
*/
|
||||
@Delete(':id/members/:userId')
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async removeMember(
|
||||
@Param('id') id: string,
|
||||
@Param('userId') userId: string,
|
||||
@Req() req: Request,
|
||||
) {
|
||||
await this.groupsService.removeMember(this.getTenantId(req), id, userId);
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user