115 lines
2.9 KiB
TypeScript
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();
|
|
}
|
|
}
|