Fix jwt-token
This commit is contained in:
@@ -12,6 +12,7 @@ import Detections from './pages/Detections';
|
|||||||
import Alerts from './pages/Alerts';
|
import Alerts from './pages/Alerts';
|
||||||
import Debug from './pages/Debug';
|
import Debug from './pages/Debug';
|
||||||
import Settings from './pages/Settings';
|
import Settings from './pages/Settings';
|
||||||
|
import SecurityLogs from './pages/SecurityLogs';
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import Register from './pages/Register';
|
import Register from './pages/Register';
|
||||||
import ProtectedRoute from './components/ProtectedRoute';
|
import ProtectedRoute from './components/ProtectedRoute';
|
||||||
|
|||||||
303
client/src/pages/SecurityLogs.jsx
Normal file
303
client/src/pages/SecurityLogs.jsx
Normal file
@@ -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 (
|
||||||
|
<div className="p-6">
|
||||||
|
<Alert className="border-red-200 bg-red-50">
|
||||||
|
<AlertDescription className="text-red-800">
|
||||||
|
Access denied. Only tenant administrators can view security logs.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(pagination.total / pagination.limit);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="text-3xl font-bold mb-2">Security Logs</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Security events for {tenant?.name || 'your organization'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert className="mb-6 border-red-200 bg-red-50">
|
||||||
|
<AlertDescription className="text-red-800">
|
||||||
|
{error}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Filters</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">Security Level</label>
|
||||||
|
<select
|
||||||
|
value={filters.level}
|
||||||
|
onChange={(e) => setFilters(prev => ({ ...prev, level: e.target.value }))}
|
||||||
|
className="w-full p-2 border rounded-md"
|
||||||
|
>
|
||||||
|
<option value="all">All Levels</option>
|
||||||
|
<option value="critical">Critical</option>
|
||||||
|
<option value="high">High</option>
|
||||||
|
<option value="medium">Medium</option>
|
||||||
|
<option value="low">Low</option>
|
||||||
|
<option value="info">Info</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">Event Type</label>
|
||||||
|
<select
|
||||||
|
value={filters.eventType}
|
||||||
|
onChange={(e) => setFilters(prev => ({ ...prev, eventType: e.target.value }))}
|
||||||
|
className="w-full p-2 border rounded-md"
|
||||||
|
>
|
||||||
|
<option value="all">All Events</option>
|
||||||
|
<option value="failed_login">Failed Logins</option>
|
||||||
|
<option value="successful_login">Successful Logins</option>
|
||||||
|
<option value="suspicious_activity">Suspicious Activity</option>
|
||||||
|
<option value="country_alert">Country Alerts</option>
|
||||||
|
<option value="brute_force">Brute Force</option>
|
||||||
|
<option value="account_lockout">Account Lockouts</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">Time Range</label>
|
||||||
|
<select
|
||||||
|
value={filters.timeRange}
|
||||||
|
onChange={(e) => setFilters(prev => ({ ...prev, timeRange: e.target.value }))}
|
||||||
|
className="w-full p-2 border rounded-md"
|
||||||
|
>
|
||||||
|
<option value="1h">Last Hour</option>
|
||||||
|
<option value="24h">Last 24 Hours</option>
|
||||||
|
<option value="7d">Last 7 Days</option>
|
||||||
|
<option value="30d">Last 30 Days</option>
|
||||||
|
<option value="all">All Time</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">Search</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="IP, username..."
|
||||||
|
value={filters.search}
|
||||||
|
onChange={(e) => setFilters(prev => ({ ...prev, search: e.target.value }))}
|
||||||
|
className="w-full p-2 border rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Security Logs Table */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex justify-between items-center">
|
||||||
|
Security Events
|
||||||
|
<span className="text-sm font-normal text-gray-500">
|
||||||
|
{pagination.total} total events
|
||||||
|
</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex justify-center py-8">
|
||||||
|
<div className="text-gray-500">Loading security logs...</div>
|
||||||
|
</div>
|
||||||
|
) : logs.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
No security logs found matching your criteria
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b">
|
||||||
|
<th className="text-left p-2">Time</th>
|
||||||
|
<th className="text-left p-2">Level</th>
|
||||||
|
<th className="text-left p-2">Event</th>
|
||||||
|
<th className="text-left p-2">Message</th>
|
||||||
|
<th className="text-left p-2">Details</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{logs.map((log) => (
|
||||||
|
<tr key={log.id} className="border-b hover:bg-gray-50">
|
||||||
|
<td className="p-2 text-sm">
|
||||||
|
<div>{new Date(log.timestamp).toLocaleString()}</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{formatDistanceToNow(new Date(log.timestamp), { addSuffix: true })}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-2">
|
||||||
|
<Badge className={getLogLevelBadge(log.level)}>
|
||||||
|
{log.level.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="p-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>{getEventTypeIcon(log.event_type)}</span>
|
||||||
|
<span className="text-sm">{log.event_type.replace('_', ' ').toUpperCase()}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-2 text-sm max-w-md">
|
||||||
|
<div className="truncate" title={log.message}>
|
||||||
|
{log.message}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-2 text-xs text-gray-600 max-w-md">
|
||||||
|
<div className="truncate" title={formatMetadata(log.metadata)}>
|
||||||
|
{formatMetadata(log.metadata)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex justify-between items-center mt-6">
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Page {pagination.page} of {totalPages}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setPagination(prev => ({ ...prev, page: Math.max(1, prev.page - 1) }))}
|
||||||
|
disabled={pagination.page === 1}
|
||||||
|
className="px-3 py-1 text-sm border rounded disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setPagination(prev => ({ ...prev, page: Math.min(totalPages, prev.page + 1) }))}
|
||||||
|
disabled={pagination.page === totalPages}
|
||||||
|
className="px-3 py-1 text-sm border rounded disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SecurityLogs;
|
||||||
@@ -10,6 +10,7 @@ import Tenants from './pages/Tenants'
|
|||||||
import TenantUsersPage from './pages/TenantUsersPage'
|
import TenantUsersPage from './pages/TenantUsersPage'
|
||||||
import Users from './pages/Users'
|
import Users from './pages/Users'
|
||||||
import System from './pages/System'
|
import System from './pages/System'
|
||||||
|
import SecurityLogs from './pages/SecurityLogs'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -29,6 +30,7 @@ function App() {
|
|||||||
<Route path="tenants/:tenantId/users" element={<TenantUsersPage />} />
|
<Route path="tenants/:tenantId/users" element={<TenantUsersPage />} />
|
||||||
<Route path="users" element={<Users />} />
|
<Route path="users" element={<Users />} />
|
||||||
<Route path="system" element={<System />} />
|
<Route path="system" element={<System />} />
|
||||||
|
<Route path="security-logs" element={<SecurityLogs />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
<Toaster
|
<Toaster
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
BuildingOfficeIcon,
|
BuildingOfficeIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
CogIcon,
|
CogIcon,
|
||||||
|
ShieldCheckIcon,
|
||||||
ArrowRightOnRectangleIcon
|
ArrowRightOnRectangleIcon
|
||||||
} from '@heroicons/react/24/outline'
|
} from '@heroicons/react/24/outline'
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ const Layout = () => {
|
|||||||
{ name: t('nav.dashboard'), href: '/dashboard', icon: HomeIcon },
|
{ name: t('nav.dashboard'), href: '/dashboard', icon: HomeIcon },
|
||||||
{ name: t('nav.tenants'), href: '/tenants', icon: BuildingOfficeIcon },
|
{ name: t('nav.tenants'), href: '/tenants', icon: BuildingOfficeIcon },
|
||||||
{ name: t('nav.users'), href: '/users', icon: UsersIcon },
|
{ 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 },
|
{ name: t('nav.system'), href: '/system', icon: CogIcon },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
276
management/src/pages/SecurityLogs.jsx
Normal file
276
management/src/pages/SecurityLogs.jsx
Normal file
@@ -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 (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="text-3xl font-bold mb-2">Security Logs</h1>
|
||||||
|
<p className="text-gray-600">Monitor security events across all tenants</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert className="mb-6 border-red-200 bg-red-50">
|
||||||
|
<AlertDescription className="text-red-800">
|
||||||
|
{error}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Filters</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">Security Level</label>
|
||||||
|
<select
|
||||||
|
value={filters.level}
|
||||||
|
onChange={(e) => setFilters(prev => ({ ...prev, level: e.target.value }))}
|
||||||
|
className="w-full p-2 border rounded-md"
|
||||||
|
>
|
||||||
|
<option value="all">All Levels</option>
|
||||||
|
<option value="critical">Critical</option>
|
||||||
|
<option value="high">High</option>
|
||||||
|
<option value="medium">Medium</option>
|
||||||
|
<option value="low">Low</option>
|
||||||
|
<option value="info">Info</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">Event Type</label>
|
||||||
|
<select
|
||||||
|
value={filters.eventType}
|
||||||
|
onChange={(e) => setFilters(prev => ({ ...prev, eventType: e.target.value }))}
|
||||||
|
className="w-full p-2 border rounded-md"
|
||||||
|
>
|
||||||
|
<option value="all">All Events</option>
|
||||||
|
<option value="failed_login">Failed Logins</option>
|
||||||
|
<option value="successful_login">Successful Logins</option>
|
||||||
|
<option value="suspicious_activity">Suspicious Activity</option>
|
||||||
|
<option value="country_alert">Country Alerts</option>
|
||||||
|
<option value="brute_force">Brute Force</option>
|
||||||
|
<option value="account_lockout">Account Lockouts</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">Time Range</label>
|
||||||
|
<select
|
||||||
|
value={filters.timeRange}
|
||||||
|
onChange={(e) => setFilters(prev => ({ ...prev, timeRange: e.target.value }))}
|
||||||
|
className="w-full p-2 border rounded-md"
|
||||||
|
>
|
||||||
|
<option value="1h">Last Hour</option>
|
||||||
|
<option value="24h">Last 24 Hours</option>
|
||||||
|
<option value="7d">Last 7 Days</option>
|
||||||
|
<option value="30d">Last 30 Days</option>
|
||||||
|
<option value="all">All Time</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">Search</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="IP, username, tenant..."
|
||||||
|
value={filters.search}
|
||||||
|
onChange={(e) => setFilters(prev => ({ ...prev, search: e.target.value }))}
|
||||||
|
className="w-full p-2 border rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Security Logs Table */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex justify-between items-center">
|
||||||
|
Security Events
|
||||||
|
<span className="text-sm font-normal text-gray-500">
|
||||||
|
{pagination.total} total events
|
||||||
|
</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex justify-center py-8">
|
||||||
|
<div className="text-gray-500">Loading security logs...</div>
|
||||||
|
</div>
|
||||||
|
) : logs.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
No security logs found matching your criteria
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b">
|
||||||
|
<th className="text-left p-2">Time</th>
|
||||||
|
<th className="text-left p-2">Level</th>
|
||||||
|
<th className="text-left p-2">Event</th>
|
||||||
|
<th className="text-left p-2">Message</th>
|
||||||
|
<th className="text-left p-2">Details</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{logs.map((log) => (
|
||||||
|
<tr key={log.id} className="border-b hover:bg-gray-50">
|
||||||
|
<td className="p-2 text-sm">
|
||||||
|
<div>{new Date(log.timestamp).toLocaleString()}</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{formatDistanceToNow(new Date(log.timestamp), { addSuffix: true })}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-2">
|
||||||
|
<Badge className={getLogLevelBadge(log.level)}>
|
||||||
|
{log.level.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="p-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>{getEventTypeIcon(log.event_type)}</span>
|
||||||
|
<span className="text-sm">{log.event_type.replace('_', ' ').toUpperCase()}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-2 text-sm max-w-md">
|
||||||
|
<div className="truncate" title={log.message}>
|
||||||
|
{log.message}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-2 text-xs text-gray-600 max-w-md">
|
||||||
|
<div className="truncate" title={formatMetadata(log.metadata)}>
|
||||||
|
{formatMetadata(log.metadata)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex justify-between items-center mt-6">
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Page {pagination.page} of {totalPages}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setPagination(prev => ({ ...prev, page: Math.max(1, prev.page - 1) }))}
|
||||||
|
disabled={pagination.page === 1}
|
||||||
|
className="px-3 py-1 text-sm border rounded disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setPagination(prev => ({ ...prev, page: Math.min(totalPages, prev.page + 1) }))}
|
||||||
|
disabled={pagination.page === totalPages}
|
||||||
|
className="px-3 py-1 text-sm border rounded disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SecurityLogs;
|
||||||
@@ -5,6 +5,7 @@ const translations = {
|
|||||||
dashboard: 'Dashboard',
|
dashboard: 'Dashboard',
|
||||||
tenants: 'Tenants',
|
tenants: 'Tenants',
|
||||||
users: 'Users',
|
users: 'Users',
|
||||||
|
security_logs: 'Security Logs',
|
||||||
system: 'System'
|
system: 'System'
|
||||||
},
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const detectionsRoutes = require('./detections');
|
|||||||
const droneTypesRoutes = require('./droneTypes');
|
const droneTypesRoutes = require('./droneTypes');
|
||||||
const tenantDebugRoutes = require('./tenant-debug');
|
const tenantDebugRoutes = require('./tenant-debug');
|
||||||
const dataRetentionRoutes = require('./dataRetention');
|
const dataRetentionRoutes = require('./dataRetention');
|
||||||
|
const securityLogsRoutes = require('./securityLogs');
|
||||||
|
|
||||||
// Management portal routes (before API versioning)
|
// Management portal routes (before API versioning)
|
||||||
router.use('/management', managementRoutes);
|
router.use('/management', managementRoutes);
|
||||||
@@ -37,6 +38,7 @@ router.use('/v1/device-health', deviceHealthRoutes);
|
|||||||
router.use('/v1/detectors', detectorsRoutes);
|
router.use('/v1/detectors', detectorsRoutes);
|
||||||
router.use('/v1/detections', detectionsRoutes);
|
router.use('/v1/detections', detectionsRoutes);
|
||||||
router.use('/v1/drone-types', droneTypesRoutes);
|
router.use('/v1/drone-types', droneTypesRoutes);
|
||||||
|
router.use('/v1/security-logs', securityLogsRoutes);
|
||||||
|
|
||||||
// Default routes (no version prefix for backward compatibility)
|
// Default routes (no version prefix for backward compatibility)
|
||||||
router.use('/devices', deviceRoutes);
|
router.use('/devices', deviceRoutes);
|
||||||
@@ -49,6 +51,7 @@ router.use('/debug', debugRoutes);
|
|||||||
router.use('/detectors', detectorsRoutes);
|
router.use('/detectors', detectorsRoutes);
|
||||||
router.use('/detections', detectionsRoutes);
|
router.use('/detections', detectionsRoutes);
|
||||||
router.use('/drone-types', droneTypesRoutes);
|
router.use('/drone-types', droneTypesRoutes);
|
||||||
|
router.use('/security-logs', securityLogsRoutes);
|
||||||
router.use('/tenant-debug', tenantDebugRoutes);
|
router.use('/tenant-debug', tenantDebugRoutes);
|
||||||
router.use('/data-retention', dataRetentionRoutes);
|
router.use('/data-retention', dataRetentionRoutes);
|
||||||
|
|
||||||
@@ -67,6 +70,7 @@ router.get('/', (req, res) => {
|
|||||||
health: '/api/health',
|
health: '/api/health',
|
||||||
'device-health': '/api/device-health',
|
'device-health': '/api/device-health',
|
||||||
'drone-types': '/api/drone-types',
|
'drone-types': '/api/drone-types',
|
||||||
|
'security-logs': '/api/security-logs',
|
||||||
'data-retention': '/api/data-retention'
|
'data-retention': '/api/data-retention'
|
||||||
},
|
},
|
||||||
microservices: {
|
microservices: {
|
||||||
|
|||||||
@@ -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;
|
module.exports = router;
|
||||||
|
|||||||
192
server/routes/securityLogs.js
Normal file
192
server/routes/securityLogs.js
Normal file
@@ -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;
|
||||||
Reference in New Issue
Block a user