feat(260911-e2s): Benutzerzaehler im TenantController binden (Fan-out je Mandant)
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
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
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';
|
||||
@@ -21,6 +22,30 @@ 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)
|
||||
@@ -38,22 +63,26 @@ export class TenantController {
|
||||
@Get()
|
||||
async findAll() {
|
||||
const tenants = await this.prisma.tenant.findMany({
|
||||
include: {
|
||||
_count: {
|
||||
select: { users: true },
|
||||
},
|
||||
},
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
|
||||
return tenants.map((t: any) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
slug: t.slug,
|
||||
isActive: t.isActive,
|
||||
createdAt: t.createdAt,
|
||||
userCount: t._count.users,
|
||||
}));
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,24 +93,24 @@ export class TenantController {
|
||||
async findOne(@Param('id') id: string) {
|
||||
const tenant = await this.prisma.tenant.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
_count: {
|
||||
select: { users: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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: tenant._count.users,
|
||||
userCount,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,20 +154,18 @@ export class TenantController {
|
||||
async remove(@Param('id') id: string) {
|
||||
const tenant = await this.prisma.tenant.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
users: { where: { isActive: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!tenant) {
|
||||
throw new NotFoundException('Tenant not found');
|
||||
}
|
||||
|
||||
if (tenant._count.users > 0) {
|
||||
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.',
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user