/** * Tenant Middleware - RLS context for multi-tenancy * * @module @erp-suite/core/middleware */ import { Request, Response, NextFunction } from 'express'; import { AuthRequest } from './auth.middleware'; /** * Extended Express Request with tenant context */ export interface TenantRequest extends AuthRequest { tenantId?: string; } /** * Database query function type */ export type QueryFn = (sql: string, params: unknown[]) => Promise; /** * Tenant middleware configuration */ export interface TenantMiddlewareConfig { query: QueryFn; skipPaths?: string[]; } /** * Creates a tenant middleware that sets RLS context * * This middleware must run after auth middleware to access user.tenantId. * It sets the PostgreSQL session variable for Row-Level Security (RLS). * * @param config - Middleware configuration * @returns Express middleware function * * @example * ```typescript * import { createTenantMiddleware } from '@erp-suite/core/middleware'; * * const tenantMiddleware = createTenantMiddleware({ * query: (sql, params) => pool.query(sql, params), * skipPaths: ['/health'], * }); * * app.use(authMiddleware); * app.use(tenantMiddleware); * ``` */ export function createTenantMiddleware(config: TenantMiddlewareConfig) { return async ( req: TenantRequest, res: Response, next: NextFunction, ): Promise => { // Skip tenant context for certain paths if (config.skipPaths?.some((path) => req.path.startsWith(path))) { return next(); } // Extract tenant ID from authenticated user const tenantId = req.user?.tenantId; if (!tenantId) { res.status(401).json({ error: 'No tenant context available' }); return; } try { // Set PostgreSQL session variable for RLS await config.query('SET LOCAL app.current_tenant_id = $1', [tenantId]); req.tenantId = tenantId; next(); } catch (error) { console.error('Failed to set tenant context:', error); res.status(500).json({ error: 'Failed to set tenant context' }); } }; } /** * NestJS Interceptor for tenant context * * @example * ```typescript * import { TenantInterceptor } from '@erp-suite/core/middleware'; * * @Controller('api') * @UseInterceptors(TenantInterceptor) * export class ApiController { * // Tenant-isolated routes * } * ``` */ export class TenantInterceptor { constructor(private readonly query: QueryFn) {} async intercept(context: any, next: any): Promise { const request = context.switchToHttp().getRequest(); const tenantId = request.user?.tenantId; if (!tenantId) { throw new Error('No tenant context available'); } // Set PostgreSQL session variable for RLS await this.query('SET LOCAL app.current_tenant_id = $1', [tenantId]); request.tenantId = tenantId; return next.handle(); } }