Fix jwt-token

This commit is contained in:
2025-09-24 05:17:53 +02:00
parent 3e61013a8a
commit 1a72774848
9 changed files with 870 additions and 0 deletions

View File

@@ -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';

View 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;

View File

@@ -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

View File

@@ -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 },
] ]

View 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;

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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;

View 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;