Fix jwt-token
This commit is contained in:
@@ -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() {
|
||||
<Route path="tenants/:tenantId/users" element={<TenantUsersPage />} />
|
||||
<Route path="users" element={<Users />} />
|
||||
<Route path="system" element={<System />} />
|
||||
<Route path="security-logs" element={<SecurityLogs />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
<Toaster
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
BuildingOfficeIcon,
|
||||
UsersIcon,
|
||||
CogIcon,
|
||||
ShieldCheckIcon,
|
||||
ArrowRightOnRectangleIcon
|
||||
} from '@heroicons/react/24/outline'
|
||||
|
||||
@@ -21,6 +22,7 @@ const Layout = () => {
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
|
||||
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',
|
||||
tenants: 'Tenants',
|
||||
users: 'Users',
|
||||
security_logs: 'Security Logs',
|
||||
system: 'System'
|
||||
},
|
||||
navigation: {
|
||||
|
||||
Reference in New Issue
Block a user