erp-suite/apps/shared-libs/core/middleware/tenant.middleware.ts

115 lines
2.9 KiB
TypeScript

/**
* 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<unknown[]>;
/**
* 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<void> => {
// 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<any> {
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();
}
}