Fix jwt-token

This commit is contained in:
2025-09-23 13:55:10 +02:00
parent ee4d3503e5
commit 8fbe2cb354
13 changed files with 1626 additions and 2 deletions

View File

@@ -236,7 +236,7 @@ async function startServer() {
deviceHealthService.start();
console.log('🏥 Device health monitoring: ✅ Started');
// Graceful shutdown for device health service
// Graceful shutdown for services
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
deviceHealthService.stop();

View File

@@ -3,10 +3,10 @@
* 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 MultiTenantAuth = require('./multi-tenant-auth');
const multiAuth = new MultiTenantAuth();
/**

View File

@@ -616,6 +616,135 @@ router.put('/security', authenticateToken, requirePermissions(['security.edit'])
}
});
/**
* GET /tenant/limits
* Get current tenant limits and usage status
*/
router.get('/limits', authenticateToken, 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 { getTenantLimitsStatus } = require('../middleware/tenant-limits');
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/data-retention/preview
* Preview what data would be deleted by retention cleanup
* Note: Actual cleanup is handled by separate data-retention-service container
*/
router.get('/data-retention/preview', authenticateToken, requirePermissions(['settings.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'
});
}
// Calculate what would be deleted (preview only)
const retentionDays = tenant.features?.data_retention_days || 90;
if (retentionDays === -1) {
return res.json({
success: true,
data: {
tenantSlug: tenant.slug,
retentionDays: 'unlimited',
cutoffDate: null,
toDelete: {
detections: 0,
heartbeats: 0,
logs: 0
},
note: 'This tenant has unlimited data retention'
}
});
}
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
const { DroneDetection, Heartbeat } = require('../models');
const { Op } = require('sequelize');
const [detectionsCount, heartbeatsCount] = 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 }
}
})
]);
res.json({
success: true,
data: {
tenantSlug: tenant.slug,
retentionDays,
cutoffDate: cutoffDate.toISOString(),
toDelete: {
detections: detectionsCount,
heartbeats: heartbeatsCount,
logs: 0 // Security logs are cleaned up by the data retention service
},
note: 'Actual cleanup is performed daily at 2:00 AM UTC by the data-retention-service container'
}
});
} catch (error) {
console.error('Error previewing data retention cleanup:', error);
res.status(500).json({
success: false,
message: 'Failed to preview data retention cleanup'
});
}
});
/**
* GET /tenant/users
* Get users in current tenant (user admin or higher)

View File

@@ -0,0 +1,292 @@
/**
* Data Retention Service
* Automatically cleans up old data based on tenant retention policies
*/
const cron = require('node-cron');
const { Op } = require('sequelize');
const { securityLogger } = require('../middleware/logger');
class DataRetentionService {
constructor() {
this.isRunning = false;
this.lastCleanup = null;
this.cleanupStats = {
totalRuns: 0,
totalDetectionsDeleted: 0,
totalHeartbeatsDeleted: 0,
totalLogsDeleted: 0,
lastRunDuration: 0
};
}
/**
* Start the data retention cleanup service
* Runs daily at 2 AM
*/
start() {
console.log('🗂️ Starting Data Retention Service...');
// Run daily at 2:00 AM
cron.schedule('0 2 * * *', async () => {
await this.performCleanup();
}, {
scheduled: true,
timezone: "UTC"
});
// Also run immediately if NODE_ENV is development
if (process.env.NODE_ENV === 'development') {
console.log('🧹 Development mode: Running initial data retention cleanup...');
setTimeout(() => this.performCleanup(), 5000); // Wait 5 seconds for app to fully start
}
console.log('✅ Data Retention Service started - will run daily at 2:00 AM UTC');
}
/**
* Perform cleanup for all tenants
*/
async performCleanup() {
if (this.isRunning) {
console.log('⏳ Data retention cleanup already running, skipping...');
return;
}
this.isRunning = true;
const startTime = Date.now();
try {
console.log('🧹 Starting data retention cleanup...');
const { Tenant, DroneDetection, Heartbeat, SecurityLog } = require('../models');
// Get all tenants with their retention policies
const tenants = await Tenant.findAll({
attributes: ['id', 'slug', 'features'],
where: {
is_active: true
}
});
let totalDetectionsDeleted = 0;
let totalHeartbeatsDeleted = 0;
let totalLogsDeleted = 0;
for (const tenant of tenants) {
const retentionDays = tenant.features?.data_retention_days;
// Skip if unlimited retention (-1)
if (retentionDays === -1) {
console.log(`⏭️ Skipping tenant ${tenant.slug} - unlimited retention`);
continue;
}
// Default to 90 days if not specified
const effectiveRetentionDays = retentionDays || 90;
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - effectiveRetentionDays);
console.log(`🧹 Cleaning tenant ${tenant.slug} - removing data older than ${effectiveRetentionDays} days (before ${cutoffDate.toISOString()})`);
try {
// Clean up drone detections
const deletedDetections = await DroneDetection.destroy({
where: {
tenant_id: tenant.id,
timestamp: {
[Op.lt]: cutoffDate
}
}
});
// Clean up heartbeats
const deletedHeartbeats = await Heartbeat.destroy({
where: {
tenant_id: tenant.id,
timestamp: {
[Op.lt]: cutoffDate
}
}
});
// Clean up security logs (if they have tenant_id)
let deletedLogs = 0;
try {
deletedLogs = await SecurityLog.destroy({
where: {
tenant_id: tenant.id,
timestamp: {
[Op.lt]: cutoffDate
}
}
});
} catch (error) {
// SecurityLog might not have tenant_id field, skip if error
console.log(`⚠️ Skipping security logs cleanup for tenant ${tenant.slug}: ${error.message}`);
}
totalDetectionsDeleted += deletedDetections;
totalHeartbeatsDeleted += deletedHeartbeats;
totalLogsDeleted += deletedLogs;
console.log(`✅ Tenant ${tenant.slug}: Deleted ${deletedDetections} detections, ${deletedHeartbeats} heartbeats, ${deletedLogs} logs`);
// Log significant cleanup events
if (deletedDetections > 100 || deletedHeartbeats > 100) {
securityLogger.logSecurityEvent('info', 'Data retention cleanup performed', {
action: 'data_retention_cleanup',
tenantId: tenant.id,
tenantSlug: tenant.slug,
retentionDays: effectiveRetentionDays,
cutoffDate: cutoffDate.toISOString(),
deletedDetections,
deletedHeartbeats,
deletedLogs
});
}
} catch (error) {
console.error(`❌ Error cleaning tenant ${tenant.slug}:`, error);
securityLogger.logSecurityEvent('error', 'Data retention cleanup failed', {
action: 'data_retention_cleanup_error',
tenantId: tenant.id,
tenantSlug: tenant.slug,
error: error.message,
stack: error.stack
});
}
}
const duration = Date.now() - startTime;
this.lastCleanup = new Date();
this.cleanupStats.totalRuns++;
this.cleanupStats.totalDetectionsDeleted += totalDetectionsDeleted;
this.cleanupStats.totalHeartbeatsDeleted += totalHeartbeatsDeleted;
this.cleanupStats.totalLogsDeleted += totalLogsDeleted;
this.cleanupStats.lastRunDuration = duration;
console.log(`✅ Data retention cleanup completed in ${duration}ms`);
console.log(`📊 Total deleted: ${totalDetectionsDeleted} detections, ${totalHeartbeatsDeleted} heartbeats, ${totalLogsDeleted} logs`);
// Log cleanup summary
securityLogger.logSecurityEvent('info', 'Data retention cleanup completed', {
action: 'data_retention_cleanup_summary',
duration,
tenantsProcessed: tenants.length,
totalDetectionsDeleted,
totalHeartbeatsDeleted,
totalLogsDeleted,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('❌ Data retention cleanup failed:', error);
securityLogger.logSecurityEvent('error', 'Data retention cleanup service failed', {
action: 'data_retention_service_error',
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
});
} finally {
this.isRunning = false;
}
}
/**
* Get cleanup statistics
*/
getStats() {
return {
...this.cleanupStats,
isRunning: this.isRunning,
lastCleanup: this.lastCleanup,
nextScheduledRun: '2:00 AM UTC daily'
};
}
/**
* Manually trigger cleanup (for testing/admin use)
*/
async triggerManualCleanup() {
console.log('🔧 Manual data retention cleanup triggered');
await this.performCleanup();
}
/**
* Preview what would be deleted for a specific tenant
*/
async previewCleanup(tenantId) {
try {
const { Tenant, DroneDetection, Heartbeat, SecurityLog } = require('../models');
const tenant = await Tenant.findByPk(tenantId);
if (!tenant) {
throw new Error('Tenant not found');
}
const retentionDays = tenant.features?.data_retention_days || 90;
if (retentionDays === -1) {
return {
tenantSlug: tenant.slug,
retentionDays: 'unlimited',
toDelete: {
detections: 0,
heartbeats: 0,
logs: 0
}
};
}
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
const [detectionsCount, heartbeatsCount] = 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 }
}
})
]);
let logsCount = 0;
try {
logsCount = await SecurityLog.count({
where: {
tenant_id: tenant.id,
timestamp: { [Op.lt]: cutoffDate }
}
});
} catch (error) {
// SecurityLog might not have tenant_id
}
return {
tenantSlug: tenant.slug,
retentionDays,
cutoffDate: cutoffDate.toISOString(),
toDelete: {
detections: detectionsCount,
heartbeats: heartbeatsCount,
logs: logsCount
}
};
} catch (error) {
console.error('Error previewing cleanup:', error);
throw error;
}
}
}
module.exports = DataRetentionService;