c8de72e762
findAll/findOne/remove zaehlen Benutzer je Mandant jetzt ueber drei
gebundene Aufrufstellen (tenantPrisma.user.count mit where: { tenantId
}, in remove zusaetzlich isActive: true) statt ueber den
Relationszaehler, der nach dem Scharfschalten unbemerkt unter der
Regel von User gelaufen waere (260911-e2s, Aufgabe 1, Pruefungen 5-7).
Fan-out-Muster aus UserService.findAllForPlatformAdmin uebernommen; die
vier tenant-Zugriffe bleiben ungebunden (Tenant ohne Regel). Antwortform,
Meldungen und Statuscodes unveraendert.
tenant.controller.spec.ts legt die Testlage aus dem Nichts an (20
Faelle): Zwei-Klienten-Nachweis ueber __makeBoundClient, Rollen-
Metadaten-Test (Klasse SUPER_ADMIN, kein Handler ueberschreibt), Wachhund
gegen mehrfache Klientenerzeugung. Falsifizierungsnachweis durchgefuehrt:
der probeweise ungebundene Zaehler in findOne macht 2 Faelle rot mit
"Cannot read properties of undefined (reading 'count')" — die dkv-Form
der Falsifizierung, nicht nur eine falsche Zahl —, danach zurueckgenommen.
Klassifikation und Entwicklungsanleitung nachgezogen: 64 Paare (ein
neues, tenant.controller.ts/user), Uebersichtszeile 8/3, Klassen-
Verteilung 32 muss-mandantengebunden, Erkennungsluecke fuer
Relationseinbindungen im Kopf der Bestandsaufnahme benannt, "Zwei
belegte Befunde" und "Was diese Etappe NICHT entscheidet" (erster
Punkt aufgeloest). Beide Dokument-Falsifizierungsnachweise durchgefuehrt
(falsche Klasse macht rls-access-inventory.spec.ts rot, falsche
Uebersichtszahl macht das herleitende Gate rot), zurueckgenommen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
178 lines
4.9 KiB
TypeScript
178 lines
4.9 KiB
TypeScript
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' };
|
|
}
|
|
}
|