Fix jwt-token
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
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 { 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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
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