/** * Tenant Limits Middleware * Enforces tenant subscription limits for users, devices, API rate limits, etc. */ const { securityLogger } = require('./logger'); // Initialize multi-tenant auth const MultiTenantAuth = require('./multi-tenant-auth'); const multiAuth = new MultiTenantAuth(); /** * Redis-like in-memory store for rate limiting (replace with Redis in production) */ class RateLimitStore { constructor() { this.store = new Map(); this.cleanup(); } get(key) { const data = this.store.get(key); if (!data) return null; // Check if expired if (Date.now() > data.expires) { this.store.delete(key); return null; } return data; } set(key, value, ttlMs) { this.store.set(key, { ...value, expires: Date.now() + ttlMs }); } delete(key) { this.store.delete(key); } // Clean up expired entries every minute cleanup() { setInterval(() => { const now = Date.now(); for (const [key, data] of this.store.entries()) { if (now > data.expires) { this.store.delete(key); } } }, 60000); } } const rateLimitStore = new RateLimitStore(); /** * Get tenant and validate access */ async function getTenantFromRequest(req) { const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { throw new Error('Unable to determine tenant'); } const { Tenant } = require('../models'); const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { throw new Error('Tenant not found'); } return tenant; } /** * Check if tenant has reached user limit */ async function checkUserLimit(tenantId, excludeUserId = null) { const { User } = require('../models'); const whereClause = { tenant_id: tenantId }; if (excludeUserId) { whereClause.id = { [require('sequelize').Op.ne]: excludeUserId }; } const userCount = await User.count({ where: whereClause }); return userCount; } /** * Check if tenant has reached device limit */ async function checkDeviceLimit(tenantId, excludeDeviceId = null) { const { Device } = require('../models'); const whereClause = { tenant_id: tenantId }; if (excludeDeviceId) { whereClause.id = { [require('sequelize').Op.ne]: excludeDeviceId }; } const deviceCount = await Device.count({ where: whereClause }); return deviceCount; } /** * Middleware to enforce user creation limits */ function enforceUserLimit() { return async (req, res, next) => { try { const tenant = await getTenantFromRequest(req); const maxUsers = tenant.features?.max_users; // -1 means unlimited if (maxUsers === -1) { return next(); } const currentUserCount = await checkUserLimit(tenant.id); if (currentUserCount >= maxUsers) { securityLogger.logSecurityEvent('warning', 'User creation blocked due to tenant limit', { action: 'user_creation_limit_exceeded', tenantId: tenant.id, tenantSlug: tenant.slug, currentUserCount, maxUsers, userId: req.user?.id, username: req.user?.username, ip: req.ip, userAgent: req.get('User-Agent') }); return res.status(403).json({ success: false, message: `Tenant has reached the maximum number of users (${maxUsers}). Please upgrade your subscription or remove existing users.`, error_code: 'TENANT_USER_LIMIT_EXCEEDED', current_count: currentUserCount, max_allowed: maxUsers }); } next(); } catch (error) { console.error('Error checking user limit:', error); res.status(500).json({ success: false, message: 'Failed to validate user limit' }); } }; } /** * Middleware to enforce device creation limits */ function enforceDeviceLimit() { return async (req, res, next) => { try { const tenant = await getTenantFromRequest(req); const maxDevices = tenant.features?.max_devices; // -1 means unlimited if (maxDevices === -1) { return next(); } const currentDeviceCount = await checkDeviceLimit(tenant.id); if (currentDeviceCount >= maxDevices) { securityLogger.logSecurityEvent('warning', 'Device creation blocked due to tenant limit', { action: 'device_creation_limit_exceeded', tenantId: tenant.id, tenantSlug: tenant.slug, currentDeviceCount, maxDevices, userId: req.user?.id, username: req.user?.username, ip: req.ip, userAgent: req.get('User-Agent') }); return res.status(403).json({ success: false, message: `Tenant has reached the maximum number of devices (${maxDevices}). Please upgrade your subscription or remove existing devices.`, error_code: 'TENANT_DEVICE_LIMIT_EXCEEDED', current_count: currentDeviceCount, max_allowed: maxDevices }); } next(); } catch (error) { console.error('Error checking device limit:', error); res.status(500).json({ success: false, message: 'Failed to validate device limit' }); } }; } /** * Middleware to enforce API rate limits per tenant * Tracks actual API requests (not page views) shared among all tenant users */ function enforceApiRateLimit(windowMs = 60000) { // Default 1 minute window return async (req, res, next) => { try { const tenant = await getTenantFromRequest(req); const maxRequests = tenant.features?.api_rate_limit; // -1 means unlimited if (maxRequests === -1) { return next(); } const key = `api_rate_limit:${tenant.id}`; const now = Date.now(); const windowStart = now - windowMs; // Get current rate limit data let rateLimitData = rateLimitStore.get(key); if (!rateLimitData) { rateLimitData = { requests: [], totalRequests: 0 }; } // Remove old requests outside the window rateLimitData.requests = rateLimitData.requests.filter(timestamp => timestamp > windowStart); // Check if limit exceeded if (rateLimitData.requests.length >= maxRequests) { const resetTime = rateLimitData.requests[0] + windowMs; const retryAfter = Math.ceil((resetTime - now) / 1000); securityLogger.logSecurityEvent('warning', 'API rate limit exceeded for tenant', { action: 'api_rate_limit_exceeded', tenantId: tenant.id, tenantSlug: tenant.slug, currentRequests: rateLimitData.requests.length, maxRequests, windowMs, userId: req.user?.id, username: req.user?.username, endpoint: req.path, method: req.method, ip: req.ip, userAgent: req.get('User-Agent') }); res.set({ 'X-RateLimit-Limit': maxRequests, 'X-RateLimit-Remaining': 0, 'X-RateLimit-Reset': Math.ceil(resetTime / 1000), 'Retry-After': retryAfter }); return res.status(429).json({ success: false, message: `API rate limit exceeded. Maximum ${maxRequests} requests per ${windowMs/1000} seconds for your tenant.`, error_code: 'TENANT_API_RATE_LIMIT_EXCEEDED', max_requests: maxRequests, window_seconds: windowMs / 1000, retry_after_seconds: retryAfter }); } // Add current request rateLimitData.requests.push(now); rateLimitData.totalRequests++; // Store updated data rateLimitStore.set(key, rateLimitData, windowMs); // Set rate limit headers res.set({ 'X-RateLimit-Limit': maxRequests, 'X-RateLimit-Remaining': Math.max(0, maxRequests - rateLimitData.requests.length), 'X-RateLimit-Reset': Math.ceil((now + windowMs) / 1000) }); next(); } catch (error) { console.error('Error checking API rate limit:', error); // Don't block on rate limit errors, but log them next(); } }; } /** * Get tenant limits status */ async function getTenantLimitsStatus(tenantId) { try { const { Tenant } = require('../models'); const tenant = await Tenant.findByPk(tenantId); if (!tenant) { throw new Error('Tenant not found'); } const [userCount, deviceCount] = await Promise.all([ checkUserLimit(tenantId), checkDeviceLimit(tenantId) ]); const rateLimitKey = `api_rate_limit:${tenantId}`; const rateLimitData = rateLimitStore.get(rateLimitKey); const currentApiRequests = rateLimitData ? rateLimitData.requests.length : 0; return { users: { current: userCount, limit: tenant.features?.max_users || 0, unlimited: tenant.features?.max_users === -1 }, devices: { current: deviceCount, limit: tenant.features?.max_devices || 0, unlimited: tenant.features?.max_devices === -1 }, api_requests: { current_minute: currentApiRequests, limit_per_minute: tenant.features?.api_rate_limit || 0, unlimited: tenant.features?.api_rate_limit === -1 }, data_retention: { days: tenant.features?.data_retention_days || 90, unlimited: tenant.features?.data_retention_days === -1 } }; } catch (error) { console.error('Error getting tenant limits status:', error); throw error; } } module.exports = { enforceUserLimit, enforceDeviceLimit, enforceApiRateLimit, getTenantLimitsStatus, checkUserLimit, checkDeviceLimit, rateLimitStore };