From 8320a34035488887f2c582ff1d55cc569069ae3a Mon Sep 17 00:00:00 2001 From: Schalli Date: Sat, 27 Jun 2026 20:57:03 +0200 Subject: [PATCH] fix(07): replace TenantMiddleware with TenantGuard to fix tenant context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Middleware runs before guards in NestJS — req.user was always undefined when TenantMiddleware executed, so req.tenantId was never set. Convert to TenantGuard (APP_GUARD, registered after JwtAuthGuard) so it runs after JWT validation and can read req.user.tenantId correctly. Co-Authored-By: Claude Sonnet 4.6 --- apps/api/src/app.module.ts | 16 ++++----- apps/api/src/tenant/tenant.guard.ts | 50 +++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 apps/api/src/tenant/tenant.guard.ts diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 56b87bb..5aecc53 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -1,4 +1,4 @@ -import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { ScheduleModule } from '@nestjs/schedule'; @@ -16,7 +16,7 @@ import { DomaincheckModule } from './domaincheck/domaincheck.module'; import { ModuleRegistryModule } from './module-registry/module-registry.module'; import { PrismaModule } from './prisma/prisma.module'; import { SettingsModule } from './settings/settings.module'; -import { TenantMiddleware } from './tenant/tenant.middleware'; +import { TenantGuard } from './tenant/tenant.guard'; import { TenantModule } from './tenant/tenant.module'; import { UserModule } from './user/user.module'; @@ -44,6 +44,11 @@ import { UserModule } from './user/user.module'; provide: APP_GUARD, useClass: JwtAuthGuard, }, + // Runs after JwtAuthGuard — sets req.tenantId and req.tenantPrisma from req.user + { + provide: APP_GUARD, + useClass: TenantGuard, + }, // Global roles guard: checks @Roles() decorator { provide: APP_GUARD, @@ -56,9 +61,4 @@ import { UserModule } from './user/user.module'; }, ], }) -export class AppModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - // TenantMiddleware runs AFTER AuthGuard (guards run first in NestJS pipeline) - consumer.apply(TenantMiddleware).forRoutes('*'); - } -} +export class AppModule {} diff --git a/apps/api/src/tenant/tenant.guard.ts b/apps/api/src/tenant/tenant.guard.ts new file mode 100644 index 0000000..ef36bc1 --- /dev/null +++ b/apps/api/src/tenant/tenant.guard.ts @@ -0,0 +1,50 @@ +import { + CanActivate, + ExecutionContext, + ForbiddenException, + Injectable, +} from '@nestjs/common'; +import { forTenant } from '../prisma/prisma-tenant.extension'; +import { PrismaService } from '../prisma/prisma.service'; + +/** + * Runs AFTER JwtAuthGuard (guard execution order follows APP_GUARD registration order). + * At this point req.user is populated — middleware ran too early to access it. + * + * Sets req.tenantId and req.tenantPrisma for downstream controllers. + * Super-Admin can override tenant via x-tenant-id header (D-10). + */ +@Injectable() +export class TenantGuard implements CanActivate { + constructor(private readonly prisma: PrismaService) {} + + canActivate(context: ExecutionContext): boolean { + const req = context.switchToHttp().getRequest(); + const user = req.user; + + if (!user) { + // Public route (login, health) — skip tenant context + return true; + } + + let tenantId: string | undefined = user.tenantId; + + if (user.role === 'SUPER_ADMIN' && req.headers['x-tenant-id']) { + tenantId = req.headers['x-tenant-id'] as string; + } + + if (!tenantId && user.role !== 'SUPER_ADMIN') { + throw new ForbiddenException('No tenant context'); + } + + if (tenantId) { + req.tenantPrisma = forTenant(this.prisma, tenantId); + req.tenantId = tenantId; + } else { + req.tenantPrisma = this.prisma; + req.tenantId = null; + } + + return true; + } +}