Files
Hotel-Booking/Frontend/src/pages/admin/SecurityManagementPage.tsx
Iliyan Angelov cf97df9aeb updates
2025-11-28 20:24:58 +02:00

1557 lines
65 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
Shield,
AlertTriangle,
CheckCircle,
XCircle,
RefreshCw,
X,
Ban,
Globe,
Lock,
Activity,
TrendingUp,
AlertCircle,
Info
} from 'lucide-react';
import { securityService, SecurityEvent, SecurityStats, OAuthProvider, DataSubjectRequest } from '../../services/api/securityService';
import { toast } from 'react-toastify';
import Loading from '../../components/common/Loading';
import Pagination from '../../components/common/Pagination';
import { formatDate } from '../../utils/format';
type SecurityTab = 'events' | 'stats' | 'ip-whitelist' | 'ip-blacklist' | 'oauth' | 'gdpr' | 'scan';
const SecurityManagementPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<SecurityTab>('events');
const [loading, setLoading] = useState(false);
const [events, setEvents] = useState<SecurityEvent[]>([]);
const [stats, setStats] = useState<SecurityStats | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [filters, setFilters] = useState({
event_type: '',
severity: '',
resolved: '',
days: 7,
search: ''
});
useEffect(() => {
if (activeTab === 'events') {
fetchEvents();
} else if (activeTab === 'stats') {
fetchStats();
}
}, [activeTab, filters, currentPage]);
const fetchEvents = async () => {
setLoading(true);
try {
const data = await securityService.getSecurityEvents({
event_type: filters.event_type || undefined,
severity: filters.severity || undefined,
resolved: filters.resolved ? filters.resolved === 'true' : undefined,
days: filters.days,
limit: 20,
offset: (currentPage - 1) * 20
});
setEvents(data);
setTotalPages(Math.ceil(data.length / 20));
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to fetch security events');
} finally {
setLoading(false);
}
};
const fetchStats = async () => {
setLoading(true);
try {
const data = await securityService.getSecurityStats(filters.days);
setStats(data);
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to fetch security statistics');
} finally {
setLoading(false);
}
};
const handleResolveEvent = async (eventId: number) => {
try {
await securityService.resolveSecurityEvent(eventId);
toast.success('Event resolved successfully');
fetchEvents();
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to resolve event');
}
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'critical': return 'bg-red-100 text-red-800 border-red-200';
case 'high': return 'bg-orange-100 text-orange-800 border-orange-200';
case 'medium': return 'bg-yellow-100 text-yellow-800 border-yellow-200';
case 'low': return 'bg-blue-100 text-blue-800 border-blue-200';
default: return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
const getEventTypeIcon = (eventType: string) => {
if (eventType.includes('login')) return <Lock className="w-4 h-4" />;
if (eventType.includes('permission')) return <Ban className="w-4 h-4" />;
if (eventType.includes('suspicious')) return <AlertTriangle className="w-4 h-4" />;
return <Activity className="w-4 h-4" />;
};
if (loading && !events.length && !stats) {
return <Loading />;
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50">
<div className="max-w-7xl mx-auto px-3 sm:px-4 md:px-6 lg:px-8 py-4 sm:py-6 md:py-8 space-y-4 sm:space-y-5 md:space-y-6">
{/* Header */}
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-red-400/5 via-transparent to-orange-600/5 rounded-2xl sm:rounded-3xl blur-3xl"></div>
<div className="relative bg-white/80 backdrop-blur-xl rounded-2xl sm:rounded-3xl shadow-2xl border border-red-200/30 p-4 sm:p-6 md:p-8">
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4 md:gap-5">
<div className="relative flex-shrink-0">
<div className="absolute inset-0 bg-gradient-to-br from-red-400 to-orange-600 rounded-xl sm:rounded-2xl blur-lg opacity-50"></div>
<div className="relative p-3 sm:p-4 rounded-xl sm:rounded-2xl bg-gradient-to-br from-red-500 via-red-500 to-orange-600 shadow-xl border border-red-400/50">
<Shield className="w-6 h-6 sm:w-7 sm:h-7 md:w-8 md:h-8 text-white" />
</div>
</div>
<div className="flex-1 min-w-0">
<h1 className="text-2xl sm:text-3xl md:text-4xl font-extrabold bg-gradient-to-r from-slate-900 via-red-700 to-slate-900 bg-clip-text text-transparent">
Security Management
</h1>
<p className="text-sm sm:text-base text-gray-600 mt-1 sm:mt-2">Monitor and manage security events, IP access, and compliance</p>
</div>
</div>
</div>
</div>
{/* Tabs */}
<div className="bg-white rounded-xl sm:rounded-2xl shadow-lg border border-gray-200 p-2">
<div className="overflow-x-auto">
<div className="flex gap-2 min-w-max sm:min-w-0 sm:flex-wrap">
{[
{ id: 'events', label: 'Security Events', icon: Activity },
{ id: 'stats', label: 'Statistics', icon: TrendingUp },
{ id: 'ip-whitelist', label: 'IP Whitelist', icon: CheckCircle },
{ id: 'ip-blacklist', label: 'IP Blacklist', icon: Ban },
{ id: 'oauth', label: 'OAuth Providers', icon: Globe },
{ id: 'gdpr', label: 'GDPR Requests', icon: Shield },
{ id: 'scan', label: 'Security Scan', icon: Activity }
].map(tab => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as SecurityTab)}
className={`flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1.5 sm:py-2 rounded-lg sm:rounded-xl font-medium transition-all text-xs sm:text-sm whitespace-nowrap ${
activeTab === tab.id
? 'bg-gradient-to-r from-red-500 to-orange-500 text-white shadow-lg'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<Icon className="w-3 h-3 sm:w-4 sm:h-4 flex-shrink-0" />
<span className="hidden xs:inline">{tab.label}</span>
<span className="xs:hidden">{tab.label.split(' ')[0]}</span>
</button>
);
})}
</div>
</div>
</div>
{/* Content */}
<div className="bg-white rounded-xl sm:rounded-2xl shadow-lg border border-gray-200 p-3 sm:p-4 md:p-6">
{activeTab === 'events' && (
<div className="space-y-4 sm:space-y-5 md:space-y-6">
{/* Filters */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
<select
value={filters.event_type}
onChange={(e) => setFilters({ ...filters, event_type: e.target.value })}
className="px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
>
<option value="">All Event Types</option>
<option value="login_attempt">Login Attempt</option>
<option value="login_failure">Login Failure</option>
<option value="permission_denied">Permission Denied</option>
<option value="suspicious_activity">Suspicious Activity</option>
<option value="ip_blocked">IP Blocked</option>
</select>
<select
value={filters.severity}
onChange={(e) => setFilters({ ...filters, severity: e.target.value })}
className="px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
>
<option value="">All Severities</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<select
value={filters.resolved}
onChange={(e) => setFilters({ ...filters, resolved: e.target.value })}
className="px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
>
<option value="">All Status</option>
<option value="false">Unresolved</option>
<option value="true">Resolved</option>
</select>
<select
value={filters.days}
onChange={(e) => setFilters({ ...filters, days: parseInt(e.target.value) })}
className="px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
>
<option value="1">Last 24 hours</option>
<option value="7">Last 7 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
</div>
{/* Events Table - Desktop */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-3 px-4 font-semibold text-gray-700 text-sm">Type</th>
<th className="text-left py-3 px-4 font-semibold text-gray-700 text-sm">Severity</th>
<th className="text-left py-3 px-4 font-semibold text-gray-700 text-sm">IP Address</th>
<th className="text-left py-3 px-4 font-semibold text-gray-700 text-sm">Description</th>
<th className="text-left py-3 px-4 font-semibold text-gray-700 text-sm">Date</th>
<th className="text-left py-3 px-4 font-semibold text-gray-700 text-sm">Status</th>
<th className="text-left py-3 px-4 font-semibold text-gray-700 text-sm">Actions</th>
</tr>
</thead>
<tbody>
{events.map((event) => (
<tr key={event.id} className="border-b border-gray-100 hover:bg-gray-50">
<td className="py-3 px-4">
<div className="flex items-center gap-2">
{getEventTypeIcon(event.event_type)}
<span className="text-sm font-medium">{event.event_type.replace(/_/g, ' ')}</span>
</div>
</td>
<td className="py-3 px-4">
<span className={`px-2 py-1 rounded-full text-xs font-medium border ${getSeverityColor(event.severity)}`}>
{event.severity}
</span>
</td>
<td className="py-3 px-4 text-sm text-gray-600">{event.ip_address || '-'}</td>
<td className="py-3 px-4 text-sm text-gray-600">{event.description || '-'}</td>
<td className="py-3 px-4 text-sm text-gray-600">{formatDate(event.created_at)}</td>
<td className="py-3 px-4">
{event.resolved ? (
<span className="flex items-center gap-1 text-green-600">
<CheckCircle className="w-4 h-4" />
Resolved
</span>
) : (
<span className="flex items-center gap-1 text-orange-600">
<AlertCircle className="w-4 h-4" />
Unresolved
</span>
)}
</td>
<td className="py-3 px-4">
{!event.resolved && (
<button
onClick={() => handleResolveEvent(event.id)}
className="px-3 py-1 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors text-sm"
>
Resolve
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Events Cards - Mobile/Tablet */}
<div className="lg:hidden space-y-3">
{events.map((event) => (
<div key={event.id} className="bg-gray-50 rounded-lg p-4 border border-gray-200 space-y-3">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2 flex-1 min-w-0">
{getEventTypeIcon(event.event_type)}
<span className="text-sm font-medium truncate">{event.event_type.replace(/_/g, ' ')}</span>
</div>
<span className={`px-2 py-1 rounded-full text-xs font-medium border flex-shrink-0 ${getSeverityColor(event.severity)}`}>
{event.severity}
</span>
</div>
<div className="grid grid-cols-2 gap-2 text-sm">
<div>
<span className="text-gray-500">IP:</span>
<span className="ml-1 text-gray-700">{event.ip_address || '-'}</span>
</div>
<div>
<span className="text-gray-500">Date:</span>
<span className="ml-1 text-gray-700">{formatDate(event.created_at)}</span>
</div>
</div>
{event.description && (
<div className="text-sm">
<span className="text-gray-500">Description:</span>
<p className="text-gray-700 mt-1">{event.description}</p>
</div>
)}
<div className="flex items-center justify-between pt-2 border-t border-gray-200">
{event.resolved ? (
<span className="flex items-center gap-1 text-green-600 text-sm">
<CheckCircle className="w-4 h-4" />
Resolved
</span>
) : (
<span className="flex items-center gap-1 text-orange-600 text-sm">
<AlertCircle className="w-4 h-4" />
Unresolved
</span>
)}
{!event.resolved && (
<button
onClick={() => handleResolveEvent(event.id)}
className="px-3 py-1.5 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors text-sm"
>
Resolve
</button>
)}
</div>
</div>
))}
</div>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
</div>
)}
{activeTab === 'stats' && stats && (
<div className="space-y-4 sm:space-y-5 md:space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-5 md:gap-6">
<div className="bg-gradient-to-br from-red-50 to-orange-50 rounded-xl p-4 sm:p-5 md:p-6 border border-red-100">
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0 pr-2">
<p className="text-xs sm:text-sm text-gray-600">Total Events</p>
<p className="text-2xl sm:text-3xl font-bold text-gray-900 mt-1 sm:mt-2">{stats.total_events}</p>
</div>
<Activity className="w-10 h-10 sm:w-12 sm:h-12 text-red-500 opacity-50 flex-shrink-0" />
</div>
</div>
<div className="bg-gradient-to-br from-orange-50 to-yellow-50 rounded-xl p-4 sm:p-5 md:p-6 border border-orange-100">
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0 pr-2">
<p className="text-xs sm:text-sm text-gray-600">Critical Unresolved</p>
<p className="text-2xl sm:text-3xl font-bold text-gray-900 mt-1 sm:mt-2">{stats.unresolved_critical}</p>
</div>
<AlertTriangle className="w-10 h-10 sm:w-12 sm:h-12 text-orange-500 opacity-50 flex-shrink-0" />
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-5 md:gap-6">
<div className="bg-gray-50 rounded-xl p-4 sm:p-5 md:p-6">
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">Events by Type</h3>
<div className="space-y-2">
{Object.entries(stats.by_type).map(([type, count]) => (
<div key={type} className="flex items-center justify-between text-sm sm:text-base">
<span className="text-gray-600 truncate pr-2">{type.replace(/_/g, ' ')}</span>
<span className="font-semibold flex-shrink-0">{count}</span>
</div>
))}
</div>
</div>
<div className="bg-gray-50 rounded-xl p-4 sm:p-5 md:p-6">
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">Events by Severity</h3>
<div className="space-y-2">
{Object.entries(stats.by_severity).map(([severity, count]) => (
<div key={severity} className="flex items-center justify-between text-sm sm:text-base">
<span className={`text-xs sm:text-sm px-2 py-1 rounded flex-shrink-0 ${getSeverityColor(severity)}`}>
{severity}
</span>
<span className="font-semibold ml-auto">{count}</span>
</div>
))}
</div>
</div>
</div>
</div>
)}
{activeTab === 'ip-whitelist' && (
<IPWhitelistTab />
)}
{activeTab === 'ip-blacklist' && (
<IPBlacklistTab />
)}
{activeTab === 'oauth' && (
<OAuthProvidersTab />
)}
{activeTab === 'gdpr' && (
<GDPRRequestsTab />
)}
{activeTab === 'scan' && (
<SecurityScanTab />
)}
</div>
</div>
</div>
);
};
// IP Whitelist Tab Component
const IPWhitelistTab: React.FC = () => {
const [ips, setIPs] = useState<any[]>([]);
const [, setLoading] = useState(false);
const [showAddModal, setShowAddModal] = useState(false);
const [newIP, setNewIP] = useState({ ip_address: '', description: '' });
useEffect(() => {
fetchIPs();
}, []);
const fetchIPs = async () => {
setLoading(true);
try {
const data = await securityService.getWhitelistedIPs();
setIPs(data);
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to fetch whitelisted IPs');
} finally {
setLoading(false);
}
};
const handleAdd = async () => {
try {
await securityService.addIPToWhitelist(newIP.ip_address, newIP.description);
toast.success('IP added to whitelist');
setShowAddModal(false);
setNewIP({ ip_address: '', description: '' });
fetchIPs();
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to add IP to whitelist');
}
};
const handleRemove = async (ipAddress: string) => {
if (!window.confirm('Are you sure you want to remove this IP from whitelist?')) return;
try {
await securityService.removeIPFromWhitelist(ipAddress);
toast.success('IP removed from whitelist');
fetchIPs();
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to remove IP from whitelist');
}
};
return (
<div className="space-y-4">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
<h3 className="text-lg sm:text-xl font-semibold">IP Whitelist</h3>
<button
onClick={() => setShowAddModal(true)}
className="w-full sm:w-auto px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors text-sm sm:text-base"
>
Add IP
</button>
</div>
{showAddModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl p-4 sm:p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
<h4 className="text-base sm:text-lg font-semibold mb-4">Add IP to Whitelist</h4>
<div className="space-y-4">
<input
type="text"
placeholder="IP Address (e.g., 192.168.1.1 or 192.168.1.0/24)"
value={newIP.ip_address}
onChange={(e) => setNewIP({ ...newIP, ip_address: e.target.value })}
className="w-full px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg"
/>
<textarea
placeholder="Description (optional)"
value={newIP.description}
onChange={(e) => setNewIP({ ...newIP, description: e.target.value })}
className="w-full px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg"
rows={3}
/>
<div className="flex flex-col sm:flex-row gap-2">
<button
onClick={handleAdd}
className="flex-1 px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 text-sm sm:text-base"
>
Add
</button>
<button
onClick={() => setShowAddModal(false)}
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 text-sm sm:text-base"
>
Cancel
</button>
</div>
</div>
</div>
</div>
)}
{/* Desktop Table */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-3 px-4 font-semibold text-sm">IP Address</th>
<th className="text-left py-3 px-4 font-semibold text-sm">Description</th>
<th className="text-left py-3 px-4 font-semibold text-sm">Status</th>
<th className="text-left py-3 px-4 font-semibold text-sm">Actions</th>
</tr>
</thead>
<tbody>
{ips.map((ip) => (
<tr key={ip.id} className="border-b border-gray-100">
<td className="py-3 px-4 text-sm">{ip.ip_address}</td>
<td className="py-3 px-4 text-sm text-gray-600">{ip.description || '-'}</td>
<td className="py-3 px-4">
<span className="px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs">
Active
</span>
</td>
<td className="py-3 px-4">
<button
onClick={() => handleRemove(ip.ip_address)}
className="px-3 py-1 bg-red-500 text-white rounded-lg hover:bg-red-600 text-sm"
>
Remove
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile Cards */}
<div className="lg:hidden space-y-3">
{ips.map((ip) => (
<div key={ip.id} className="bg-gray-50 rounded-lg p-4 border border-gray-200 space-y-2">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">{ip.ip_address}</p>
{ip.description && (
<p className="text-xs sm:text-sm text-gray-600 mt-1">{ip.description}</p>
)}
</div>
<span className="px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs flex-shrink-0">
Active
</span>
</div>
<button
onClick={() => handleRemove(ip.ip_address)}
className="w-full sm:w-auto px-3 py-1.5 bg-red-500 text-white rounded-lg hover:bg-red-600 text-sm"
>
Remove
</button>
</div>
))}
</div>
</div>
);
};
// IP Blacklist Tab Component
const IPBlacklistTab: React.FC = () => {
const [ips, setIPs] = useState<any[]>([]);
const [, setLoading] = useState(false);
const [showAddModal, setShowAddModal] = useState(false);
const [newIP, setNewIP] = useState({ ip_address: '', reason: '' });
useEffect(() => {
fetchIPs();
}, []);
const fetchIPs = async () => {
setLoading(true);
try {
const data = await securityService.getBlacklistedIPs();
setIPs(data);
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to fetch blacklisted IPs');
} finally {
setLoading(false);
}
};
const handleAdd = async () => {
try {
await securityService.addIPToBlacklist(newIP.ip_address, newIP.reason);
toast.success('IP added to blacklist');
setShowAddModal(false);
setNewIP({ ip_address: '', reason: '' });
fetchIPs();
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to add IP to blacklist');
}
};
const handleRemove = async (ipAddress: string) => {
if (!window.confirm('Are you sure you want to remove this IP from blacklist?')) return;
try {
await securityService.removeIPFromBlacklist(ipAddress);
toast.success('IP removed from blacklist');
fetchIPs();
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to remove IP from blacklist');
}
};
return (
<div className="space-y-4">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
<h3 className="text-lg sm:text-xl font-semibold">IP Blacklist</h3>
<button
onClick={() => setShowAddModal(true)}
className="w-full sm:w-auto px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors text-sm sm:text-base"
>
Add IP
</button>
</div>
{showAddModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl p-4 sm:p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
<h4 className="text-base sm:text-lg font-semibold mb-4">Add IP to Blacklist</h4>
<div className="space-y-4">
<input
type="text"
placeholder="IP Address"
value={newIP.ip_address}
onChange={(e) => setNewIP({ ...newIP, ip_address: e.target.value })}
className="w-full px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg"
/>
<textarea
placeholder="Reason (optional)"
value={newIP.reason}
onChange={(e) => setNewIP({ ...newIP, reason: e.target.value })}
className="w-full px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg"
rows={3}
/>
<div className="flex flex-col sm:flex-row gap-2">
<button
onClick={handleAdd}
className="flex-1 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 text-sm sm:text-base"
>
Add
</button>
<button
onClick={() => setShowAddModal(false)}
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 text-sm sm:text-base"
>
Cancel
</button>
</div>
</div>
</div>
</div>
)}
{/* Desktop Table */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-3 px-4 font-semibold text-sm">IP Address</th>
<th className="text-left py-3 px-4 font-semibold text-sm">Reason</th>
<th className="text-left py-3 px-4 font-semibold text-sm">Status</th>
<th className="text-left py-3 px-4 font-semibold text-sm">Actions</th>
</tr>
</thead>
<tbody>
{ips.map((ip) => (
<tr key={ip.id} className="border-b border-gray-100">
<td className="py-3 px-4 text-sm">{ip.ip_address}</td>
<td className="py-3 px-4 text-sm text-gray-600">{ip.reason || '-'}</td>
<td className="py-3 px-4">
<span className="px-2 py-1 bg-red-100 text-red-800 rounded-full text-xs">
Blocked
</span>
</td>
<td className="py-3 px-4">
<button
onClick={() => handleRemove(ip.ip_address)}
className="px-3 py-1 bg-green-500 text-white rounded-lg hover:bg-green-600 text-sm"
>
Unblock
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile Cards */}
<div className="lg:hidden space-y-3">
{ips.map((ip) => (
<div key={ip.id} className="bg-gray-50 rounded-lg p-4 border border-gray-200 space-y-2">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">{ip.ip_address}</p>
{ip.reason && (
<p className="text-xs sm:text-sm text-gray-600 mt-1">{ip.reason}</p>
)}
</div>
<span className="px-2 py-1 bg-red-100 text-red-800 rounded-full text-xs flex-shrink-0">
Blocked
</span>
</div>
<button
onClick={() => handleRemove(ip.ip_address)}
className="w-full sm:w-auto px-3 py-1.5 bg-green-500 text-white rounded-lg hover:bg-green-600 text-sm"
>
Unblock
</button>
</div>
))}
</div>
</div>
);
};
// OAuth Providers Tab Component
const OAuthProvidersTab: React.FC = () => {
const [providers, setProviders] = useState<OAuthProvider[]>([]);
const [loading, setLoading] = useState(false);
const [showAddModal, setShowAddModal] = useState(false);
const [editingProvider, setEditingProvider] = useState<OAuthProvider | null>(null);
const [formData, setFormData] = useState({
name: '',
display_name: '',
client_id: '',
client_secret: '',
authorization_url: '',
token_url: '',
userinfo_url: '',
scopes: '',
is_active: true,
is_sso_enabled: false
});
useEffect(() => {
fetchProviders();
}, []);
const fetchProviders = async () => {
setLoading(true);
try {
const data = await securityService.getOAuthProviders();
setProviders(data);
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to fetch OAuth providers');
} finally {
setLoading(false);
}
};
const handleAdd = async () => {
try {
await securityService.createOAuthProvider(formData);
toast.success('OAuth provider created');
setShowAddModal(false);
resetForm();
fetchProviders();
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to create OAuth provider');
}
};
const handleUpdate = async () => {
if (!editingProvider) return;
try {
await securityService.updateOAuthProvider(editingProvider.id, formData);
toast.success('OAuth provider updated');
setEditingProvider(null);
resetForm();
fetchProviders();
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to update OAuth provider');
}
};
const handleDelete = async (providerId: number) => {
if (!window.confirm('Are you sure you want to delete this OAuth provider?')) return;
try {
await securityService.deleteOAuthProvider(providerId);
toast.success('OAuth provider deleted');
fetchProviders();
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to delete OAuth provider');
}
};
const resetForm = () => {
setFormData({
name: '',
display_name: '',
client_id: '',
client_secret: '',
authorization_url: '',
token_url: '',
userinfo_url: '',
scopes: '',
is_active: true,
is_sso_enabled: false
});
};
const openEditModal = (provider: OAuthProvider) => {
setEditingProvider(provider);
setFormData({
name: provider.name,
display_name: provider.display_name,
client_id: '',
client_secret: '',
authorization_url: '',
token_url: '',
userinfo_url: '',
scopes: '',
is_active: provider.is_active,
is_sso_enabled: provider.is_sso_enabled
});
setShowAddModal(true);
};
if (loading && providers.length === 0) {
return <Loading />;
}
return (
<div className="space-y-4">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
<h3 className="text-lg sm:text-xl font-semibold">OAuth Providers</h3>
<button
onClick={() => {
resetForm();
setEditingProvider(null);
setShowAddModal(true);
}}
className="w-full sm:w-auto px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors text-sm sm:text-base"
>
Add Provider
</button>
</div>
{showAddModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl p-4 sm:p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<h4 className="text-base sm:text-lg font-semibold mb-4">
{editingProvider ? 'Edit OAuth Provider' : 'Add OAuth Provider'}
</h4>
<div className="space-y-3 sm:space-y-4">
<input
type="text"
placeholder="Name (e.g., google, microsoft)"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg"
disabled={!!editingProvider}
/>
<input
type="text"
placeholder="Display Name"
value={formData.display_name}
onChange={(e) => setFormData({ ...formData, display_name: e.target.value })}
className="w-full px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg"
/>
<input
type="text"
placeholder="Client ID"
value={formData.client_id}
onChange={(e) => setFormData({ ...formData, client_id: e.target.value })}
className="w-full px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg"
/>
<input
type="password"
placeholder={editingProvider ? "Client Secret (leave blank to keep current)" : "Client Secret"}
value={formData.client_secret}
onChange={(e) => setFormData({ ...formData, client_secret: e.target.value })}
className="w-full px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg"
/>
<input
type="text"
placeholder="Authorization URL"
value={formData.authorization_url}
onChange={(e) => setFormData({ ...formData, authorization_url: e.target.value })}
className="w-full px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg"
/>
<input
type="text"
placeholder="Token URL"
value={formData.token_url}
onChange={(e) => setFormData({ ...formData, token_url: e.target.value })}
className="w-full px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg"
/>
<input
type="text"
placeholder="Userinfo URL"
value={formData.userinfo_url}
onChange={(e) => setFormData({ ...formData, userinfo_url: e.target.value })}
className="w-full px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg"
/>
<input
type="text"
placeholder="Scopes (space-separated)"
value={formData.scopes}
onChange={(e) => setFormData({ ...formData, scopes: e.target.value })}
className="w-full px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg"
/>
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4">
<label className="flex items-center gap-2 text-sm sm:text-base">
<input
type="checkbox"
checked={formData.is_active}
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
className="w-4 h-4"
/>
<span>Active</span>
</label>
<label className="flex items-center gap-2 text-sm sm:text-base">
<input
type="checkbox"
checked={formData.is_sso_enabled}
onChange={(e) => setFormData({ ...formData, is_sso_enabled: e.target.checked })}
className="w-4 h-4"
/>
<span>SSO Enabled</span>
</label>
</div>
<div className="flex flex-col sm:flex-row gap-2 pt-2">
<button
onClick={editingProvider ? handleUpdate : handleAdd}
className="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 text-sm sm:text-base"
>
{editingProvider ? 'Update' : 'Add'}
</button>
<button
onClick={() => {
setShowAddModal(false);
setEditingProvider(null);
resetForm();
}}
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 text-sm sm:text-base"
>
Cancel
</button>
</div>
</div>
</div>
</div>
)}
{/* Desktop Table */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-3 px-4 font-semibold text-sm">Name</th>
<th className="text-left py-3 px-4 font-semibold text-sm">Display Name</th>
<th className="text-left py-3 px-4 font-semibold text-sm">Status</th>
<th className="text-left py-3 px-4 font-semibold text-sm">SSO</th>
<th className="text-left py-3 px-4 font-semibold text-sm">Actions</th>
</tr>
</thead>
<tbody>
{providers.map((provider) => (
<tr key={provider.id} className="border-b border-gray-100">
<td className="py-3 px-4 font-medium text-sm">{provider.name}</td>
<td className="py-3 px-4 text-sm">{provider.display_name}</td>
<td className="py-3 px-4">
<span className={`px-2 py-1 rounded-full text-xs ${
provider.is_active
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}>
{provider.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td className="py-3 px-4">
{provider.is_sso_enabled ? (
<CheckCircle className="w-5 h-5 text-green-500" />
) : (
<XCircle className="w-5 h-5 text-gray-400" />
)}
</td>
<td className="py-3 px-4">
<div className="flex gap-2">
<button
onClick={() => openEditModal(provider)}
className="px-3 py-1 bg-blue-500 text-white rounded-lg hover:bg-blue-600 text-sm"
>
Edit
</button>
<button
onClick={() => handleDelete(provider.id)}
className="px-3 py-1 bg-red-500 text-white rounded-lg hover:bg-red-600 text-sm"
>
Delete
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile Cards */}
<div className="lg:hidden space-y-3">
{providers.map((provider) => (
<div key={provider.id} className="bg-gray-50 rounded-lg p-4 border border-gray-200 space-y-3">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<p className="font-medium text-sm">{provider.name}</p>
<p className="text-xs sm:text-sm text-gray-600 mt-1">{provider.display_name}</p>
</div>
<div className="flex flex-col items-end gap-2 flex-shrink-0">
<span className={`px-2 py-1 rounded-full text-xs ${
provider.is_active
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}>
{provider.is_active ? 'Active' : 'Inactive'}
</span>
{provider.is_sso_enabled ? (
<CheckCircle className="w-5 h-5 text-green-500" />
) : (
<XCircle className="w-5 h-5 text-gray-400" />
)}
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2 pt-2 border-t border-gray-200">
<button
onClick={() => openEditModal(provider)}
className="flex-1 px-3 py-1.5 bg-blue-500 text-white rounded-lg hover:bg-blue-600 text-sm"
>
Edit
</button>
<button
onClick={() => handleDelete(provider.id)}
className="flex-1 px-3 py-1.5 bg-red-500 text-white rounded-lg hover:bg-red-600 text-sm"
>
Delete
</button>
</div>
</div>
))}
</div>
</div>
);
};
// GDPR Requests Tab Component
const GDPRRequestsTab: React.FC = () => {
const [requests, setRequests] = useState<DataSubjectRequest[]>([]);
const [loading, setLoading] = useState(false);
const [selectedRequest, setSelectedRequest] = useState<DataSubjectRequest | null>(null);
const [filters, setFilters] = useState({
status: '',
request_type: ''
});
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
useEffect(() => {
fetchRequests();
}, [filters, currentPage]);
const fetchRequests = async () => {
setLoading(true);
try {
const data = await securityService.getGDPRRequests({
status: filters.status || undefined,
request_type: filters.request_type || undefined,
limit: 20,
offset: (currentPage - 1) * 20
});
setRequests(data);
setTotalPages(Math.ceil(data.length / 20));
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to fetch GDPR requests');
} finally {
setLoading(false);
}
};
const handleAssign = async (requestId: number) => {
try {
await securityService.assignGDPRRequest(requestId);
toast.success('Request assigned to you');
fetchRequests();
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to assign request');
}
};
const handleComplete = async (requestId: number) => {
try {
await securityService.completeGDPRRequest(requestId);
toast.success('Request marked as completed');
setSelectedRequest(null);
fetchRequests();
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to complete request');
}
};
const handleViewDetails = async (requestId: number) => {
try {
const request = await securityService.getGDPRRequest(requestId);
setSelectedRequest(request);
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to fetch request details');
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'bg-green-100 text-green-800';
case 'in_progress': return 'bg-blue-100 text-blue-800';
case 'pending': return 'bg-yellow-100 text-yellow-800';
case 'rejected': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
};
if (loading && requests.length === 0) {
return <Loading />;
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg sm:text-xl font-semibold">GDPR Requests</h3>
</div>
{/* Filters */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
<select
value={filters.status}
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
className="px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg"
>
<option value="">All Statuses</option>
<option value="pending">Pending</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
<option value="rejected">Rejected</option>
</select>
<select
value={filters.request_type}
onChange={(e) => setFilters({ ...filters, request_type: e.target.value })}
className="px-3 sm:px-4 py-2 text-sm sm:text-base border border-gray-300 rounded-lg"
>
<option value="">All Types</option>
<option value="access">Access</option>
<option value="erasure">Erasure</option>
<option value="portability">Portability</option>
<option value="rectification">Rectification</option>
</select>
</div>
{/* Desktop Table */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-3 px-4 font-semibold text-sm">ID</th>
<th className="text-left py-3 px-4 font-semibold text-sm">Email</th>
<th className="text-left py-3 px-4 font-semibold text-sm">Type</th>
<th className="text-left py-3 px-4 font-semibold text-sm">Status</th>
<th className="text-left py-3 px-4 font-semibold text-sm">Verified</th>
<th className="text-left py-3 px-4 font-semibold text-sm">Date</th>
<th className="text-left py-3 px-4 font-semibold text-sm">Actions</th>
</tr>
</thead>
<tbody>
{requests.map((request) => (
<tr key={request.id} className="border-b border-gray-100 hover:bg-gray-50">
<td className="py-3 px-4 text-sm">#{request.id}</td>
<td className="py-3 px-4 text-sm">{request.email}</td>
<td className="py-3 px-4 text-sm">{request.request_type}</td>
<td className="py-3 px-4">
<span className={`px-2 py-1 rounded-full text-xs ${getStatusColor(request.status)}`}>
{request.status}
</span>
</td>
<td className="py-3 px-4">
{request.verified ? (
<CheckCircle className="w-5 h-5 text-green-500" />
) : (
<XCircle className="w-5 h-5 text-gray-400" />
)}
</td>
<td className="py-3 px-4 text-sm">{formatDate(request.created_at)}</td>
<td className="py-3 px-4">
<div className="flex gap-2">
<button
onClick={() => handleViewDetails(request.id)}
className="px-3 py-1 bg-blue-500 text-white rounded-lg hover:bg-blue-600 text-sm"
>
View
</button>
{request.status === 'pending' && (
<button
onClick={() => handleAssign(request.id)}
className="px-3 py-1 bg-green-500 text-white rounded-lg hover:bg-green-600 text-sm"
>
Assign
</button>
)}
{request.status === 'in_progress' && (
<button
onClick={() => handleComplete(request.id)}
className="px-3 py-1 bg-purple-500 text-white rounded-lg hover:bg-purple-600 text-sm"
>
Complete
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile Cards */}
<div className="lg:hidden space-y-3">
{requests.map((request) => (
<div key={request.id} className="bg-gray-50 rounded-lg p-4 border border-gray-200 space-y-3">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-semibold text-sm">#{request.id}</span>
<span className={`px-2 py-1 rounded-full text-xs flex-shrink-0 ${getStatusColor(request.status)}`}>
{request.status}
</span>
</div>
<p className="text-sm text-gray-600 truncate">{request.email}</p>
</div>
{request.verified ? (
<CheckCircle className="w-5 h-5 text-green-500 flex-shrink-0" />
) : (
<XCircle className="w-5 h-5 text-gray-400 flex-shrink-0" />
)}
</div>
<div className="grid grid-cols-2 gap-2 text-sm">
<div>
<span className="text-gray-500">Type:</span>
<span className="ml-1 text-gray-700">{request.request_type}</span>
</div>
<div>
<span className="text-gray-500">Date:</span>
<span className="ml-1 text-gray-700">{formatDate(request.created_at)}</span>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2 pt-2 border-t border-gray-200">
<button
onClick={() => handleViewDetails(request.id)}
className="flex-1 px-3 py-1.5 bg-blue-500 text-white rounded-lg hover:bg-blue-600 text-sm"
>
View
</button>
{request.status === 'pending' && (
<button
onClick={() => handleAssign(request.id)}
className="flex-1 px-3 py-1.5 bg-green-500 text-white rounded-lg hover:bg-green-600 text-sm"
>
Assign
</button>
)}
{request.status === 'in_progress' && (
<button
onClick={() => handleComplete(request.id)}
className="flex-1 px-3 py-1.5 bg-purple-500 text-white rounded-lg hover:bg-purple-600 text-sm"
>
Complete
</button>
)}
</div>
</div>
))}
</div>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
{/* Request Details Modal */}
{selectedRequest && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl p-4 sm:p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-start mb-4">
<h4 className="text-base sm:text-lg font-semibold">GDPR Request Details</h4>
<button
onClick={() => setSelectedRequest(null)}
className="text-gray-500 hover:text-gray-700 flex-shrink-0 ml-2"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="space-y-3 sm:space-y-4">
<div>
<label className="text-xs sm:text-sm font-medium text-gray-600">Request Type</label>
<p className="mt-1 text-sm sm:text-base">{selectedRequest.request_type}</p>
</div>
<div>
<label className="text-xs sm:text-sm font-medium text-gray-600">Email</label>
<p className="mt-1 text-sm sm:text-base break-words">{selectedRequest.email}</p>
</div>
<div>
<label className="text-xs sm:text-sm font-medium text-gray-600">Status</label>
<p className="mt-1">
<span className={`px-2 py-1 rounded-full text-xs ${getStatusColor(selectedRequest.status)}`}>
{selectedRequest.status}
</span>
</p>
</div>
{selectedRequest.description && (
<div>
<label className="text-xs sm:text-sm font-medium text-gray-600">Description</label>
<p className="mt-1 text-sm sm:text-base">{selectedRequest.description}</p>
</div>
)}
<div>
<label className="text-xs sm:text-sm font-medium text-gray-600">Created At</label>
<p className="mt-1 text-sm sm:text-base">{formatDate(selectedRequest.created_at)}</p>
</div>
{selectedRequest.status === 'in_progress' && (
<button
onClick={() => handleComplete(selectedRequest.id)}
className="w-full px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 text-sm sm:text-base"
>
Mark as Completed
</button>
)}
</div>
</div>
</div>
)}
</div>
);
};
// Security Scan Tab Component
const SecurityScanTab: React.FC = () => {
const [scanning, setScanning] = useState(false);
const [scanResults, setScanResults] = useState<any>(null);
const [scheduleInterval, setScheduleInterval] = useState(24);
const [scheduled, setScheduled] = useState(false);
const handleRunScan = async () => {
setScanning(true);
setScanResults(null);
try {
const results = await securityService.runSecurityScan();
setScanResults(results);
toast.success(`Security scan completed: ${results.total_issues || 0} issues found`);
} catch (error: any) {
console.error('Security scan error:', error);
const errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message || 'Failed to run security scan';
toast.error(errorMessage);
} finally {
setScanning(false);
}
};
const handleScheduleScan = async () => {
try {
await securityService.scheduleSecurityScan(scheduleInterval);
setScheduled(true);
toast.success(`Security scan scheduled to run every ${scheduleInterval} hours`);
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to schedule security scan');
}
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'critical': return 'bg-red-100 text-red-800 border-red-200';
case 'high': return 'bg-orange-100 text-orange-800 border-orange-200';
case 'medium': return 'bg-yellow-100 text-yellow-800 border-yellow-200';
case 'low': return 'bg-blue-100 text-blue-800 border-blue-200';
default: return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
return (
<div className="space-y-4 sm:space-y-5 md:space-y-6">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
<div className="flex-1 min-w-0">
<h3 className="text-lg sm:text-xl font-semibold">Security Scanning</h3>
<p className="text-xs sm:text-sm text-gray-600 mt-1">Run automated security scans to detect vulnerabilities</p>
</div>
<button
onClick={handleRunScan}
disabled={scanning}
className="w-full sm:w-auto px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 text-sm sm:text-base"
>
{scanning ? (
<>
<RefreshCw className="w-4 h-4 animate-spin" />
Scanning...
</>
) : (
<>
<Activity className="w-4 h-4" />
Run Scan Now
</>
)}
</button>
</div>
{/* Schedule Section */}
<div className="bg-gray-50 rounded-xl p-4 sm:p-5 md:p-6 border border-gray-200">
<h4 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">Schedule Automatic Scans</h4>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 sm:gap-4">
<label className="flex flex-col sm:flex-row items-start sm:items-center gap-2 text-sm sm:text-base">
<span>Run scan every</span>
<div className="flex items-center gap-2 w-full sm:w-auto">
<input
type="number"
min="1"
max="168"
value={scheduleInterval}
onChange={(e) => setScheduleInterval(parseInt(e.target.value))}
className="w-20 px-3 py-2 text-sm sm:text-base border border-gray-300 rounded-lg"
/>
<span>hours</span>
</div>
</label>
<button
onClick={handleScheduleScan}
className="w-full sm:w-auto px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 text-sm sm:text-base"
>
Schedule
</button>
</div>
{scheduled && (
<p className="mt-2 text-xs sm:text-sm text-green-600">
Scans scheduled to run every {scheduleInterval} hours
</p>
)}
<p className="mt-4 text-xs sm:text-sm text-gray-600">
<Info className="w-3 h-3 sm:w-4 sm:h-4 inline mr-1" />
In production, use a task scheduler (like Celery or APScheduler) to run scans automatically.
</p>
</div>
{/* Scan Results */}
{scanResults && (
<div className="space-y-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
<h4 className="text-base sm:text-lg font-semibold">Scan Results</h4>
<div className="text-xs sm:text-sm text-gray-600">
Completed in {scanResults.duration_seconds?.toFixed(2)}s
</div>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">
<div className="bg-red-50 rounded-xl p-3 sm:p-4 border border-red-200">
<div className="text-xs sm:text-sm text-red-600 font-medium">Critical</div>
<div className="text-xl sm:text-2xl font-bold text-red-800 mt-1">{scanResults.critical_issues || 0}</div>
</div>
<div className="bg-orange-50 rounded-xl p-3 sm:p-4 border border-orange-200">
<div className="text-xs sm:text-sm text-orange-600 font-medium">High</div>
<div className="text-xl sm:text-2xl font-bold text-orange-800 mt-1">{scanResults.high_issues || 0}</div>
</div>
<div className="bg-yellow-50 rounded-xl p-3 sm:p-4 border border-yellow-200">
<div className="text-xs sm:text-sm text-yellow-600 font-medium">Medium</div>
<div className="text-xl sm:text-2xl font-bold text-yellow-800 mt-1">{scanResults.medium_issues || 0}</div>
</div>
<div className="bg-blue-50 rounded-xl p-3 sm:p-4 border border-blue-200">
<div className="text-xs sm:text-sm text-blue-600 font-medium">Low</div>
<div className="text-xl sm:text-2xl font-bold text-blue-800 mt-1">{scanResults.low_issues || 0}</div>
</div>
<div className="bg-gray-50 rounded-xl p-3 sm:p-4 border border-gray-200 col-span-2 sm:col-span-1">
<div className="text-xs sm:text-sm text-gray-600 font-medium">Total</div>
<div className="text-xl sm:text-2xl font-bold text-gray-800 mt-1">{scanResults.total_issues || 0}</div>
</div>
</div>
{/* Check Results */}
<div className="space-y-3">
{scanResults.checks?.map((check: any, index: number) => (
<div
key={index}
className={`border rounded-lg sm:rounded-xl p-3 sm:p-4 ${
check.status === 'failed' ? 'border-red-200 bg-red-50' :
check.status === 'warning' ? 'border-yellow-200 bg-yellow-50' :
'border-green-200 bg-green-50'
}`}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 mb-2">
<h5 className="font-semibold text-sm sm:text-base">{check.check_name}</h5>
<span className={`px-2 py-1 rounded-full text-xs border flex-shrink-0 ${getSeverityColor(check.severity)}`}>
{check.severity}
</span>
<span className={`px-2 py-1 rounded-full text-xs flex-shrink-0 ${
check.status === 'failed' ? 'bg-red-100 text-red-800' :
check.status === 'warning' ? 'bg-yellow-100 text-yellow-800' :
'bg-green-100 text-green-800'
}`}>
{check.status}
</span>
</div>
<p className="text-xs sm:text-sm text-gray-700 mb-2">{check.description}</p>
{check.recommendation && (
<p className="text-xs sm:text-sm text-blue-700 font-medium">
💡 Recommendation: {check.recommendation}
</p>
)}
{check.issue_count > 0 && (
<p className="text-xs sm:text-sm text-gray-600 mt-2">
Issues found: {check.issue_count}
</p>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
{!scanResults && !scanning && (
<div className="text-center py-8 sm:py-12 bg-gray-50 rounded-xl border border-gray-200 px-4">
<Activity className="w-12 h-12 sm:w-16 sm:h-16 text-gray-400 mx-auto mb-3 sm:mb-4" />
<p className="text-sm sm:text-base text-gray-600">No scan results yet. Click "Run Scan Now" to start a security scan.</p>
</div>
)}
</div>
);
};
export default SecurityManagementPage;