/** * TENANT GUARD - REFERENCE IMPLEMENTATION * * @description Guard para validación de multi-tenancy. * Asegura que el usuario pertenece al tenant correcto. * * @usage * ```typescript * @UseGuards(JwtAuthGuard, TenantGuard) * @Get('data') * getData(@CurrentTenant() tenant: Tenant) { ... } * ``` * * @origin gamilit/apps/backend/src/shared/guards/tenant.guard.ts */ import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; // Adaptar imports según proyecto // import { Tenant } from '../entities'; /** * Metadata key para configuración de tenant */ export const TENANT_KEY = 'tenant'; export const SKIP_TENANT_KEY = 'skipTenant'; @Injectable() export class TenantGuard implements CanActivate { constructor( private readonly reflector: Reflector, @InjectRepository(Tenant, 'auth') private readonly tenantRepository: Repository, ) {} async canActivate(context: ExecutionContext): Promise { // Verificar si la ruta debe saltarse la validación de tenant const skipTenant = this.reflector.getAllAndOverride(SKIP_TENANT_KEY, [ context.getHandler(), context.getClass(), ]); if (skipTenant) { return true; } const request = context.switchToHttp().getRequest(); const user = request.user; if (!user) { throw new ForbiddenException('Usuario no autenticado'); } // Obtener tenant del usuario (asumiendo que está en el JWT o perfil) const tenantId = user.tenant_id || request.headers['x-tenant-id']; if (!tenantId) { throw new ForbiddenException('Tenant no especificado'); } // Validar que el tenant existe y está activo const tenant = await this.tenantRepository.findOne({ where: { id: tenantId, is_active: true }, }); if (!tenant) { throw new ForbiddenException('Tenant inválido o inactivo'); } // Inyectar tenant en request para uso posterior request.tenant = tenant; return true; } } // ============ DECORADORES ============ import { SetMetadata, createParamDecorator } from '@nestjs/common'; /** * Decorador para saltar validación de tenant */ export const SkipTenant = () => SetMetadata(SKIP_TENANT_KEY, true); /** * Decorador para obtener el tenant actual */ export const CurrentTenant = createParamDecorator( (data: unknown, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); return request.tenant; }, ); // ============ RLS HELPER ============ /** * Helper para aplicar Row-Level Security en queries * * @usage * ```typescript * const users = await this.userRepo * .createQueryBuilder('user') * .where(withTenant('user', tenantId)) * .getMany(); * ``` */ export function withTenant(alias: string, tenantId: string): string { return `${alias}.tenant_id = '${tenantId}'`; } /** * Interceptor para inyectar tenant_id automáticamente en creates * * @usage En el servicio base * ```typescript * async create(dto: CreateDto, tenantId: string) { * const entity = this.repo.create({ * ...dto, * tenant_id: tenantId, * }); * return this.repo.save(entity); * } * ``` */ export function injectTenantId( entity: T, tenantId: string, ): T { return { ...entity, tenant_id: tenantId }; } // ============ TIPOS ============ interface Tenant { id: string; name: string; slug: string; is_active: boolean; }