Files
drone-detector/server/middleware/tenant-limits.js
2025-09-23 13:12:17 +02:00

354 lines
9.7 KiB
JavaScript

/**
* Tenant Limits Middleware
* Enforces tenant subscription limits for users, devices, API rate limits, etc.
*/
const MultiTenantAuth = require('./multi-tenant-auth');
const { securityLogger } = require('./logger');
// Initialize 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
};