Fix jwt-token
This commit is contained in:
@@ -90,6 +90,16 @@ app.use('/api/', limiter);
|
|||||||
const ipRestriction = new IPRestrictionMiddleware();
|
const ipRestriction = new IPRestrictionMiddleware();
|
||||||
app.use((req, res, next) => ipRestriction.checkIPRestriction(req, res, next));
|
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
|
// Make io available to routes
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
req.io = io;
|
req.io = io;
|
||||||
@@ -125,7 +135,11 @@ app.use(errorHandler);
|
|||||||
// Socket.IO initialization
|
// Socket.IO initialization
|
||||||
initializeSocketHandlers(io);
|
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
|
// Migration runner
|
||||||
const runMigrations = async () => {
|
const runMigrations = async () => {
|
||||||
|
|||||||
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
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ const Joi = require('joi');
|
|||||||
const { validateRequest } = require('../middleware/validation');
|
const { validateRequest } = require('../middleware/validation');
|
||||||
const { authenticateToken } = require('../middleware/auth');
|
const { authenticateToken } = require('../middleware/auth');
|
||||||
const MultiTenantAuth = require('../middleware/multi-tenant-auth');
|
const MultiTenantAuth = require('../middleware/multi-tenant-auth');
|
||||||
|
const { enforceDeviceLimit } = require('../middleware/tenant-limits');
|
||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
|
|
||||||
// Dynamic model injection for testing
|
// Dynamic model injection for testing
|
||||||
@@ -315,7 +316,7 @@ router.get('/:id', authenticateToken, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/devices - Create new device (admin only)
|
// 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 {
|
try {
|
||||||
const { Device, DroneDetection, Heartbeat, Tenant } = getModels();
|
const { Device, DroneDetection, Heartbeat, Tenant } = getModels();
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const { authenticateToken } = require('../middleware/auth');
|
|||||||
const { requirePermissions, requireAnyPermission, hasPermission } = require('../middleware/rbac');
|
const { requirePermissions, requireAnyPermission, hasPermission } = require('../middleware/rbac');
|
||||||
const MultiTenantAuth = require('../middleware/multi-tenant-auth');
|
const MultiTenantAuth = require('../middleware/multi-tenant-auth');
|
||||||
const { securityLogger } = require('../middleware/logger');
|
const { securityLogger } = require('../middleware/logger');
|
||||||
|
const { enforceUserLimit, enforceDeviceLimit, enforceApiRateLimit, getTenantLimitsStatus } = require('../middleware/tenant-limits');
|
||||||
|
|
||||||
// Initialize multi-tenant auth
|
// Initialize multi-tenant auth
|
||||||
const multiAuth = new MultiTenantAuth();
|
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 /tenant/info
|
||||||
* Get current tenant information
|
* Get current tenant information
|
||||||
@@ -624,7 +664,7 @@ router.get('/users', authenticateToken, requirePermissions(['users.view']), asyn
|
|||||||
* POST /tenant/users
|
* POST /tenant/users
|
||||||
* Create a new user in current tenant (user admin or higher, local auth only)
|
* 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 {
|
try {
|
||||||
// Determine tenant from request
|
// Determine tenant from request
|
||||||
const tenantId = await multiAuth.determineTenant(req);
|
const tenantId = await multiAuth.determineTenant(req);
|
||||||
|
|||||||
282
server/services/data-retention.js
Normal file
282
server/services/data-retention.js
Normal 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;
|
||||||
Reference in New Issue
Block a user