Files
Hotel-Booking/Frontend/src/pages/accountant/SecurityManagementPage.tsx
Iliyan Angelov 9f1aeb32da updates
2025-12-04 15:10:07 +02:00

282 lines
12 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Shield, Activity, LogOut, AlertTriangle, CheckCircle2, Clock } from 'lucide-react';
import { toast } from 'react-toastify';
import accountantSecurityService, { AccountantSession, AccountantActivityLog, MFAStatus } from '../../features/security/services/accountantSecurityService';
import Loading from '../../shared/components/Loading';
import EmptyState from '../../shared/components/EmptyState';
import { formatDate } from '../../shared/utils/format';
const SecurityManagementPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'sessions' | 'activity' | 'mfa'>('sessions');
const [loading, setLoading] = useState(true);
const [sessions, setSessions] = useState<AccountantSession[]>([]);
const [activityLogs, setActivityLogs] = useState<AccountantActivityLog[]>([]);
const [mfaStatus, setMfaStatus] = useState<MFAStatus | null>(null);
const [pagination, setPagination] = useState<any>(null);
useEffect(() => {
if (activeTab === 'sessions') fetchSessions();
else if (activeTab === 'activity') fetchActivityLogs();
else if (activeTab === 'mfa') fetchMFAStatus();
}, [activeTab]);
const fetchSessions = async () => {
try {
setLoading(true);
const response = await accountantSecurityService.getSessions();
setSessions(response.data.sessions || []);
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to load sessions');
} finally {
setLoading(false);
}
};
const fetchActivityLogs = async () => {
try {
setLoading(true);
const response = await accountantSecurityService.getActivityLogs({ limit: 50 });
setActivityLogs(response.data.logs || []);
setPagination(response.data.pagination);
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to load activity logs');
} finally {
setLoading(false);
}
};
const fetchMFAStatus = async () => {
try {
setLoading(true);
const response = await accountantSecurityService.getMFAStatus();
setMfaStatus(response.data);
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to load MFA status');
} finally {
setLoading(false);
}
};
const handleRevokeSession = async (sessionId: number) => {
if (!confirm('Are you sure you want to revoke this session?')) return;
try {
await accountantSecurityService.revokeSession(sessionId);
toast.success('Session revoked successfully');
fetchSessions();
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to revoke session');
}
};
const handleRevokeAllSessions = async () => {
if (!confirm('Are you sure you want to revoke all sessions? You will be logged out.')) return;
try {
await accountantSecurityService.revokeAllSessions();
toast.success('All sessions revoked successfully');
fetchSessions();
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to revoke sessions');
}
};
const getRiskColor = (risk: string) => {
switch (risk) {
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';
default:
return 'bg-blue-100 text-blue-800 border-blue-200';
}
};
if (loading && sessions.length === 0 && activityLogs.length === 0 && !mfaStatus) {
return <Loading fullScreen text="Loading security information..." />;
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50 px-3 sm:px-4 md:px-6 lg:px-8 py-6 sm:py-8 md:py-10">
{/* Header */}
<div className="mb-6 sm:mb-8 md:mb-10">
<div className="flex items-center gap-2 sm:gap-3 mb-2 sm:mb-3">
<div className="h-1 w-12 sm:w-16 md:w-20 bg-gradient-to-r from-emerald-400 via-green-500 to-emerald-600 rounded-full"></div>
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 bg-clip-text text-transparent">
Security Management
</h1>
</div>
<p className="text-slate-600 text-sm sm:text-base md:text-lg font-light">
Manage sessions, view activity logs, and MFA settings
</p>
</div>
{/* Tabs */}
<div className="flex flex-wrap gap-2 mb-6 border-b border-slate-200">
<button
onClick={() => setActiveTab('sessions')}
className={`px-4 py-2 font-semibold rounded-t-lg transition-colors ${
activeTab === 'sessions'
? 'bg-emerald-500 text-white border-b-2 border-emerald-600'
: 'text-slate-600 hover:text-emerald-600'
}`}
>
<LogOut className="inline w-4 h-4 mr-2" />
Active Sessions
</button>
<button
onClick={() => setActiveTab('activity')}
className={`px-4 py-2 font-semibold rounded-t-lg transition-colors ${
activeTab === 'activity'
? 'bg-emerald-500 text-white border-b-2 border-emerald-600'
: 'text-slate-600 hover:text-emerald-600'
}`}
>
<Activity className="inline w-4 h-4 mr-2" />
Activity Logs
</button>
<button
onClick={() => setActiveTab('mfa')}
className={`px-4 py-2 font-semibold rounded-t-lg transition-colors ${
activeTab === 'mfa'
? 'bg-emerald-500 text-white border-b-2 border-emerald-600'
: 'text-slate-600 hover:text-emerald-600'
}`}
>
<Shield className="inline w-4 h-4 mr-2" />
MFA Status
</button>
</div>
{/* Content */}
{activeTab === 'sessions' && (
<div className="bg-white rounded-xl shadow-lg p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Active Sessions</h2>
<button
onClick={handleRevokeAllSessions}
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
>
Revoke All Sessions
</button>
</div>
{sessions.length === 0 ? (
<EmptyState title="No Active Sessions" description="Active sessions will appear here" />
) : (
<div className="space-y-4">
{sessions.map((session) => (
<div key={session.id} className="border border-slate-200 rounded-lg p-4">
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="font-bold">Session #{session.id}</h3>
{session.step_up_authenticated && (
<span className="px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs">
Step-up Authenticated
</span>
)}
</div>
<div className="space-y-1 text-sm text-slate-600">
<p>IP Address: {session.ip_address || 'N/A'}</p>
<p>Location: {session.city && session.country ? `${session.city}, ${session.country}` : 'N/A'}</p>
<p>Last Activity: {formatDate(session.last_activity)}</p>
<p>Expires: {formatDate(session.expires_at)}</p>
</div>
</div>
<button
onClick={() => handleRevokeSession(session.id)}
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
>
Revoke
</button>
</div>
</div>
))}
</div>
)}
</div>
)}
{activeTab === 'activity' && (
<div className="bg-white rounded-xl shadow-lg p-6">
<h2 className="text-xl font-bold mb-4">Activity Logs</h2>
{activityLogs.length === 0 ? (
<EmptyState title="No Activity Logs" description="Activity logs will appear here" />
) : (
<div className="space-y-4">
{activityLogs.map((log) => (
<div key={log.id} className="border border-slate-200 rounded-lg p-4">
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="font-bold">{log.activity_type.replace(/_/g, ' ')}</h3>
<span className={`px-2 py-1 rounded-full text-xs font-semibold border ${getRiskColor(log.risk_level)}`}>
{log.risk_level}
</span>
{log.is_unusual && (
<span className="px-2 py-1 bg-orange-100 text-orange-800 rounded-full text-xs">
Unusual
</span>
)}
</div>
<p className="text-slate-600 mb-2">{log.activity_description}</p>
<div className="flex flex-wrap gap-4 text-sm text-slate-500">
<span>IP: {log.ip_address || 'N/A'}</span>
{log.country && <span>Location: {log.country}</span>}
<span></span>
<span>{formatDate(log.created_at)}</span>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{activeTab === 'mfa' && mfaStatus && (
<div className="bg-white rounded-xl shadow-lg p-6">
<h2 className="text-xl font-bold mb-4">MFA Status</h2>
<div className="space-y-4">
<div className="p-4 bg-slate-50 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="font-semibold">MFA Required</span>
<span className={mfaStatus.requires_mfa ? 'text-green-600' : 'text-gray-600'}>
{mfaStatus.requires_mfa ? 'Yes' : 'No'}
</span>
</div>
<div className="flex items-center justify-between mb-2">
<span className="font-semibold">MFA Enabled</span>
<span className={mfaStatus.mfa_enabled ? 'text-green-600' : 'text-red-600'}>
{mfaStatus.mfa_enabled ? 'Yes' : 'No'}
</span>
</div>
<div className="flex items-center justify-between mb-2">
<span className="font-semibold">MFA Enforced</span>
<span className={mfaStatus.is_enforced ? 'text-green-600' : 'text-red-600'}>
{mfaStatus.is_enforced ? 'Yes' : 'No'}
</span>
</div>
<div className="flex items-center justify-between">
<span className="font-semibold">Backup Codes</span>
<span>{mfaStatus.backup_codes_count} remaining</span>
</div>
</div>
{mfaStatus.enforcement_reason && (
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-yellow-800">
<strong>Note:</strong> {mfaStatus.enforcement_reason}
</p>
</div>
)}
</div>
</div>
)}
</div>
);
};
export default SecurityManagementPage;