282 lines
12 KiB
TypeScript
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;
|
|
|