express-multitenancy
Version:
Express middleware for managing multi-tenant applications with configurable tenant resolution strategies
293 lines (292 loc) • 10.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ClaimStrategy = exports.BasePathStrategy = exports.RouteStrategy = exports.HostStrategy = exports.HeaderStrategy = void 0;
/**
* Strategy for identifying tenants based on HTTP headers.
*
* This strategy extracts the tenant ID from a specified HTTP header.
* It's a common approach for API-based multi-tenant applications.
*
* @example
* ```
* // Create a strategy that looks for tenant ID in x-tenant-id header
* const headerStrategy = new HeaderStrategy('x-tenant-id');
* ```
*/
class HeaderStrategy {
/**
* Creates a new HeaderStrategy instance.
*
* @param headerName - The name of the HTTP header containing the tenant ID (case insensitive)
* Defaults to 'x-tenant-id' if not provided.
*/
constructor(headerName = 'x-tenant-id') {
this.headerName = headerName;
}
/**
* Extracts tenant ID from the specified HTTP header.
*
* @param req - Express request object
* @returns The tenant ID from the header, or null if header is not present
*/
async resolveTenantId(req) {
return req.headers[this.headerName.toLowerCase()] || null;
}
}
exports.HeaderStrategy = HeaderStrategy;
/**
* Strategy for identifying tenants based on hostname.
*
* This strategy extracts the tenant ID from the hostname using a regular expression pattern.
* It's useful for subdomain-based multi-tenancy (e.g., tenant1.example.com).
*
* @example
* ```
* // Create a strategy that extracts tenant ID from subdomain
* const hostStrategy = new HostStrategy(/^([^.]+)/); // matches "tenant1" from "tenant1.example.com"
* ```
*/
class HostStrategy {
/**
* Creates a new HostStrategy instance.
*
* @param pattern - Regular expression pattern with a capture group for the tenant ID
*/
constructor(pattern) {
this.pattern = pattern;
}
/**
* Extracts tenant ID from the hostname using the provided regex pattern.
*
* @param req - Express request object
* @returns The tenant ID extracted from hostname, or null if no match
*/
async resolveTenantId(req) {
const hostname = req.hostname;
if (!hostname)
return null;
const match = hostname.match(this.pattern);
// Return the first capture group if there's a match
return match && match[1] ? match[1] : null;
}
}
exports.HostStrategy = HostStrategy;
/**
* Strategy for identifying tenants based on route parameters.
*
* This strategy extracts the tenant ID from route parameters.
* It's useful for route-based multi-tenancy (e.g., /api/:tenantId/resources).
*
* @example
* ```
* // Define a route with tenant parameter
* app.get('/api/:tenantId/resources', (req, res) => { ... });
*
* // Create a strategy that extracts tenant ID from route parameter
* const routeStrategy = new RouteStrategy('tenantId');
*
* // For Express 5 compatibility, mount the middleware on specific routes
* app.use(['/api/:tenantId', '/api/:tenantId/*'], multitenancy({
* strategies: [routeStrategy],
* store: myStore
* }));
* ```
*/
class RouteStrategy {
/**
* Creates a new RouteStrategy instance.
*
* @param paramName - Name of the route parameter to extract as tenant ID (default: 'tenantId')
*/
constructor(paramName = 'tenantId') {
this.paramName = paramName;
}
/**
* Extracts tenant ID from the route parameter specified in constructor.
*
* @param req - Express request object
* @returns The tenant ID from route parameter, or null if not present
*/
async resolveTenantId(req) {
return req.params?.[this.paramName] || null;
}
}
exports.RouteStrategy = RouteStrategy;
/**
* Strategy for identifying tenants based on URL path segments.
*
* This strategy extracts the tenant ID from a specific segment of the URL path.
* It's useful for path-based multi-tenancy (e.g., /tenant1/api/resources).
*
* @example
* ```
* // Create a strategy that uses the first path segment as tenant ID
* const basePathStrategy = new BasePathStrategy({ position: 1 }); // extracts "tenant1" from "/tenant1/api/resources"
*
* // Create a strategy that also rebases the path (removes the tenant segment)
* const basePathStrategy = new BasePathStrategy({ rebasePath: true });
* // After tenant resolution, "/tenant1/api/resources" becomes "/api/resources"
* ```
*/
class BasePathStrategy {
/**
* Creates a new BasePathStrategy instance.
*
* @param options - Configuration options for the strategy
*/
constructor(options) {
// Set default values by merging with provided options
this.options = {
position: 1,
rebasePath: false,
...options,
};
}
/**
* Extracts tenant ID from the specified position in the URL path.
*
* @param req - Express request object
* @returns The tenant ID from the path segment, or null if not present
*/
async resolveTenantId(req) {
const path = req.path;
if (!path)
return null;
// Split path and remove empty segments
const segments = path.split('/').filter((segment) => segment.length > 0);
// Position is 1-based for user-friendliness, but array is 0-based
const index = this.options.position - 1;
// Check if the index is valid
const tenantId = segments.length > index ? segments[index] : null;
// If rebasePath is true and we found a tenant, modify the request path
// by removing the tenant segment from the original path
if (tenantId && this.options.rebasePath) {
// Create a new path without the tenant segment
const newSegments = [...segments];
newSegments.splice(index, 1);
const newPath = '/' + newSegments.join('/');
// Override the path and original URL in the request object
// Note: This modifies Express's internal url property
Object.defineProperty(req, 'path', {
get: function () {
return newPath;
},
set: function () {
/* Ignore */
},
});
const originalUrl = req.originalUrl;
const pathIndex = originalUrl.indexOf(path);
if (pathIndex >= 0) {
const newOriginalUrl = originalUrl.substring(0, pathIndex) +
newPath +
originalUrl.substring(pathIndex + path.length);
Object.defineProperty(req, 'originalUrl', {
get: function () {
return newOriginalUrl;
},
set: function () {
/* Ignore */
},
});
}
}
return tenantId;
}
}
exports.BasePathStrategy = BasePathStrategy;
/**
* Strategy for identifying tenants based on authentication claims.
*
* This strategy extracts the tenant ID from JWT claims or other auth tokens.
* It's useful for applications where tenant context is tied to user authentication.
*
* @example
* ```
* // Create a strategy that extracts tenant ID from a specific claim
* const claimStrategy = new ClaimStrategy('tenantId');
*
* // Extract from nested claim property
* const nestedClaimStrategy = new ClaimStrategy('app_metadata.tenant_id');
*
* // With custom auth extractor
* const customStrategy = new ClaimStrategy('tenant', {
* authExtractor: (req) => req.user // For passport or similar auth middleware
* });
* ```
*/
class ClaimStrategy {
/**
* Creates a new ClaimStrategy instance.
*
* @param claimPath - Path to the claim containing tenant ID (dot notation for nested properties)
* @param options - Configuration options for the strategy
*/
constructor(claimPath, options = {}) {
this.claimPath = claimPath;
this.authExtractor = options.authExtractor || this.defaultAuthExtractor;
this.debug = options.debug || false;
}
/**
* Extracts tenant ID from the authentication claims.
*
* @param req - Express request object
* @returns The tenant ID from the claims, or null if not found
*/
async resolveTenantId(req) {
const payload = this.authExtractor(req);
if (!payload) {
if (this.debug)
console.log('[ClaimStrategy] No auth payload found');
return null;
}
const tenantId = this.getNestedProperty(payload, this.claimPath);
if (this.debug) {
console.log(`[ClaimStrategy] Extracted tenantId: ${tenantId}`);
}
return tenantId;
}
/**
* Default extractor that looks for JWT in Authorization header.
* This handles "Bearer <token>" format and attempts to decode the JWT.
*/
defaultAuthExtractor(req) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return null;
}
const token = authHeader.split(' ')[1];
if (!token)
return null;
try {
// Simple JWT parsing (without verification)
// Note: In production, you would want to verify the token
const base64Payload = token.split('.')[1];
return JSON.parse(Buffer.from(base64Payload, 'base64').toString('utf8'));
}
catch (error) {
if (this.debug) {
console.error('[ClaimStrategy] Failed to parse JWT token:', error);
}
return null;
}
}
/**
* Gets a nested property from an object using dot notation.
*/
getNestedProperty(obj, path) {
if (!obj || !path)
return null;
const parts = path.split('.');
let current = obj;
for (const part of parts) {
if (current == null || typeof current !== 'object') {
return null;
}
// Type assertion to access property with string index
current = current[part];
}
return current?.toString() || null;
}
}
exports.ClaimStrategy = ClaimStrategy;