Fix jwt-token
This commit is contained in:
354
server/middleware/tenant-limits.js
Normal file
354
server/middleware/tenant-limits.js
Normal file
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
Reference in New Issue
Block a user