Fix jwt-token

This commit is contained in:
2025-09-23 13:12:17 +02:00
parent 44047f9c98
commit ee4d3503e5
5 changed files with 694 additions and 3 deletions

View File

@@ -90,6 +90,16 @@ app.use('/api/', limiter);
const ipRestriction = new IPRestrictionMiddleware();
app.use((req, res, next) => ipRestriction.checkIPRestriction(req, res, next));
// Tenant-specific API rate limiting (for authenticated endpoints)
const { enforceApiRateLimit } = require('./middleware/tenant-limits');
app.use('/api', (req, res, next) => {
// Apply tenant rate limiting only to authenticated API endpoints
if (req.headers.authorization) {
return enforceApiRateLimit()(req, res, next);
}
next();
});
// Make io available to routes
app.use((req, res, next) => {
req.io = io;
@@ -125,7 +135,11 @@ app.use(errorHandler);
// Socket.IO initialization
initializeSocketHandlers(io);
const PORT = process.env.PORT || 3001;
// Initialize services
const dataRetentionService = require('./services/data-retention');
console.log('✅ Data retention service initialized');
const PORT = process.env.PORT || 5000;
// Migration runner
const runMigrations = async () => {

View 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
};

View File

@@ -4,6 +4,7 @@ const Joi = require('joi');
const { validateRequest } = require('../middleware/validation');
const { authenticateToken } = require('../middleware/auth');
const MultiTenantAuth = require('../middleware/multi-tenant-auth');
const { enforceDeviceLimit } = require('../middleware/tenant-limits');
const { Op } = require('sequelize');
// Dynamic model injection for testing
@@ -315,7 +316,7 @@ router.get('/:id', authenticateToken, async (req, res) => {
});
// POST /api/devices - Create new device (admin only)
router.post('/', authenticateToken, validateRequest(deviceSchema), async (req, res) => {
router.post('/', authenticateToken, enforceDeviceLimit(), validateRequest(deviceSchema), async (req, res) => {
try {
const { Device, DroneDetection, Heartbeat, Tenant } = getModels();

View File

@@ -13,6 +13,7 @@ const { authenticateToken } = require('../middleware/auth');
const { requirePermissions, requireAnyPermission, hasPermission } = require('../middleware/rbac');
const MultiTenantAuth = require('../middleware/multi-tenant-auth');
const { securityLogger } = require('../middleware/logger');
const { enforceUserLimit, enforceDeviceLimit, enforceApiRateLimit, getTenantLimitsStatus } = require('../middleware/tenant-limits');
// Initialize multi-tenant auth
const multiAuth = new MultiTenantAuth();
@@ -55,6 +56,45 @@ const upload = multer({
}
});
/**
* GET /tenant/limits
* Get current tenant limits and usage
*/
router.get('/limits', authenticateToken, requirePermissions(['tenant.view']), async (req, res) => {
try {
// Determine tenant from request
const tenantId = await multiAuth.determineTenant(req);
if (!tenantId) {
return res.status(400).json({
success: false,
message: 'Unable to determine tenant'
});
}
const tenant = await Tenant.findOne({ where: { slug: tenantId } });
if (!tenant) {
return res.status(404).json({
success: false,
message: 'Tenant not found'
});
}
const limitsStatus = await getTenantLimitsStatus(tenant.id);
res.json({
success: true,
data: limitsStatus
});
} catch (error) {
console.error('Error fetching tenant limits:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch tenant limits'
});
}
});
/**
* GET /tenant/info
* Get current tenant information
@@ -624,7 +664,7 @@ router.get('/users', authenticateToken, requirePermissions(['users.view']), asyn
* POST /tenant/users
* Create a new user in current tenant (user admin or higher, local auth only)
*/
router.post('/users', authenticateToken, requirePermissions(['users.create']), async (req, res) => {
router.post('/users', authenticateToken, requirePermissions(['users.create']), enforceUserLimit(), async (req, res) => {
try {
// Determine tenant from request
const tenantId = await multiAuth.determineTenant(req);

View File

@@ -0,0 +1,282 @@
/**
* Data Retention Cleanup Service
* Automatically removes old data based on tenant retention policies
*/
const cron = require('node-cron');
const { Op } = require('sequelize');
class DataRetentionService {
constructor() {
this.isRunning = false;
this.lastCleanup = null;
// Run cleanup daily at 2 AM
this.scheduleCleanup();
}
/**
* Schedule automatic cleanup
*/
scheduleCleanup() {
// Run at 2:00 AM every day
cron.schedule('0 2 * * *', async () => {
console.log('🗑️ Starting scheduled data retention cleanup...');
await this.runCleanup();
});
console.log('📅 Data retention cleanup scheduled for 2:00 AM daily');
}
/**
* Run cleanup for all tenants
*/
async runCleanup() {
if (this.isRunning) {
console.log('⚠️ Data retention cleanup already running, skipping...');
return;
}
try {
this.isRunning = true;
const startTime = Date.now();
console.log('🗑️ Starting data retention cleanup...');
const { Tenant } = require('../models');
const tenants = await Tenant.findAll({
where: {
is_active: true
}
});
let totalCleaned = {
detections: 0,
heartbeats: 0,
logs: 0,
sessions: 0
};
for (const tenant of tenants) {
const retentionDays = tenant.features?.data_retention_days;
// Skip tenants with unlimited retention (-1)
if (retentionDays === -1) {
console.log(`⏭️ Skipping tenant ${tenant.slug} - unlimited retention`);
continue;
}
console.log(`🧹 Cleaning data for tenant ${tenant.slug} (${retentionDays} days retention)`);
const cleanupResult = await this.cleanupTenantData(tenant.id, retentionDays);
totalCleaned.detections += cleanupResult.detections;
totalCleaned.heartbeats += cleanupResult.heartbeats;
totalCleaned.logs += cleanupResult.logs;
totalCleaned.sessions += cleanupResult.sessions;
}
const duration = Date.now() - startTime;
this.lastCleanup = new Date();
console.log(`✅ Data retention cleanup completed in ${duration}ms`);
console.log(`📊 Cleaned up:`, totalCleaned);
} catch (error) {
console.error('❌ Error during data retention cleanup:', error);
} finally {
this.isRunning = false;
}
}
/**
* Clean up data for a specific tenant
*/
async cleanupTenantData(tenantId, retentionDays) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
console.log(`🗑️ Cleaning data older than ${cutoffDate.toISOString()} for tenant ${tenantId}`);
const { DroneDetection, Heartbeat, ApiLog, Session } = require('../models');
const cleanupResults = {
detections: 0,
heartbeats: 0,
logs: 0,
sessions: 0
};
try {
// Clean up drone detections
const deletedDetections = await DroneDetection.destroy({
where: {
tenant_id: tenantId,
timestamp: {
[Op.lt]: cutoffDate
}
}
});
cleanupResults.detections = deletedDetections;
// Clean up heartbeats
const deletedHeartbeats = await Heartbeat.destroy({
where: {
tenant_id: tenantId,
timestamp: {
[Op.lt]: cutoffDate
}
}
});
cleanupResults.heartbeats = deletedHeartbeats;
// Clean up API logs (if exists)
try {
const deletedLogs = await ApiLog.destroy({
where: {
tenant_id: tenantId,
created_at: {
[Op.lt]: cutoffDate
}
}
});
cleanupResults.logs = deletedLogs;
} catch (error) {
// ApiLog table might not exist, skip silently
console.log(`⏭️ Skipping API logs cleanup for tenant ${tenantId} (table might not exist)`);
}
// Clean up old sessions
try {
const deletedSessions = await Session.destroy({
where: {
tenant_id: tenantId,
updated_at: {
[Op.lt]: cutoffDate
}
}
});
cleanupResults.sessions = deletedSessions;
} catch (error) {
// Session table might not exist, skip silently
console.log(`⏭️ Skipping sessions cleanup for tenant ${tenantId} (table might not exist)`);
}
console.log(`✅ Tenant ${tenantId} cleanup:`, cleanupResults);
} catch (error) {
console.error(`❌ Error cleaning data for tenant ${tenantId}:`, error);
}
return cleanupResults;
}
/**
* Manual cleanup for a specific tenant
*/
async manualCleanup(tenantId, retentionDays = null) {
const { Tenant } = require('../models');
const tenant = await Tenant.findByPk(tenantId);
if (!tenant) {
throw new Error('Tenant not found');
}
const days = retentionDays || tenant.features?.data_retention_days;
if (days === -1) {
throw new Error('Tenant has unlimited retention, manual cleanup requires explicit retention days');
}
console.log(`🧹 Manual cleanup for tenant ${tenant.slug} (${days} days retention)`);
return await this.cleanupTenantData(tenantId, days);
}
/**
* Get cleanup statistics
*/
async getCleanupStats() {
const { Tenant, DroneDetection, Heartbeat } = require('../models');
const tenants = await Tenant.findAll({
where: { is_active: true }
});
const stats = [];
for (const tenant of tenants) {
const retentionDays = tenant.features?.data_retention_days;
if (retentionDays === -1) {
stats.push({
tenant_id: tenant.id,
tenant_slug: tenant.slug,
retention_days: 'unlimited',
old_detections: 0,
old_heartbeats: 0,
next_cleanup: 'never'
});
continue;
}
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
const [oldDetections, oldHeartbeats] = await Promise.all([
DroneDetection.count({
where: {
tenant_id: tenant.id,
timestamp: { [Op.lt]: cutoffDate }
}
}),
Heartbeat.count({
where: {
tenant_id: tenant.id,
timestamp: { [Op.lt]: cutoffDate }
}
})
]);
stats.push({
tenant_id: tenant.id,
tenant_slug: tenant.slug,
retention_days: retentionDays,
old_detections: oldDetections,
old_heartbeats: oldHeartbeats,
next_cleanup: this.getNextCleanupTime()
});
}
return {
last_cleanup: this.lastCleanup,
next_cleanup: this.getNextCleanupTime(),
is_running: this.isRunning,
tenant_stats: stats
};
}
/**
* Get next scheduled cleanup time
*/
getNextCleanupTime() {
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(2, 0, 0, 0);
return tomorrow;
}
/**
* Force immediate cleanup (for testing/admin use)
*/
async forceCleanup() {
console.log('🚨 Force cleanup initiated');
await this.runCleanup();
}
}
// Create singleton instance
const dataRetentionService = new DataRetentionService();
module.exports = dataRetentionService;