From 8bfb8912c15a3a58e3b28e0b725de0b6a66db0ea Mon Sep 17 00:00:00 2001 From: Consultoria AS Date: Sun, 15 Mar 2026 23:19:12 +0000 Subject: [PATCH] feat: rewrite tenant middleware for pool-based tenant resolution - Resolve tenant DB via TenantConnectionManager instead of SET search_path - Add tenantPool to Express Request for direct pool queries - Keep tenantSchema as backward compat until all services are migrated - Support admin impersonation via X-View-Tenant header Co-Authored-By: Claude Opus 4.6 --- apps/api/src/middlewares/tenant.middleware.ts | 65 +++++++++++-------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/apps/api/src/middlewares/tenant.middleware.ts b/apps/api/src/middlewares/tenant.middleware.ts index 07f67ed..67d08fd 100644 --- a/apps/api/src/middlewares/tenant.middleware.ts +++ b/apps/api/src/middlewares/tenant.middleware.ts @@ -1,48 +1,61 @@ import type { Request, Response, NextFunction } from 'express'; -import { prisma } from '../config/database.js'; -import { AppError } from './error.middleware.js'; +import type { Pool } from 'pg'; +import { prisma, tenantDb } from '../config/database.js'; declare global { namespace Express { interface Request { - tenantSchema?: string; + tenantPool?: Pool; + tenantSchema?: string; // @deprecated - use tenantPool instead viewingTenantId?: string; } } } export async function tenantMiddleware(req: Request, res: Response, next: NextFunction) { - if (!req.user) { - return next(new AppError(401, 'No autenticado')); - } - try { - // Check if admin is viewing a different tenant - const viewTenantId = req.headers['x-view-tenant'] as string | undefined; + if (!req.user) { + return res.status(401).json({ message: 'No autenticado' }); + } + let tenantId = req.user.tenantId; + let databaseName = req.user.databaseName; - // Only admins can view other tenants - if (viewTenantId && req.user.role === 'admin') { - tenantId = viewTenantId; - req.viewingTenantId = viewTenantId; + // Admin impersonation via X-View-Tenant header + const viewTenantHeader = req.headers['x-view-tenant'] as string; + if (viewTenantHeader && req.user.role === 'admin') { + const viewedTenant = await prisma.tenant.findFirst({ + where: { + OR: [ + { id: viewTenantHeader }, + { rfc: viewTenantHeader }, + ], + }, + select: { id: true, databaseName: true, active: true }, + }); + + if (!viewedTenant) { + return res.status(404).json({ message: 'Tenant no encontrado' }); + } + + if (!viewedTenant.active) { + return res.status(403).json({ message: 'Tenant inactivo' }); + } + + tenantId = viewedTenant.id; + databaseName = viewedTenant.databaseName; + req.viewingTenantId = viewedTenant.id; } - const tenant = await prisma.tenant.findUnique({ - where: { id: tenantId }, - select: { databaseName: true, active: true }, - }); + // New pool-based approach + req.tenantPool = tenantDb.getPool(tenantId, databaseName); - if (!tenant || !tenant.active) { - return next(new AppError(403, 'Tenant no encontrado o inactivo')); - } - - req.tenantSchema = tenant.databaseName; - - // Set search_path for this request (will be replaced by pool-based approach) - await prisma.$executeRawUnsafe(`SET search_path TO "${tenant.databaseName}", public`); + // Backward compat: tenantSchema still used by controllers until Task 8 migration + req.tenantSchema = databaseName; next(); } catch (error) { - next(new AppError(500, 'Error al configurar tenant')); + console.error('[TenantMiddleware] Error:', error); + return res.status(500).json({ message: 'Error al resolver tenant' }); } }