diff --git a/data-retention-service/index.js b/data-retention-service/index.js index 7f7109c..78fd072 100644 --- a/data-retention-service/index.js +++ b/data-retention-service/index.js @@ -308,7 +308,7 @@ class DataRetentionService { // Set CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); res.setHeader('Content-Type', 'application/json'); @@ -343,6 +343,39 @@ class DataRetentionService { res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); } + + } else if (req.method === 'POST') { + if (parsedUrl.pathname === '/cleanup') { + // Manual cleanup trigger + if (this.isRunning) { + res.writeHead(409); + res.end(JSON.stringify({ + error: 'Cleanup already in progress', + message: 'A cleanup operation is currently running. Please wait for it to complete.' + })); + return; + } + + console.log('๐Ÿงน Manual cleanup triggered via HTTP API'); + + // Trigger cleanup asynchronously + this.performCleanup().then(() => { + console.log('โœ… Manual cleanup completed successfully'); + }).catch((error) => { + console.error('โŒ Manual cleanup failed:', error); + }); + + res.writeHead(202); + res.end(JSON.stringify({ + success: true, + message: 'Data retention cleanup initiated', + timestamp: new Date().toISOString() + })); + + } else { + res.writeHead(404); + res.end(JSON.stringify({ error: 'Not found' })); + } } else { res.writeHead(405); res.end(JSON.stringify({ error: 'Method not allowed' })); diff --git a/management/src/components/DataRetentionMetrics.jsx b/management/src/components/DataRetentionMetrics.jsx index 546bdd6..da68a7d 100644 --- a/management/src/components/DataRetentionMetrics.jsx +++ b/management/src/components/DataRetentionMetrics.jsx @@ -1,12 +1,15 @@ import React, { useState, useEffect } from 'react' import api from '../services/api' +import toast from 'react-hot-toast' import { TrashIcon, ServerIcon, ChartBarIcon, ClockIcon, ExclamationTriangleIcon, - CheckCircleIcon + CheckCircleIcon, + PlayIcon, + EyeIcon } from '@heroicons/react/24/outline' const DataRetentionMetrics = () => { @@ -14,6 +17,9 @@ const DataRetentionMetrics = () => { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [lastUpdate, setLastUpdate] = useState(null) + const [cleanupLoading, setCleanupLoading] = useState(false) + const [previewData, setPreviewData] = useState(null) + const [showPreview, setShowPreview] = useState(false) useEffect(() => { loadDataRetentionMetrics() @@ -36,6 +42,42 @@ const DataRetentionMetrics = () => { } } + const loadCleanupPreview = async () => { + try { + setError(null) + const response = await api.get('/data-retention/stats') + setPreviewData(response.data.data) + setShowPreview(true) + } catch (error) { + console.error('Error loading cleanup preview:', error) + toast.error('Failed to load cleanup preview') + } + } + + const executeCleanup = async () => { + if (!window.confirm('Are you sure you want to execute data retention cleanup? This will permanently delete old data according to each tenant\'s retention policy.')) { + return + } + + setCleanupLoading(true) + try { + // Note: This endpoint would need to be implemented in the backend + const response = await api.post('/data-retention/cleanup') + toast.success('Data retention cleanup initiated successfully') + + // Refresh metrics after cleanup + setTimeout(() => { + loadDataRetentionMetrics() + }, 2000) + + } catch (error) { + console.error('Error executing cleanup:', error) + toast.error(error.response?.data?.message || 'Failed to execute cleanup') + } finally { + setCleanupLoading(false) + } + } + const formatUptime = (seconds) => { if (!seconds) return 'Unknown' const days = Math.floor(seconds / 86400) @@ -111,8 +153,36 @@ const DataRetentionMetrics = () => { )} -
- {lastUpdate && `Updated ${lastUpdate.toLocaleTimeString()}`} + +
+
+ {lastUpdate && `Updated ${lastUpdate.toLocaleTimeString()}`} +
+ + {/* Action Buttons */} + {isConnected && ( +
+ + +
+ )}
@@ -211,6 +281,78 @@ const DataRetentionMetrics = () => {

)} + + {/* Cleanup Preview Modal */} + {showPreview && previewData && ( +
+
+
+

Data Retention Cleanup Preview

+ +
+ +
+

+ This preview shows what data would be deleted based on each tenant's retention policy. +

+ + {previewData.tenants && previewData.tenants.map((tenant, index) => ( +
+
+

{tenant.name}

+ + {tenant.retentionDays === -1 ? 'Unlimited' : `${tenant.retentionDays} days`} + +
+ + {tenant.retentionDays === -1 ? ( +

No data will be deleted (unlimited retention)

+ ) : ( +
+
+ Detections: + {tenant.toDelete?.detections || 0} +
+
+ Heartbeats: + {tenant.toDelete?.heartbeats || 0} +
+
+ Logs: + {tenant.toDelete?.logs || 0} +
+
+ )} +
+ ))} + +
+ + +
+
+
+
+ )} ) } diff --git a/management/src/pages/Dashboard.jsx b/management/src/pages/Dashboard.jsx index f1aeeb7..77de063 100644 --- a/management/src/pages/Dashboard.jsx +++ b/management/src/pages/Dashboard.jsx @@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react' import api from '../services/api' import { t } from '../utils/tempTranslations' // Temporary translation system import { BuildingOfficeIcon, UsersIcon, ServerIcon, ChartBarIcon } from '@heroicons/react/24/outline' -import DataRetentionMetrics from '../components/DataRetentionMetrics' const Dashboard = () => { const [stats, setStats] = useState({ @@ -97,8 +96,8 @@ const Dashboard = () => { ))} - {/* Quick Actions and Data Retention */} -
+ {/* Quick Actions and Recent Activity */} +

Quick Actions

@@ -143,9 +142,6 @@ const Dashboard = () => {
- - {/* Data Retention Service Metrics */} -
) } diff --git a/management/src/pages/System.jsx b/management/src/pages/System.jsx index 867edee..d1137d1 100644 --- a/management/src/pages/System.jsx +++ b/management/src/pages/System.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react' import api from '../services/api' import toast from 'react-hot-toast' import { t } from '../utils/tempTranslations' // Temporary translation system +import DataRetentionMetrics from '../components/DataRetentionMetrics' import { CogIcon, ServerIcon, @@ -516,6 +517,11 @@ const System = () => { + + {/* Data Retention Service */} +
+ +
) } diff --git a/server/routes/dataRetention.js b/server/routes/dataRetention.js index 50bf282..bcae498 100644 --- a/server/routes/dataRetention.js +++ b/server/routes/dataRetention.js @@ -375,4 +375,79 @@ router.get('/status', async (req, res) => { } }); +/** + * POST /api/data-retention/cleanup + * Trigger manual data retention cleanup + * RESTRICTED: Management users only + */ +router.post('/cleanup', async (req, res) => { + try { + // Add security headers + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-Frame-Options', 'DENY'); + res.setHeader('X-XSS-Protection', '1; mode=block'); + + // Make HTTP request to data retention service cleanup endpoint + const response = await new Promise((resolve, reject) => { + const options = { + hostname: DATA_RETENTION_HOST, + port: DATA_RETENTION_PORT, + path: '/cleanup', + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + timeout: 30000 // 30 second timeout for cleanup operation + }; + + const req = http.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => { + try { + const parsed = data ? JSON.parse(data) : {}; + resolve({ status: res.statusCode, data: parsed }); + } catch (e) { + resolve({ status: res.statusCode, data: { message: data } }); + } + }); + }); + + req.on('error', reject); + req.on('timeout', () => reject(new Error('Data retention service timeout'))); + req.end(); + }); + + if (response.status === 200 || response.status === 202) { + // Log successful cleanup trigger + await auditLogger.logSuccess(req.managementUser, req, '/cleanup'); + console.log(`โœ… Data retention cleanup triggered by ${req.managementUser.username}`); + + res.json({ + success: true, + data: response.data, + message: 'Data retention cleanup initiated successfully', + timestamp: new Date().toISOString(), + triggeredBy: { + username: req.managementUser.username, + role: req.managementUser.role + } + }); + } else { + res.status(response.status).json({ + success: false, + error: 'Failed to trigger cleanup in data retention service', + details: response.data + }); + } + } catch (error) { + console.error(`โŒ Data retention cleanup trigger error for ${req.managementUser.username}:`, error); + res.status(503).json({ + success: false, + error: 'Data retention service unavailable', + details: error.message + }); + } +}); + module.exports = router; \ No newline at end of file