import { Injectable, NestMiddleware } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { DataSource } from 'typeorm'; import { Request, Response, NextFunction } from 'express'; import * as jwt from 'jsonwebtoken'; export interface TenantRequest extends Request { tenantSchema?: string; orgId?: string; userId?: string; userRole?: string; orgPastDue?: boolean; } @Injectable() export class TenantMiddleware implements NestMiddleware { // In-memory cache for org info to avoid DB hit per request private orgCache = new Map(); private static readonly CACHE_TTL = 60_000; // 60 seconds constructor( private configService: ConfigService, private dataSource: DataSource, ) {} async use(req: TenantRequest, res: Response, next: NextFunction) { // Try to extract tenant info from Authorization header JWT const authHeader = req.headers.authorization; if (authHeader && authHeader.startsWith('Bearer ')) { try { const token = authHeader.substring(7); const secret = this.configService.get('JWT_SECRET'); const decoded = jwt.verify(token, secret!) as any; if (decoded?.orgId) { // Look up org info (status + schema) from orgId with caching const orgInfo = await this.getOrgInfo(decoded.orgId); if (orgInfo) { if (['suspended', 'archived'].includes(orgInfo.status)) { res.status(403).json({ statusCode: 403, message: `This organization has been ${orgInfo.status}. Please contact your administrator.`, }); return; } // past_due: allow through with read-only flag (WriteAccessGuard enforces) if (orgInfo.status === 'past_due') { req.orgPastDue = true; } req.tenantSchema = orgInfo.schemaName; } req.orgId = decoded.orgId; req.userId = decoded.sub; req.userRole = decoded.role; } else if (decoded?.sub) { // Superadmin or user without org — still set userId req.userId = decoded.sub; } } catch { // Token invalid or expired - let Passport handle the auth error } } next(); } private async getOrgInfo(orgId: string): Promise<{ status: string; schemaName: string } | null> { const cached = this.orgCache.get(orgId); if (cached && Date.now() - cached.cachedAt < TenantMiddleware.CACHE_TTL) { return { status: cached.status, schemaName: cached.schemaName }; } try { const result = await this.dataSource.query( `SELECT status, schema_name as "schemaName" FROM shared.organizations WHERE id = $1`, [orgId], ); if (result.length > 0) { this.orgCache.set(orgId, { status: result[0].status, schemaName: result[0].schemaName, cachedAt: Date.now(), }); return { status: result[0].status, schemaName: result[0].schemaName }; } } catch { // Non-critical — don't block requests on cache miss errors } return null; } }