import { BadRequestException, Body, Controller, Delete, Get, NotFoundException, Param, Patch, Post, UseGuards, } from '@nestjs/common'; import { Role } from '@prisma/client'; 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 { CreateTenantDto } from './dto/create-tenant.dto'; import { TenantService } from './tenant.service'; /** * Tenant CRUD controller. * D-10: Only Super-Admin can manage tenants. * T-02-09: Tenant deletion blocked if active users exist. * * User counts (260911-e2s, Aufgabe 3): `findAll`/`findOne`/`remove` used to * read the user count through a relation include on the four unbound * tenant reads below. Prisma renders that as a single statement with a * LEFT JOIN into the protected `User` table — which carries a row-level * security rule. After the switch flips, an unbound relation count reads * zero for every tenant (measured, 260911-e2s Aufgabe 1, Pruefungen 5-7), * which would make the platform-admin tenant list show zero users * everywhere and let the delete gate below pass through with active users * still present. Fixed by the fan-out pattern `UserService * .findAllForPlatformAdmin` already uses: read tenants unbound, then bind * ONE user count per tenant via the tenant-binding helper. The explicit * `where: { tenantId }` on each bound count is TODAY (role runs with * BYPASSRLS, WINDOWS #18) the only filter actually in effect. * * The four tenant reads/writes below stay unbound on purpose — `Tenant` * carries no row-level security rule in any shipped migration (measured, * 260911-e2s Aufgabe 1, Pruefungen 1/2); nothing on `Tenant` itself needs * binding. * * This controller keeps talking to Prisma directly rather than going * through a service method (same pattern as `user.controller.ts`) — a * deliberate choice, not an oversight; see * docs/mandantentrennung-zugriffsklassifikation.md, "(n5)". */ @Controller('tenants') @UseGuards(RolesGuard) @Roles(Role.SUPER_ADMIN) export class TenantController { constructor( private readonly tenantService: TenantService, private readonly prisma: PrismaService, ) {} /** * GET /tenants * Returns all tenants with user count. */ @Get() async findAll() { const tenants = await this.prisma.tenant.findMany({ orderBy: { name: 'asc' }, }); const results: any[] = []; for (const tenant of tenants) { const tenantPrisma = forTenant(this.prisma, tenant.id) as any; const userCount = await tenantPrisma.user.count({ where: { tenantId: tenant.id }, }); results.push({ id: tenant.id, name: tenant.name, slug: tenant.slug, isActive: tenant.isActive, createdAt: tenant.createdAt, userCount, }); } return results; } /** * GET /tenants/:id * Returns single tenant with user count. */ @Get(':id') async findOne(@Param('id') id: string) { const tenant = await this.prisma.tenant.findUnique({ where: { id }, }); if (!tenant) { throw new NotFoundException('Tenant not found'); } const tenantPrisma = forTenant(this.prisma, id) as any; const userCount = await tenantPrisma.user.count({ where: { tenantId: id }, }); return { id: tenant.id, name: tenant.name, slug: tenant.slug, isActive: tenant.isActive, createdAt: tenant.createdAt, userCount, }; } /** * POST /tenants * Create a new tenant. */ @Post() async create(@Body() dto: CreateTenantDto) { return this.tenantService.create({ name: dto.name, slug: dto.slug, }); } /** * PATCH /tenants/:id * Update tenant name or isActive. */ @Patch(':id') async update( @Param('id') id: string, @Body() dto: { name?: string; isActive?: boolean }, ) { const existing = await this.tenantService.findById(id); if (!existing) { throw new NotFoundException('Tenant not found'); } return this.tenantService.update(id, { name: dto.name, isActive: dto.isActive, }); } /** * DELETE /tenants/:id * T-02-09: Only delete if no active users exist. */ @Delete(':id') async remove(@Param('id') id: string) { const tenant = await this.prisma.tenant.findUnique({ where: { id }, }); if (!tenant) { throw new NotFoundException('Tenant not found'); } const tenantPrisma = forTenant(this.prisma, id) as any; const activeUserCount = await tenantPrisma.user.count({ where: { tenantId: id, isActive: true }, }); if (activeUserCount > 0) { throw new BadRequestException( 'Cannot delete tenant with active users. Deactivate or reassign users first.', ); } await this.prisma.tenant.delete({ where: { id } }); return { message: 'Tenant deleted' }; } }