Fix jwt-token
This commit is contained in:
@@ -308,7 +308,7 @@ class DataRetentionService {
|
|||||||
|
|
||||||
// Set CORS headers
|
// Set CORS headers
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
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('Access-Control-Allow-Headers', 'Content-Type');
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
|
||||||
@@ -343,6 +343,39 @@ class DataRetentionService {
|
|||||||
res.writeHead(404);
|
res.writeHead(404);
|
||||||
res.end(JSON.stringify({ error: 'Not found' }));
|
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 {
|
} else {
|
||||||
res.writeHead(405);
|
res.writeHead(405);
|
||||||
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import api from '../services/api'
|
import api from '../services/api'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
import {
|
import {
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
ChartBarIcon,
|
ChartBarIcon,
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
ExclamationTriangleIcon,
|
ExclamationTriangleIcon,
|
||||||
CheckCircleIcon
|
CheckCircleIcon,
|
||||||
|
PlayIcon,
|
||||||
|
EyeIcon
|
||||||
} from '@heroicons/react/24/outline'
|
} from '@heroicons/react/24/outline'
|
||||||
|
|
||||||
const DataRetentionMetrics = () => {
|
const DataRetentionMetrics = () => {
|
||||||
@@ -14,6 +17,9 @@ const DataRetentionMetrics = () => {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const [lastUpdate, setLastUpdate] = useState(null)
|
const [lastUpdate, setLastUpdate] = useState(null)
|
||||||
|
const [cleanupLoading, setCleanupLoading] = useState(false)
|
||||||
|
const [previewData, setPreviewData] = useState(null)
|
||||||
|
const [showPreview, setShowPreview] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadDataRetentionMetrics()
|
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) => {
|
const formatUptime = (seconds) => {
|
||||||
if (!seconds) return 'Unknown'
|
if (!seconds) return 'Unknown'
|
||||||
const days = Math.floor(seconds / 86400)
|
const days = Math.floor(seconds / 86400)
|
||||||
@@ -111,9 +153,37 @@ const DataRetentionMetrics = () => {
|
|||||||
<ExclamationTriangleIcon className="h-5 w-5 text-red-500 ml-2" />
|
<ExclamationTriangleIcon className="h-5 w-5 text-red-500 ml-2" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
{lastUpdate && `Updated ${lastUpdate.toLocaleTimeString()}`}
|
{lastUpdate && `Updated ${lastUpdate.toLocaleTimeString()}`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
{isConnected && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={loadCleanupPreview}
|
||||||
|
className="px-3 py-1.5 bg-blue-100 text-blue-700 rounded-md hover:bg-blue-200 transition-colors text-sm flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<EyeIcon className="h-4 w-4" />
|
||||||
|
Preview Cleanup
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={executeCleanup}
|
||||||
|
disabled={cleanupLoading}
|
||||||
|
className="px-3 py-1.5 bg-red-100 text-red-700 rounded-md hover:bg-red-200 transition-colors text-sm flex items-center gap-1 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{cleanupLoading ? (
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-red-700"></div>
|
||||||
|
) : (
|
||||||
|
<PlayIcon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{cleanupLoading ? 'Running...' : 'Run Cleanup'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isConnected ? (
|
{isConnected ? (
|
||||||
@@ -211,6 +281,78 @@ const DataRetentionMetrics = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Cleanup Preview Modal */}
|
||||||
|
{showPreview && previewData && (
|
||||||
|
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||||
|
<div className="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-medium">Data Retention Cleanup Preview</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPreview(false)}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
This preview shows what data would be deleted based on each tenant's retention policy.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{previewData.tenants && previewData.tenants.map((tenant, index) => (
|
||||||
|
<div key={index} className="border rounded-lg p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h4 className="font-medium">{tenant.name}</h4>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
{tenant.retentionDays === -1 ? 'Unlimited' : `${tenant.retentionDays} days`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tenant.retentionDays === -1 ? (
|
||||||
|
<p className="text-sm text-gray-600">No data will be deleted (unlimited retention)</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Detections:</span>
|
||||||
|
<span className="ml-2">{tenant.toDelete?.detections || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Heartbeats:</span>
|
||||||
|
<span className="ml-2">{tenant.toDelete?.heartbeats || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Logs:</span>
|
||||||
|
<span className="ml-2">{tenant.toDelete?.logs || 0}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPreview(false)}
|
||||||
|
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowPreview(false)
|
||||||
|
executeCleanup()
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"
|
||||||
|
>
|
||||||
|
Execute Cleanup
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react'
|
|||||||
import api from '../services/api'
|
import api from '../services/api'
|
||||||
import { t } from '../utils/tempTranslations' // Temporary translation system
|
import { t } from '../utils/tempTranslations' // Temporary translation system
|
||||||
import { BuildingOfficeIcon, UsersIcon, ServerIcon, ChartBarIcon } from '@heroicons/react/24/outline'
|
import { BuildingOfficeIcon, UsersIcon, ServerIcon, ChartBarIcon } from '@heroicons/react/24/outline'
|
||||||
import DataRetentionMetrics from '../components/DataRetentionMetrics'
|
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
@@ -97,8 +96,8 @@ const Dashboard = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Actions and Data Retention */}
|
{/* Quick Actions and Recent Activity */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Quick Actions</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Quick Actions</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -143,9 +142,6 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Data Retention Service Metrics */}
|
|
||||||
<DataRetentionMetrics />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'
|
|||||||
import api from '../services/api'
|
import api from '../services/api'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { t } from '../utils/tempTranslations' // Temporary translation system
|
import { t } from '../utils/tempTranslations' // Temporary translation system
|
||||||
|
import DataRetentionMetrics from '../components/DataRetentionMetrics'
|
||||||
import {
|
import {
|
||||||
CogIcon,
|
CogIcon,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
@@ -516,6 +517,11 @@ const System = () => {
|
|||||||
</div>
|
</div>
|
||||||
</StatusCard>
|
</StatusCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Data Retention Service */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<DataRetentionMetrics />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
module.exports = router;
|
||||||
Reference in New Issue
Block a user