Fix jwt-token
This commit is contained in:
@@ -1,8 +1,5 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react';
|
import React, { useState, useEffect, useContext } from 'react';
|
||||||
import { AuthContext } from '../contexts/AuthContext';
|
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';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
|
|
||||||
const SecurityLogs = () => {
|
const SecurityLogs = () => {
|
||||||
@@ -23,19 +20,12 @@ const SecurityLogs = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only allow admins to view security logs
|
if (user && tenant) {
|
||||||
if (!user || user.role !== 'admin') {
|
|
||||||
setError('Access denied. Only tenant administrators can view security logs.');
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadSecurityLogs();
|
loadSecurityLogs();
|
||||||
}, [filters, pagination.page, user]);
|
}
|
||||||
|
}, [user, tenant, filters, pagination.page]);
|
||||||
|
|
||||||
const loadSecurityLogs = async () => {
|
const loadSecurityLogs = async () => {
|
||||||
if (!user || user.role !== 'admin') return;
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
@@ -44,10 +34,10 @@ const SecurityLogs = () => {
|
|||||||
...filters
|
...filters
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch(`/api/security-logs?${params}`, {
|
const response = await fetch(`/api/${tenant.slug}/security-logs?${params}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
'Authorization': `Bearer ${user.token}`,
|
||||||
'X-Tenant-ID': tenant?.id || ''
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,11 +61,11 @@ const SecurityLogs = () => {
|
|||||||
|
|
||||||
const getLogLevelBadge = (level) => {
|
const getLogLevelBadge = (level) => {
|
||||||
const styles = {
|
const styles = {
|
||||||
'critical': 'bg-red-500 text-white',
|
'critical': 'bg-red-500 text-white px-2 py-1 rounded text-xs font-semibold',
|
||||||
'high': 'bg-orange-500 text-white',
|
'high': 'bg-orange-500 text-white px-2 py-1 rounded text-xs font-semibold',
|
||||||
'medium': 'bg-yellow-500 text-black',
|
'medium': 'bg-yellow-500 text-black px-2 py-1 rounded text-xs font-semibold',
|
||||||
'low': 'bg-blue-500 text-white',
|
'low': 'bg-blue-500 text-white px-2 py-1 rounded text-xs font-semibold',
|
||||||
'info': 'bg-gray-500 text-white'
|
'info': 'bg-gray-500 text-white px-2 py-1 rounded text-xs font-semibold'
|
||||||
};
|
};
|
||||||
return styles[level] || styles.info;
|
return styles[level] || styles.info;
|
||||||
};
|
};
|
||||||
@@ -100,55 +90,48 @@ const SecurityLogs = () => {
|
|||||||
if (metadata.ip_address) items.push(`IP: ${metadata.ip_address}`);
|
if (metadata.ip_address) items.push(`IP: ${metadata.ip_address}`);
|
||||||
if (metadata.country) items.push(`Country: ${metadata.country}`);
|
if (metadata.country) items.push(`Country: ${metadata.country}`);
|
||||||
if (metadata.user_agent) items.push(`Agent: ${metadata.user_agent.substring(0, 50)}...`);
|
if (metadata.user_agent) items.push(`Agent: ${metadata.user_agent.substring(0, 50)}...`);
|
||||||
if (metadata.username) items.push(`User: ${metadata.username}`);
|
|
||||||
return items.join(' | ');
|
return items.join(' | ');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Access control check
|
const totalPages = Math.ceil(pagination.total / pagination.limit);
|
||||||
if (!user || user.role !== 'admin') {
|
|
||||||
|
// Don't render if user is not authenticated
|
||||||
|
if (!user || !tenant) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<Alert className="border-red-200 bg-red-50">
|
<div className="text-center py-8 text-gray-500">
|
||||||
<AlertDescription className="text-red-800">
|
Please log in to view security logs
|
||||||
Access denied. Only tenant administrators can view security logs.
|
</div>
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalPages = Math.ceil(pagination.total / pagination.limit);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-3xl font-bold mb-2">Security Logs</h1>
|
<h1 className="text-3xl font-bold mb-2 text-gray-900">Security Logs</h1>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">Monitor security events for your tenant: {tenant.name}</p>
|
||||||
Security events for {tenant?.name || 'your organization'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<Alert className="mb-6 border-red-200 bg-red-50">
|
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-md">
|
||||||
<AlertDescription className="text-red-800">
|
<div className="text-red-800">{error}</div>
|
||||||
{error}
|
</div>
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<Card className="mb-6">
|
<div className="bg-white rounded-lg shadow mb-6">
|
||||||
<CardHeader>
|
<div className="px-6 py-4 border-b border-gray-200">
|
||||||
<CardTitle>Filters</CardTitle>
|
<h3 className="text-lg font-medium text-gray-900">Filters</h3>
|
||||||
</CardHeader>
|
</div>
|
||||||
<CardContent>
|
<div className="p-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-2">Security Level</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">Security Level</label>
|
||||||
<select
|
<select
|
||||||
value={filters.level}
|
value={filters.level}
|
||||||
onChange={(e) => setFilters(prev => ({ ...prev, level: e.target.value }))}
|
onChange={(e) => setFilters(prev => ({ ...prev, level: e.target.value }))}
|
||||||
className="w-full p-2 border rounded-md"
|
className="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
||||||
>
|
>
|
||||||
<option value="all">All Levels</option>
|
<option value="all">All Levels</option>
|
||||||
<option value="critical">Critical</option>
|
<option value="critical">Critical</option>
|
||||||
@@ -159,11 +142,11 @@ const SecurityLogs = () => {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-2">Event Type</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">Event Type</label>
|
||||||
<select
|
<select
|
||||||
value={filters.eventType}
|
value={filters.eventType}
|
||||||
onChange={(e) => setFilters(prev => ({ ...prev, eventType: e.target.value }))}
|
onChange={(e) => setFilters(prev => ({ ...prev, eventType: e.target.value }))}
|
||||||
className="w-full p-2 border rounded-md"
|
className="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
||||||
>
|
>
|
||||||
<option value="all">All Events</option>
|
<option value="all">All Events</option>
|
||||||
<option value="failed_login">Failed Logins</option>
|
<option value="failed_login">Failed Logins</option>
|
||||||
@@ -175,11 +158,11 @@ const SecurityLogs = () => {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-2">Time Range</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">Time Range</label>
|
||||||
<select
|
<select
|
||||||
value={filters.timeRange}
|
value={filters.timeRange}
|
||||||
onChange={(e) => setFilters(prev => ({ ...prev, timeRange: e.target.value }))}
|
onChange={(e) => setFilters(prev => ({ ...prev, timeRange: e.target.value }))}
|
||||||
className="w-full p-2 border rounded-md"
|
className="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
||||||
>
|
>
|
||||||
<option value="1h">Last Hour</option>
|
<option value="1h">Last Hour</option>
|
||||||
<option value="24h">Last 24 Hours</option>
|
<option value="24h">Last 24 Hours</option>
|
||||||
@@ -189,30 +172,30 @@ const SecurityLogs = () => {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-2">Search</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">Search</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="IP, username..."
|
placeholder="IP, username..."
|
||||||
value={filters.search}
|
value={filters.search}
|
||||||
onChange={(e) => setFilters(prev => ({ ...prev, search: e.target.value }))}
|
onChange={(e) => setFilters(prev => ({ ...prev, search: e.target.value }))}
|
||||||
className="w-full p-2 border rounded-md"
|
className="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
|
|
||||||
{/* Security Logs Table */}
|
{/* Security Logs Table */}
|
||||||
<Card>
|
<div className="bg-white rounded-lg shadow">
|
||||||
<CardHeader>
|
<div className="px-6 py-4 border-b border-gray-200">
|
||||||
<CardTitle className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
Security Events
|
<h3 className="text-lg font-medium text-gray-900">Security Events</h3>
|
||||||
<span className="text-sm font-normal text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
{pagination.total} total events
|
{pagination.total} total events
|
||||||
</span>
|
</span>
|
||||||
</CardTitle>
|
</div>
|
||||||
</CardHeader>
|
</div>
|
||||||
<CardContent>
|
<div className="p-6">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex justify-center py-8">
|
<div className="flex justify-center py-8">
|
||||||
<div className="text-gray-500">Loading security logs...</div>
|
<div className="text-gray-500">Loading security logs...</div>
|
||||||
@@ -225,40 +208,40 @@ const SecurityLogs = () => {
|
|||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b">
|
<tr className="border-b border-gray-200">
|
||||||
<th className="text-left p-2">Time</th>
|
<th className="text-left p-3 text-sm font-medium text-gray-500 uppercase tracking-wider">Time</th>
|
||||||
<th className="text-left p-2">Level</th>
|
<th className="text-left p-3 text-sm font-medium text-gray-500 uppercase tracking-wider">Level</th>
|
||||||
<th className="text-left p-2">Event</th>
|
<th className="text-left p-3 text-sm font-medium text-gray-500 uppercase tracking-wider">Event</th>
|
||||||
<th className="text-left p-2">Message</th>
|
<th className="text-left p-3 text-sm font-medium text-gray-500 uppercase tracking-wider">Message</th>
|
||||||
<th className="text-left p-2">Details</th>
|
<th className="text-left p-3 text-sm font-medium text-gray-500 uppercase tracking-wider">Details</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{logs.map((log) => (
|
{logs.map((log) => (
|
||||||
<tr key={log.id} className="border-b hover:bg-gray-50">
|
<tr key={log.id} className="border-b border-gray-100 hover:bg-gray-50">
|
||||||
<td className="p-2 text-sm">
|
<td className="p-3 text-sm">
|
||||||
<div>{new Date(log.timestamp).toLocaleString()}</div>
|
<div>{new Date(log.timestamp).toLocaleString()}</div>
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500">
|
||||||
{formatDistanceToNow(new Date(log.timestamp), { addSuffix: true })}
|
{formatDistanceToNow(new Date(log.timestamp), { addSuffix: true })}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2">
|
<td className="p-3">
|
||||||
<Badge className={getLogLevelBadge(log.level)}>
|
<span className={getLogLevelBadge(log.level)}>
|
||||||
{log.level.toUpperCase()}
|
{log.level.toUpperCase()}
|
||||||
</Badge>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2">
|
<td className="p-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{getEventTypeIcon(log.event_type)}</span>
|
<span>{getEventTypeIcon(log.event_type)}</span>
|
||||||
<span className="text-sm">{log.event_type.replace('_', ' ').toUpperCase()}</span>
|
<span className="text-sm">{log.event_type.replace('_', ' ').toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2 text-sm max-w-md">
|
<td className="p-3 text-sm max-w-md">
|
||||||
<div className="truncate" title={log.message}>
|
<div className="truncate" title={log.message}>
|
||||||
{log.message}
|
{log.message}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2 text-xs text-gray-600 max-w-md">
|
<td className="p-3 text-xs text-gray-600 max-w-md">
|
||||||
<div className="truncate" title={formatMetadata(log.metadata)}>
|
<div className="truncate" title={formatMetadata(log.metadata)}>
|
||||||
{formatMetadata(log.metadata)}
|
{formatMetadata(log.metadata)}
|
||||||
</div>
|
</div>
|
||||||
@@ -280,22 +263,22 @@ const SecurityLogs = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setPagination(prev => ({ ...prev, page: Math.max(1, prev.page - 1) }))}
|
onClick={() => setPagination(prev => ({ ...prev, page: Math.max(1, prev.page - 1) }))}
|
||||||
disabled={pagination.page === 1}
|
disabled={pagination.page === 1}
|
||||||
className="px-3 py-1 text-sm border rounded disabled:opacity-50"
|
className="px-3 py-1 text-sm border border-gray-300 rounded disabled:opacity-50 hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPagination(prev => ({ ...prev, page: Math.min(totalPages, prev.page + 1) }))}
|
onClick={() => setPagination(prev => ({ ...prev, page: Math.min(totalPages, prev.page + 1) }))}
|
||||||
disabled={pagination.page === totalPages}
|
disabled={pagination.page === totalPages}
|
||||||
className="px-3 py-1 text-sm border rounded disabled:opacity-50"
|
className="px-3 py-1 text-sm border border-gray-300 rounded disabled:opacity-50 hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user