import { Injectable, NestMiddleware, Logger } from '@nestjs/common'; import { FastifyRequest, FastifyReply } from 'fastify'; import { TenantDatabaseService } from './tenant-database.service'; @Injectable() export class TenantMiddleware implements NestMiddleware { private readonly logger = new Logger(TenantMiddleware.name); constructor(private readonly tenantDbService: TenantDatabaseService) {} async use( req: FastifyRequest['raw'], res: FastifyReply['raw'], next: () => void, ) { try { // Extract subdomain from hostname const host = req.headers.host || ''; const hostname = host.split(':')[0]; // Remove port if present // Check Origin header to get frontend subdomain (for API calls) const origin = req.headers.origin as string; const referer = req.headers.referer as string; let parts = hostname.split('.'); this.logger.log(`Host header: ${host}, hostname: ${hostname}, origin: ${origin}, referer: ${referer}, parts: ${JSON.stringify(parts)}`); // For local development, accept x-tenant-id header let tenantId = req.headers['x-tenant-id'] as string; let subdomain: string | null = null; this.logger.log(`Host header: ${host}, hostname: ${hostname}, parts: ${JSON.stringify(parts)}, x-tenant-id: ${tenantId}`); // Try to extract subdomain from Origin header first (for API calls from frontend) if (origin) { try { const originUrl = new URL(origin); const originHost = originUrl.hostname; parts = originHost.split('.'); this.logger.log(`Using Origin header hostname: ${originHost}, parts: ${JSON.stringify(parts)}`); } catch (error) { this.logger.warn(`Failed to parse origin: ${origin}`); } } else if (referer && !tenantId) { // Fallback to Referer if no Origin try { const refererUrl = new URL(referer); const refererHost = refererUrl.hostname; parts = refererHost.split('.'); this.logger.log(`Using Referer header hostname: ${refererHost}, parts: ${JSON.stringify(parts)}`); } catch (error) { this.logger.warn(`Failed to parse referer: ${referer}`); } } // Extract subdomain (e.g., "tenant1" from "tenant1.routebox.co") // For production domains with 3+ parts, extract first part as subdomain if (parts.length >= 3) { subdomain = parts[0]; // Ignore www subdomain if (subdomain === 'www') { subdomain = null; } } // For development (e.g., tenant1.localhost), also check 2 parts else if (parts.length === 2 && parts[1] === 'localhost') { subdomain = parts[0]; } this.logger.log(`Extracted subdomain: ${subdomain}`); // Always attach subdomain to request if present if (subdomain) { (req as any).subdomain = subdomain; } // If x-tenant-id is explicitly provided, use it directly but still keep subdomain if (tenantId) { this.logger.log(`Using explicit x-tenant-id: ${tenantId}`); (req as any).tenantId = tenantId; next(); return; } // Always attach subdomain to request if present if (subdomain) { (req as any).subdomain = subdomain; } // Check if this is a central subdomain const centralSubdomains = (process.env.CENTRAL_SUBDOMAINS || 'central,admin').split(','); const isCentral = subdomain && centralSubdomains.includes(subdomain); // If it's a central subdomain, skip tenant resolution if (isCentral) { this.logger.log(`Central subdomain detected: ${subdomain}, skipping tenant resolution`); (req as any).subdomain = subdomain; next(); return; } // Get tenant by subdomain if available if (subdomain) { try { const tenant = await this.tenantDbService.getTenantByDomain(subdomain); if (tenant) { tenantId = tenant.id; this.logger.log( `Tenant identified: ${tenant.name} (${tenant.id}) from subdomain: ${subdomain}`, ); } } catch (error) { this.logger.warn(`No tenant found for subdomain: ${subdomain}`, error.message); // Fall back to using subdomain as tenantId directly if domain lookup fails tenantId = subdomain; this.logger.log(`Using subdomain as tenantId fallback: ${tenantId}`); } } if (tenantId) { // Attach tenant info to request object (req as any).tenantId = tenantId; } else { this.logger.warn(`No tenant identified from host: ${hostname}`); } next(); } catch (error) { this.logger.error('Error in tenant middleware', error); next(); } } }