diff --git a/client/src/App.jsx b/client/src/App.jsx
index cc30a89..888141f 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -12,6 +12,7 @@ import Detections from './pages/Detections';
import Alerts from './pages/Alerts';
import Debug from './pages/Debug';
import Settings from './pages/Settings';
+import SecurityLogs from './pages/SecurityLogs';
import Login from './pages/Login';
import Register from './pages/Register';
import ProtectedRoute from './components/ProtectedRoute';
diff --git a/client/src/pages/SecurityLogs.jsx b/client/src/pages/SecurityLogs.jsx
new file mode 100644
index 0000000..152872e
--- /dev/null
+++ b/client/src/pages/SecurityLogs.jsx
@@ -0,0 +1,303 @@
+import React, { useState, useEffect, useContext } from 'react';
+import { AuthContext } from '../contexts/AuthContext';
+import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/card';
+import { Alert, AlertDescription } from '../components/ui/alert';
+import { Badge } from '../components/ui/badge';
+import { formatDistanceToNow } from 'date-fns';
+
+const SecurityLogs = () => {
+ const { user, tenant } = useContext(AuthContext);
+ const [logs, setLogs] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [filters, setFilters] = useState({
+ level: 'all',
+ eventType: 'all',
+ timeRange: '24h',
+ search: ''
+ });
+ const [pagination, setPagination] = useState({
+ page: 1,
+ limit: 50,
+ total: 0
+ });
+
+ useEffect(() => {
+ // Only allow admins to view security logs
+ if (!user || user.role !== 'admin') {
+ setError('Access denied. Only tenant administrators can view security logs.');
+ setLoading(false);
+ return;
+ }
+
+ loadSecurityLogs();
+ }, [filters, pagination.page, user]);
+
+ const loadSecurityLogs = async () => {
+ if (!user || user.role !== 'admin') return;
+
+ setLoading(true);
+ try {
+ const params = new URLSearchParams({
+ page: pagination.page,
+ limit: pagination.limit,
+ ...filters
+ });
+
+ const response = await fetch(`/api/security-logs?${params}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`,
+ 'X-Tenant-ID': tenant?.id || ''
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ setLogs(data.logs || []);
+ setPagination(prev => ({
+ ...prev,
+ total: data.total || 0
+ }));
+ } catch (err) {
+ console.error('Failed to load security logs:', err);
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getLogLevelBadge = (level) => {
+ const styles = {
+ 'critical': 'bg-red-500 text-white',
+ 'high': 'bg-orange-500 text-white',
+ 'medium': 'bg-yellow-500 text-black',
+ 'low': 'bg-blue-500 text-white',
+ 'info': 'bg-gray-500 text-white'
+ };
+ return styles[level] || styles.info;
+ };
+
+ const getEventTypeIcon = (eventType) => {
+ const icons = {
+ 'failed_login': '🚫',
+ 'successful_login': '✅',
+ 'suspicious_activity': '⚠️',
+ 'country_alert': '🌍',
+ 'brute_force': '🔨',
+ 'account_lockout': '🔒',
+ 'password_reset': '🔄',
+ 'admin_action': '👤'
+ };
+ return icons[eventType] || '📋';
+ };
+
+ const formatMetadata = (metadata) => {
+ if (!metadata) return '';
+ const items = [];
+ if (metadata.ip_address) items.push(`IP: ${metadata.ip_address}`);
+ if (metadata.country) items.push(`Country: ${metadata.country}`);
+ if (metadata.user_agent) items.push(`Agent: ${metadata.user_agent.substring(0, 50)}...`);
+ if (metadata.username) items.push(`User: ${metadata.username}`);
+ return items.join(' | ');
+ };
+
+ // Access control check
+ if (!user || user.role !== 'admin') {
+ return (
+
+
+
+ Access denied. Only tenant administrators can view security logs.
+
+
+
+ );
+ }
+
+ const totalPages = Math.ceil(pagination.total / pagination.limit);
+
+ return (
+
+
+
Security Logs
+
+ Security events for {tenant?.name || 'your organization'}
+
+
+
+ {error && (
+
+
+ {error}
+
+
+ )}
+
+ {/* Filters */}
+
+
+ Filters
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setFilters(prev => ({ ...prev, search: e.target.value }))}
+ className="w-full p-2 border rounded-md"
+ />
+
+
+
+
+
+ {/* Security Logs Table */}
+
+
+
+ Security Events
+
+ {pagination.total} total events
+
+
+
+
+ {loading ? (
+
+
Loading security logs...
+
+ ) : logs.length === 0 ? (
+
+ No security logs found matching your criteria
+
+ ) : (
+
+
+
+
+ | Time |
+ Level |
+ Event |
+ Message |
+ Details |
+
+
+
+ {logs.map((log) => (
+
+ |
+ {new Date(log.timestamp).toLocaleString()}
+
+ {formatDistanceToNow(new Date(log.timestamp), { addSuffix: true })}
+
+ |
+
+
+ {log.level.toUpperCase()}
+
+ |
+
+
+ {getEventTypeIcon(log.event_type)}
+ {log.event_type.replace('_', ' ').toUpperCase()}
+
+ |
+
+
+ {log.message}
+
+ |
+
+
+ {formatMetadata(log.metadata)}
+
+ |
+
+ ))}
+
+
+
+ )}
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+
+ Page {pagination.page} of {totalPages}
+
+
+
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default SecurityLogs;
\ No newline at end of file
diff --git a/management/src/App.jsx b/management/src/App.jsx
index d245cb9..d326ddf 100644
--- a/management/src/App.jsx
+++ b/management/src/App.jsx
@@ -10,6 +10,7 @@ import Tenants from './pages/Tenants'
import TenantUsersPage from './pages/TenantUsersPage'
import Users from './pages/Users'
import System from './pages/System'
+import SecurityLogs from './pages/SecurityLogs'
function App() {
return (
@@ -29,6 +30,7 @@ function App() {
} />
} />
} />
+ } />
{
{ name: t('nav.dashboard'), href: '/dashboard', icon: HomeIcon },
{ name: t('nav.tenants'), href: '/tenants', icon: BuildingOfficeIcon },
{ name: t('nav.users'), href: '/users', icon: UsersIcon },
+ { name: t('nav.security_logs'), href: '/security-logs', icon: ShieldCheckIcon },
{ name: t('nav.system'), href: '/system', icon: CogIcon },
]
diff --git a/management/src/pages/SecurityLogs.jsx b/management/src/pages/SecurityLogs.jsx
new file mode 100644
index 0000000..26fc600
--- /dev/null
+++ b/management/src/pages/SecurityLogs.jsx
@@ -0,0 +1,276 @@
+import React, { useState, useEffect } from 'react';
+import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/card';
+import { Alert, AlertDescription } from '../components/ui/alert';
+import { Badge } from '../components/ui/badge';
+import { formatDistanceToNow } from 'date-fns';
+
+const SecurityLogs = () => {
+ const [logs, setLogs] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [filters, setFilters] = useState({
+ level: 'all',
+ eventType: 'all',
+ timeRange: '24h',
+ search: ''
+ });
+ const [pagination, setPagination] = useState({
+ page: 1,
+ limit: 50,
+ total: 0
+ });
+
+ useEffect(() => {
+ loadSecurityLogs();
+ }, [filters, pagination.page]);
+
+ const loadSecurityLogs = async () => {
+ setLoading(true);
+ try {
+ const params = new URLSearchParams({
+ page: pagination.page,
+ limit: pagination.limit,
+ ...filters
+ });
+
+ const response = await fetch(`/management/api/security-logs?${params}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('managementToken')}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ setLogs(data.logs || []);
+ setPagination(prev => ({
+ ...prev,
+ total: data.total || 0
+ }));
+ } catch (err) {
+ console.error('Failed to load security logs:', err);
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getLogLevelBadge = (level) => {
+ const styles = {
+ 'critical': 'bg-red-500 text-white',
+ 'high': 'bg-orange-500 text-white',
+ 'medium': 'bg-yellow-500 text-black',
+ 'low': 'bg-blue-500 text-white',
+ 'info': 'bg-gray-500 text-white'
+ };
+ return styles[level] || styles.info;
+ };
+
+ const getEventTypeIcon = (eventType) => {
+ const icons = {
+ 'failed_login': '🚫',
+ 'successful_login': '✅',
+ 'suspicious_activity': '⚠️',
+ 'country_alert': '🌍',
+ 'brute_force': '🔨',
+ 'account_lockout': '🔒',
+ 'password_reset': '🔄',
+ 'admin_action': '👤'
+ };
+ return icons[eventType] || '📋';
+ };
+
+ const formatMetadata = (metadata) => {
+ if (!metadata) return '';
+ const items = [];
+ if (metadata.ip_address) items.push(`IP: ${metadata.ip_address}`);
+ if (metadata.country) items.push(`Country: ${metadata.country}`);
+ if (metadata.user_agent) items.push(`Agent: ${metadata.user_agent.substring(0, 50)}...`);
+ if (metadata.tenant_slug) items.push(`Tenant: ${metadata.tenant_slug}`);
+ return items.join(' | ');
+ };
+
+ const totalPages = Math.ceil(pagination.total / pagination.limit);
+
+ return (
+
+
+
Security Logs
+
Monitor security events across all tenants
+
+
+ {error && (
+
+
+ {error}
+
+
+ )}
+
+ {/* Filters */}
+
+
+ Filters
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setFilters(prev => ({ ...prev, search: e.target.value }))}
+ className="w-full p-2 border rounded-md"
+ />
+
+
+
+
+
+ {/* Security Logs Table */}
+
+
+
+ Security Events
+
+ {pagination.total} total events
+
+
+
+
+ {loading ? (
+
+
Loading security logs...
+
+ ) : logs.length === 0 ? (
+
+ No security logs found matching your criteria
+
+ ) : (
+
+
+
+
+ | Time |
+ Level |
+ Event |
+ Message |
+ Details |
+
+
+
+ {logs.map((log) => (
+
+ |
+ {new Date(log.timestamp).toLocaleString()}
+
+ {formatDistanceToNow(new Date(log.timestamp), { addSuffix: true })}
+
+ |
+
+
+ {log.level.toUpperCase()}
+
+ |
+
+
+ {getEventTypeIcon(log.event_type)}
+ {log.event_type.replace('_', ' ').toUpperCase()}
+
+ |
+
+
+ {log.message}
+
+ |
+
+
+ {formatMetadata(log.metadata)}
+
+ |
+
+ ))}
+
+
+
+ )}
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+
+ Page {pagination.page} of {totalPages}
+
+
+
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default SecurityLogs;
\ No newline at end of file
diff --git a/management/src/utils/tempTranslations.js b/management/src/utils/tempTranslations.js
index 776ba5f..5eedc22 100644
--- a/management/src/utils/tempTranslations.js
+++ b/management/src/utils/tempTranslations.js
@@ -5,6 +5,7 @@ const translations = {
dashboard: 'Dashboard',
tenants: 'Tenants',
users: 'Users',
+ security_logs: 'Security Logs',
system: 'System'
},
navigation: {
diff --git a/server/routes/index.js b/server/routes/index.js
index 49984a1..8ca6d60 100644
--- a/server/routes/index.js
+++ b/server/routes/index.js
@@ -17,6 +17,7 @@ const detectionsRoutes = require('./detections');
const droneTypesRoutes = require('./droneTypes');
const tenantDebugRoutes = require('./tenant-debug');
const dataRetentionRoutes = require('./dataRetention');
+const securityLogsRoutes = require('./securityLogs');
// Management portal routes (before API versioning)
router.use('/management', managementRoutes);
@@ -37,6 +38,7 @@ router.use('/v1/device-health', deviceHealthRoutes);
router.use('/v1/detectors', detectorsRoutes);
router.use('/v1/detections', detectionsRoutes);
router.use('/v1/drone-types', droneTypesRoutes);
+router.use('/v1/security-logs', securityLogsRoutes);
// Default routes (no version prefix for backward compatibility)
router.use('/devices', deviceRoutes);
@@ -49,6 +51,7 @@ router.use('/debug', debugRoutes);
router.use('/detectors', detectorsRoutes);
router.use('/detections', detectionsRoutes);
router.use('/drone-types', droneTypesRoutes);
+router.use('/security-logs', securityLogsRoutes);
router.use('/tenant-debug', tenantDebugRoutes);
router.use('/data-retention', dataRetentionRoutes);
@@ -67,6 +70,7 @@ router.get('/', (req, res) => {
health: '/api/health',
'device-health': '/api/device-health',
'drone-types': '/api/drone-types',
+ 'security-logs': '/api/security-logs',
'data-retention': '/api/data-retention'
},
microservices: {
diff --git a/server/routes/management.js b/server/routes/management.js
index ced7ce1..641d5a4 100644
--- a/server/routes/management.js
+++ b/server/routes/management.js
@@ -1561,4 +1561,93 @@ router.get('/audit-logs/summary', requireManagementAuth, async (req, res) => {
}
});
+// Security logs endpoint - view ALL security logs across tenants
+router.get('/security-logs', requireManagementAuth, async (req, res) => {
+ try {
+ const {
+ page = 1,
+ limit = 50,
+ level = 'all',
+ eventType = 'all',
+ timeRange = '24h',
+ search = ''
+ } = req.query;
+
+ const { SecurityLog } = require('../models');
+
+ // Build where conditions
+ let whereConditions = {};
+
+ // Filter by security level
+ if (level !== 'all') {
+ whereConditions.level = level;
+ }
+
+ // Filter by event type
+ if (eventType !== 'all') {
+ whereConditions.event_type = eventType;
+ }
+
+ // Filter by time range
+ const now = new Date();
+ let startTime;
+ switch (timeRange) {
+ case '1h':
+ startTime = new Date(now.getTime() - 60 * 60 * 1000);
+ break;
+ case '24h':
+ startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
+ break;
+ case '7d':
+ startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
+ break;
+ case '30d':
+ startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
+ break;
+ default:
+ startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
+ }
+
+ if (timeRange !== 'all') {
+ whereConditions.timestamp = { [Op.gte]: startTime };
+ }
+
+ // Search filter
+ if (search) {
+ whereConditions[Op.or] = [
+ { message: { [Op.iLike]: `%${search}%` } },
+ { 'metadata.ip_address': { [Op.iLike]: `%${search}%` } },
+ { 'metadata.username': { [Op.iLike]: `%${search}%` } },
+ { 'metadata.tenant_slug': { [Op.iLike]: `%${search}%` } }
+ ];
+ }
+
+ const offset = (parseInt(page) - 1) * parseInt(limit);
+
+ const { rows: logs, count: total } = await SecurityLog.findAndCountAll({
+ where: whereConditions,
+ order: [['timestamp', 'DESC']],
+ limit: parseInt(limit),
+ offset: offset
+ });
+
+ res.json({
+ success: true,
+ logs,
+ total,
+ page: parseInt(page),
+ limit: parseInt(limit),
+ totalPages: Math.ceil(total / parseInt(limit))
+ });
+
+ } catch (error) {
+ console.error('Management: Error retrieving security logs:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to retrieve security logs',
+ error: error.message
+ });
+ }
+});
+
module.exports = router;
diff --git a/server/routes/securityLogs.js b/server/routes/securityLogs.js
new file mode 100644
index 0000000..e462e95
--- /dev/null
+++ b/server/routes/securityLogs.js
@@ -0,0 +1,192 @@
+const express = require('express');
+const router = express.Router();
+const { Op } = require('sequelize');
+const { authenticateToken, requireRole } = require('../middleware/auth');
+
+// Security logs endpoint - tenant-specific with admin-only access
+router.get('/', authenticateToken, requireRole('admin'), async (req, res) => {
+ try {
+ const {
+ page = 1,
+ limit = 50,
+ level = 'all',
+ eventType = 'all',
+ timeRange = '24h',
+ search = ''
+ } = req.query;
+
+ const { SecurityLog } = require('../models');
+
+ // Build where conditions - only show logs for current tenant
+ let whereConditions = {
+ tenant_id: req.user.tenant_id // Ensure tenant isolation
+ };
+
+ // Filter by security level
+ if (level !== 'all') {
+ whereConditions.level = level;
+ }
+
+ // Filter by event type
+ if (eventType !== 'all') {
+ whereConditions.event_type = eventType;
+ }
+
+ // Filter by time range
+ const now = new Date();
+ let startTime;
+ switch (timeRange) {
+ case '1h':
+ startTime = new Date(now.getTime() - 60 * 60 * 1000);
+ break;
+ case '24h':
+ startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
+ break;
+ case '7d':
+ startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
+ break;
+ case '30d':
+ startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
+ break;
+ default:
+ startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
+ }
+
+ if (timeRange !== 'all') {
+ whereConditions.timestamp = { [Op.gte]: startTime };
+ }
+
+ // Search filter - only search within tenant's logs
+ if (search) {
+ whereConditions[Op.or] = [
+ { message: { [Op.iLike]: `%${search}%` } },
+ { 'metadata.ip_address': { [Op.iLike]: `%${search}%` } },
+ { 'metadata.username': { [Op.iLike]: `%${search}%` } }
+ ];
+ }
+
+ const offset = (parseInt(page) - 1) * parseInt(limit);
+
+ const { rows: logs, count: total } = await SecurityLog.findAndCountAll({
+ where: whereConditions,
+ order: [['timestamp', 'DESC']],
+ limit: parseInt(limit),
+ offset: offset,
+ attributes: [
+ 'id',
+ 'timestamp',
+ 'level',
+ 'event_type',
+ 'message',
+ 'metadata'
+ ] // Don't expose sensitive internal data
+ });
+
+ res.json({
+ success: true,
+ logs,
+ total,
+ page: parseInt(page),
+ limit: parseInt(limit),
+ totalPages: Math.ceil(total / parseInt(limit))
+ });
+
+ } catch (error) {
+ console.error('Error retrieving tenant security logs:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to retrieve security logs',
+ error: process.env.NODE_ENV === 'development' ? error.message : undefined
+ });
+ }
+});
+
+// Security logs summary endpoint for dashboard widgets
+router.get('/summary', authenticateToken, requireRole('admin'), async (req, res) => {
+ try {
+ const { timeRange = '24h' } = req.query;
+ const { SecurityLog } = require('../models');
+
+ // Calculate time range
+ const now = new Date();
+ let startTime;
+ switch (timeRange) {
+ case '1h':
+ startTime = new Date(now.getTime() - 60 * 60 * 1000);
+ break;
+ case '24h':
+ startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
+ break;
+ case '7d':
+ startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
+ break;
+ case '30d':
+ startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
+ break;
+ default:
+ startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
+ }
+
+ const baseWhere = {
+ tenant_id: req.user.tenant_id,
+ timestamp: { [Op.gte]: startTime }
+ };
+
+ // Get summary statistics
+ const [
+ totalLogs,
+ criticalLogs,
+ highLogs,
+ failedLogins,
+ successfulLogins,
+ countryAlerts,
+ bruteForceAttempts
+ ] = await Promise.all([
+ SecurityLog.count({ where: baseWhere }),
+ SecurityLog.count({ where: { ...baseWhere, level: 'critical' } }),
+ SecurityLog.count({ where: { ...baseWhere, level: 'high' } }),
+ SecurityLog.count({ where: { ...baseWhere, event_type: 'failed_login' } }),
+ SecurityLog.count({ where: { ...baseWhere, event_type: 'successful_login' } }),
+ SecurityLog.count({ where: { ...baseWhere, event_type: 'country_alert' } }),
+ SecurityLog.count({ where: { ...baseWhere, event_type: 'brute_force' } })
+ ]);
+
+ // Get recent critical events
+ const recentCriticalEvents = await SecurityLog.findAll({
+ where: { ...baseWhere, level: { [Op.in]: ['critical', 'high'] } },
+ order: [['timestamp', 'DESC']],
+ limit: 5,
+ attributes: ['timestamp', 'level', 'event_type', 'message']
+ });
+
+ res.json({
+ success: true,
+ summary: {
+ period: {
+ start: startTime.toISOString(),
+ end: now.toISOString(),
+ range: timeRange
+ },
+ totals: {
+ totalLogs,
+ criticalLogs,
+ highLogs,
+ failedLogins,
+ successfulLogins,
+ countryAlerts,
+ bruteForceAttempts
+ },
+ recentCriticalEvents
+ }
+ });
+
+ } catch (error) {
+ console.error('Error retrieving security logs summary:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to retrieve security logs summary'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file