ifixkart-admin/app/(admin)/security/page.tsx

790 lines
35 KiB
TypeScript

'use client';
import { useEffect, useMemo, useRef, useState } from 'react';
import {
Shield,
Monitor,
Smartphone,
Tablet,
Loader2,
AlertTriangle,
Radio,
Flame,
ShieldAlert,
KeyRound,
RefreshCw,
Search,
Globe,
Clock,
X,
Copy,
Check,
ShieldOff,
} from 'lucide-react';
import { adminService, SessionInfo, AuditLogEntry } from '@/services/api/adminService';
import { parseJwt, getAccessToken } from '@/services/api/client';
import { format } from 'date-fns';
import { toast } from 'sonner';
import { TablePagination, useClientPagination } from '@/components/ui/TablePagination';
import { StatsSparklineCard, datesToSparkline, weekOverWeekChange } from '@/components/ui/StatsSparklineCard';
type DeviceFilter = 'all' | 'desktop' | 'mobile' | 'tablet';
type InsightTab = 'sessions' | 'history' | 'failed';
function normalizeDeviceType(type: string): DeviceFilter {
const value = (type || '').toLowerCase();
if (value.includes('mobile') || value.includes('phone')) return 'mobile';
if (value.includes('tablet')) return 'tablet';
return 'desktop';
}
function deviceTone(type: DeviceFilter) {
if (type === 'mobile') {
return {
box: 'bg-success/10 text-success',
bar: 'bg-success',
badge: 'bg-success text-white',
};
}
if (type === 'tablet') {
return {
box: 'bg-warning/10 text-warning',
bar: 'bg-warning',
badge: 'bg-warning text-white',
};
}
return {
box: 'bg-muted text-foreground',
bar: 'bg-muted-foreground',
badge: 'bg-muted text-foreground',
};
}
function remainingLabel(expiresAt: string) {
const ms = new Date(expiresAt).getTime() - Date.now();
if (Number.isNaN(ms) || ms <= 0) return { text: 'Expired', urgent: true };
const mins = Math.floor(ms / 60000);
if (mins < 60) return { text: `${mins}m left`, urgent: true };
const hours = Math.floor(mins / 60);
if (hours < 24) return { text: `${hours}h ${mins % 60}m left`, urgent: hours < 2 };
const days = Math.floor(hours / 24);
if (days < 7) return { text: `${days}d left`, urgent: false };
return { text: 'Active', urgent: false };
}
export default function SecurityPage() {
const [sessions, setSessions] = useState<SessionInfo[]>([]);
const [failedLogins, setFailedLogins] = useState<AuditLogEntry[]>([]);
const [loading, setLoading] = useState(true);
const [revoking, setRevoking] = useState<string | null>(null);
const [mfaCode, setMfaCode] = useState('');
const [actionLoading, setActionLoading] = useState(false);
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
const [deviceFilter, setDeviceFilter] = useState<DeviceFilter>('all');
const [deviceSearch, setDeviceSearch] = useState('');
const [insightTab, setInsightTab] = useState<InsightTab>('sessions');
const [showResetDialog, setShowResetDialog] = useState(false);
const [resetPassword, setResetPassword] = useState('');
const [resetPhrase, setResetPhrase] = useState('');
const [copiedIp, setCopiedIp] = useState<string | null>(null);
const devicesRef = useRef<HTMLDivElement>(null);
const failedRef = useRef<HTMLDivElement>(null);
const pager = useClientPagination(failedLogins);
const fetchData = async () => {
setLoading(true);
try {
const [sessionsData, failedData] = await Promise.allSettled([
adminService.listSessions(0, 100),
adminService.getFailedLogins(20),
]);
if (sessionsData.status === 'fulfilled') setSessions(sessionsData.value.sessions);
if (failedData.status === 'fulfilled') setFailedLogins(failedData.value);
} catch {
toast.error('Failed to load security data');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
const token = getAccessToken();
if (token) {
const payload = parseJwt(token);
if (payload && payload.sub) {
setCurrentUserId(payload.sub as string);
}
}
}, []);
const handleRevoke = async (sessionId: string) => {
setRevoking(sessionId);
try {
await adminService.revokeSession(sessionId);
toast.success('Session ended on this device');
fetchData();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to revoke session';
toast.error(message);
} finally {
setRevoking(null);
}
};
const handleKillSwitch = async (e: React.FormEvent) => {
e.preventDefault();
if (!currentUserId) {
toast.error('Could not determine current admin ID. Please re-authenticate.');
return;
}
if (!mfaCode.trim()) {
toast.error('Please enter your MFA verification code');
return;
}
setActionLoading(true);
try {
const res = await adminService.requestKillSwitch(currentUserId, mfaCode);
toast.success(res.detail || 'Kill switch executed successfully. All users logged out.');
setMfaCode('');
fetchData();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to execute kill switch';
toast.error(message);
} finally {
setActionLoading(false);
}
};
const handleCancelKillSwitch = async () => {
if (!currentUserId) {
toast.error('Could not determine current admin ID. Please re-authenticate.');
return;
}
setActionLoading(true);
try {
const res = await adminService.cancelKillSwitch(currentUserId);
toast.success(res.detail || 'Kill switch cancelled. Access restored.');
fetchData();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to cancel kill switch';
toast.error(message);
} finally {
setActionLoading(false);
}
};
const handleSelfDestruct = async (e: React.FormEvent) => {
e.preventDefault();
if (!currentUserId) {
toast.error('Could not determine current admin ID. Please re-authenticate.');
return;
}
if (!mfaCode.trim()) {
toast.error('Please enter your MFA verification code');
return;
}
setActionLoading(true);
try {
const res = await adminService.selfDestruct(currentUserId, mfaCode);
toast.success(res.detail || 'Self destruct completed. All sessions destroyed.');
setMfaCode('');
fetchData();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to execute self-destruct';
toast.error(message);
} finally {
setActionLoading(false);
}
};
const handleFactoryReset = async () => {
if (!resetPassword.trim()) {
toast.error('Enter your admin password');
return;
}
if (resetPhrase !== 'CONFIRM_FACTORY_RESET_WIPE_2026') {
toast.error('Invalid confirmation phrase');
return;
}
setActionLoading(true);
try {
const token = getAccessToken();
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL || ''}/api/v1/admin/security/factory-reset`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ password: resetPassword, confirmation_phrase: resetPhrase }),
}
);
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: 'Reset failed' }));
throw new Error(err.detail || 'Reset failed');
}
toast.success('Platform factory reset completed. Redirecting to login...');
setShowResetDialog(false);
localStorage.clear();
setTimeout(() => {
window.location.href = '/login';
}, 2000);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Factory reset failed';
toast.error(message);
} finally {
setActionLoading(false);
}
};
const getDeviceIcon = (type: DeviceFilter, className = 'w-5 h-5') => {
if (type === 'mobile') return <Smartphone className={className} />;
if (type === 'tablet') return <Tablet className={className} />;
return <Monitor className={className} />;
};
const activeSessions = sessions.filter((s) => s.is_active);
const boardSessions = insightTab === 'history' ? sessions : activeSessions;
const deviceCounts = useMemo(() => {
const counts: Record<string, number> = { desktop: 0, mobile: 0, tablet: 0 };
for (const session of boardSessions) {
const devType = normalizeDeviceType(session.device_type);
counts[devType] = (counts[devType] || 0) + 1;
}
return counts;
}, [boardSessions]);
const filteredDevices = useMemo(() => {
const q = deviceSearch.trim().toLowerCase();
return boardSessions.filter((session) => {
const kind = normalizeDeviceType(session.device_type);
const matchesType = deviceFilter === 'all' || kind === deviceFilter;
const haystack = [session.browser, session.operating_system, session.device_name, session.ip_address]
.join(' ')
.toLowerCase();
return matchesType && (!q || haystack.includes(q));
});
}, [boardSessions, deviceFilter, deviceSearch]);
const devicePager = useClientPagination(filteredDevices, 6);
const mixTotal = boardSessions.length || 1;
const copyIp = async (ip: string) => {
try {
await navigator.clipboard.writeText(ip);
setCopiedIp(ip);
toast.success('IP address copied');
setTimeout(() => setCopiedIp(null), 1500);
} catch {
toast.error('Could not copy IP address');
}
};
const openInsight = (tab: InsightTab) => {
setInsightTab(tab);
const node = tab === 'failed' ? failedRef.current : devicesRef.current;
node?.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
const dataCardShell =
'crm-radius-section border border-border bg-card shadow-[0_1px_3px_rgba(16,24,40,0.06)] overflow-visible min-w-0';
const labelClass = 'block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5';
const primaryButton =
'inline-flex items-center justify-center gap-2 h-9 min-h-9 px-4 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[13px] font-semibold cursor-pointer disabled:opacity-50 whitespace-nowrap';
const secondaryButton =
'inline-flex items-center justify-center gap-2 h-9 min-h-9 px-4 crm-radius-control border border-border bg-card text-foreground text-[13px] font-medium hover:bg-muted cursor-pointer disabled:opacity-50 whitespace-nowrap';
const inputClass =
'w-full h-9 pl-8 pr-3 crm-radius-control border border-border bg-card text-[13px] text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors';
return (
<div className="flex flex-col gap-5 min-w-0">
<div className="flex flex-wrap items-start justify-between gap-3 min-w-0">
<div className="min-w-0">
<div className="flex items-center gap-2">
<h1 className="text-lg font-semibold text-foreground">Security & Sessions</h1>
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 crm-radius-toggle bg-primary/10 text-primary text-[11px] font-semibold leading-none">
{activeSessions.length}
</span>
</div>
</div>
<button
type="button"
onClick={fetchData}
className="w-8 h-8 crm-radius-control border border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer transition-colors flex items-center justify-center"
title="Reload"
>
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<StatsSparklineCard
title="Active sessions"
value={loading ? '—' : activeSessions.length}
icon={Radio}
tone="green"
series={datesToSparkline(activeSessions.map((s) => s.expires_at))}
deltaPercent={weekOverWeekChange(activeSessions.map((s) => s.expires_at))}
active={insightTab === 'sessions'}
onClick={() => openInsight('sessions')}
/>
<StatsSparklineCard
title="Session history"
value={loading ? '—' : sessions.length}
icon={Monitor}
tone="blue"
series={datesToSparkline(sessions.map((s) => s.expires_at))}
deltaPercent={weekOverWeekChange(sessions.map((s) => s.expires_at))}
active={insightTab === 'history'}
onClick={() => openInsight('history')}
/>
<StatsSparklineCard
title="Failed logins"
value={loading ? '—' : failedLogins.length}
icon={AlertTriangle}
tone="orange"
series={datesToSparkline(failedLogins.map((l) => l.created_at))}
deltaPercent={weekOverWeekChange(failedLogins.map((l) => l.created_at))}
active={insightTab === 'failed'}
onClick={() => openInsight('failed')}
/>
</div>
<div ref={devicesRef} className={dataCardShell}>
<div className="p-4 border-b border-border">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3 min-w-0">
<div className="min-w-0">
<div className="flex items-center gap-2">
<h2 className="text-[16px] font-semibold text-foreground">
{insightTab === 'history' ? 'Session history' : 'Active connected devices'}
</h2>
{insightTab !== 'history' && (
<span className="inline-flex items-center gap-1.5 h-5 px-2 crm-radius-toggle bg-success/10 text-success text-[11px] font-semibold">
<span className="relative flex h-1.5 w-1.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-60" />
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-success" />
</span>
Live {activeSessions.length}
</span>
)}
</div>
<p className="text-[12px] text-muted-foreground mt-1">
{insightTab === 'history'
? 'Every recorded admin session, including devices that have already signed out.'
: 'Devices currently holding an open admin session.'}
</p>
</div>
<div className="relative w-full sm:w-[220px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground pointer-events-none" />
<input
type="text"
placeholder="Search IP, browser, OS"
value={deviceSearch}
onChange={(e) => setDeviceSearch(e.target.value)}
className={inputClass}
/>
</div>
</div>
<div className="mt-4 grid grid-cols-2 xl:grid-cols-4 gap-2">
{([
{ id: 'all' as DeviceFilter, label: 'All devices', count: boardSessions.length, icon: Radio, tone: 'bg-primary/10 text-primary', bar: 'bg-primary' },
{ id: 'desktop' as DeviceFilter, label: 'Desktop', count: deviceCounts.desktop, icon: Monitor, tone: deviceTone('desktop').box, bar: deviceTone('desktop').bar },
{ id: 'mobile' as DeviceFilter, label: 'Mobile', count: deviceCounts.mobile, icon: Smartphone, tone: deviceTone('mobile').box, bar: deviceTone('mobile').bar },
{ id: 'tablet' as DeviceFilter, label: 'Tablet', count: deviceCounts.tablet, icon: Tablet, tone: deviceTone('tablet').box, bar: deviceTone('tablet').bar },
]).map((tile) => {
const selected = deviceFilter === tile.id;
const Icon = tile.icon;
const share = mixTotal ? (tile.count / mixTotal) * 100 : 0;
return (
<button
key={tile.id}
type="button"
onClick={() => setDeviceFilter(tile.id)}
className={`text-left p-3 crm-radius-section border bg-card cursor-pointer transition-colors min-w-0 ${
selected ? 'border-[#b8c0d4]' : 'border-border hover:bg-muted/40'
}`}
>
<div className="flex items-center justify-between gap-2">
<span className={`w-8 h-8 crm-radius-icon flex items-center justify-center shrink-0 ${tile.tone}`}>
<Icon className="w-4 h-4" />
</span>
<span className="text-[20px] font-bold text-foreground leading-none tabular-nums">{tile.count}</span>
</div>
<p className="text-[12px] font-medium text-muted-foreground mt-2">{tile.label}</p>
<div className="mt-2 h-1.5 crm-radius-toggle overflow-hidden bg-muted">
<div className={`h-full ${tile.bar}`} style={{ width: `${share}%` }} />
</div>
</button>
);
})}
</div>
</div>
{loading ? (
<div className="py-16 flex justify-center">
<Loader2 className="w-6 h-6 animate-spin text-primary" />
</div>
) : filteredDevices.length === 0 ? (
<div className="py-16 text-center text-[13px] text-muted-foreground">
{deviceSearch || deviceFilter !== 'all' ? 'No connected devices match your search.' : 'No sessions found.'}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3 p-4">
{devicePager.items.map((session) => {
const kind = normalizeDeviceType(session.device_type);
const tone = deviceTone(kind);
const yours = Boolean(currentUserId && session.user_id === currentUserId);
const remaining = remainingLabel(session.expires_at);
const live = session.is_active;
return (
<article
key={session.session_id}
className={`flex min-w-0 overflow-hidden crm-radius-section border bg-card shadow-[0_1px_3px_rgba(16,24,40,0.06)] ${
yours ? 'border-[#b8c0d4]' : 'border-border'
} ${live ? '' : 'opacity-80'}`}
>
<div className={`w-12 shrink-0 flex flex-col items-center justify-center gap-1.5 py-3 ${live ? tone.box : 'bg-muted text-muted-foreground'}`}>
<div className="relative">
{getDeviceIcon(kind, 'w-5 h-5')}
{live && (
<span className="absolute -top-1 -right-1 flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-60" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-success border-2 border-card" />
</span>
)}
</div>
<span className="text-[9px] font-semibold uppercase tracking-wide leading-none">{kind}</span>
</div>
<div className="flex-1 min-w-0 p-3 flex flex-col overflow-hidden">
<div className="flex items-start justify-between gap-2 min-w-0">
<div className="min-w-0 overflow-hidden">
<p className="text-[13px] font-semibold text-foreground truncate">
{session.browser || 'Unknown browser'}
</p>
<p className="text-[12px] text-muted-foreground truncate">
{session.operating_system || 'Unknown OS'}
{session.device_name ? ` · ${session.device_name}` : ''}
</p>
</div>
<div className="flex items-center gap-1 shrink-0">
{yours && (
<span className="inline-flex items-center px-1.5 py-0.5 crm-radius-badge bg-primary/10 text-primary text-[11px] font-semibold">
You
</span>
)}
<span
className={`inline-flex items-center px-1.5 py-0.5 crm-radius-badge text-[11px] font-semibold ${
live
? remaining.urgent
? 'bg-warning text-white'
: 'bg-success text-white'
: 'bg-muted text-muted-foreground'
}`}
>
{live ? remaining.text : 'Ended'}
</span>
</div>
</div>
<div className="mt-2.5 border-t border-border pt-2 space-y-1.5 min-w-0">
<div className="flex items-center gap-2 min-w-0">
<span className="inline-flex items-center gap-1 text-[11px] text-muted-foreground w-[58px] shrink-0">
<Globe className="w-3 h-3" /> IP
</span>
<span className="min-w-0 flex-1 flex items-center gap-1 overflow-hidden">
<span className="block min-w-0 flex-1 truncate font-mono text-[12px] font-semibold text-foreground" title={session.ip_address}>
{session.ip_address || '—'}
</span>
<button
type="button"
title="Copy IP"
onClick={() => copyIp(session.ip_address)}
className="w-6 h-6 crm-radius-control border border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer flex items-center justify-center shrink-0"
>
{copiedIp === session.ip_address ? <Check className="w-3 h-3 text-success" /> : <Copy className="w-3 h-3" />}
</button>
</span>
</div>
<div className="flex items-center gap-2 min-w-0">
<span className="inline-flex items-center gap-1 text-[11px] text-muted-foreground w-[58px] shrink-0">
<Clock className="w-3 h-3" /> Expires
</span>
<span className="min-w-0 flex-1 truncate text-[12px] font-semibold text-foreground" title={format(new Date(session.expires_at), 'dd MMM yyyy, hh:mm a')}>
{format(new Date(session.expires_at), 'dd MMM, hh:mm a')}
</span>
</div>
</div>
{live ? (
<button
type="button"
onClick={() => handleRevoke(session.session_id)}
disabled={revoking === session.session_id}
className="mt-2.5 inline-flex items-center justify-center gap-1.5 h-8 min-h-8 w-full crm-radius-control border border-border bg-card text-[12px] font-semibold text-destructive hover:bg-destructive/10 cursor-pointer disabled:opacity-50 whitespace-nowrap"
>
{revoking === session.session_id ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<ShieldOff className="w-3.5 h-3.5" />
)}
Revoke
</button>
) : (
<div className="mt-2.5 h-8 min-h-8 w-full crm-radius-control border border-border bg-muted/40 text-[12px] font-medium text-muted-foreground flex items-center justify-center whitespace-nowrap">
Ended
</div>
)}
</div>
</article>
);
})}
</div>
)}
<TablePagination {...devicePager} />
</div>
<div ref={failedRef} className={dataCardShell}>
<div className="px-5 py-4 border-b border-border">
<h2 className="text-[16px] font-semibold text-foreground">Failed login audit</h2>
<p className="text-[12px] text-muted-foreground mt-0.5">Authentication attempts that did not succeed.</p>
</div>
<div className="overflow-x-auto">
<table className="crm-data-table min-w-[800px]">
<thead>
<tr className="border-b border-border bg-gray-50">
<th className="px-5 py-3 text-gray-700">Action</th>
<th className="px-5 py-3 text-gray-700">Entity</th>
<th className="px-5 py-3 text-gray-700">IP address</th>
<th className="px-5 py-3 text-gray-700">User agent</th>
<th className="px-5 py-3 text-gray-700">Time</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={5} className="px-5 py-12 text-center">
<Loader2 className="w-6 h-6 animate-spin text-primary mx-auto" />
</td>
</tr>
) : failedLogins.length === 0 ? (
<tr>
<td colSpan={5} className="px-5 py-12 text-center text-[13px] text-muted-foreground whitespace-normal">
No security failures recorded
</td>
</tr>
) : (
pager.items.map((log) => (
<tr key={log.audit_id} className="border-b border-border hover:bg-muted/10 transition-colors">
<td className="px-5 py-3.5">
<span className="inline-flex items-center px-2.5 py-1 crm-radius-badge bg-destructive text-white text-[11px] font-semibold">
{log.action}
</span>
</td>
<td className="px-5 py-3.5 text-muted-foreground">{log.entity_type}</td>
<td className="px-5 py-3.5 text-muted-foreground font-mono">{log.ip_address}</td>
<td className="px-5 py-3.5 text-muted-foreground">
<span className="crm-cell-clip max-w-[220px]" title={log.user_agent || ''}>
{log.user_agent || '—'}
</span>
</td>
<td className="px-5 py-3.5 text-muted-foreground">
{format(new Date(log.created_at), 'dd MMM yyyy, hh:mm a')}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
<TablePagination {...pager} />
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-4">
<div className={`${dataCardShell} p-4 flex flex-col`}>
<div className="flex items-center gap-2 text-destructive mb-3">
<ShieldAlert className="w-4 h-4 shrink-0" />
<h3 className="text-[14px] font-semibold">Global kill switch</h3>
</div>
<p className="text-[12px] text-muted-foreground mb-4 flex-1">
Disables public client access and ends every active user session.
</p>
<form onSubmit={handleKillSwitch} className="space-y-2">
<div className="relative">
<KeyRound className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" />
<input
type="text"
placeholder="Admin MFA code"
value={mfaCode}
onChange={(e) => setMfaCode(e.target.value)}
className={inputClass}
/>
</div>
<div className="grid grid-cols-1 gap-2 w-full">
<button type="submit" disabled={actionLoading} className={`${primaryButton} w-full bg-destructive hover:bg-destructive/90`}>
{actionLoading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : 'Trigger lockout'}
</button>
<button type="button" onClick={handleCancelKillSwitch} disabled={actionLoading} className={`${secondaryButton} w-full`}>
Restore
</button>
</div>
</form>
</div>
<div className={`${dataCardShell} p-4 flex flex-col`}>
<div className="flex items-center gap-2 text-destructive mb-3">
<Flame className="w-4 h-4 shrink-0" />
<h3 className="text-[14px] font-semibold">Force session termination</h3>
</div>
<p className="text-[12px] text-muted-foreground mb-4 flex-1">
Ends all sessions in the database without changing the lockout setting.
</p>
<form onSubmit={handleSelfDestruct} className="space-y-2">
<div className="relative">
<KeyRound className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" />
<input
type="text"
placeholder="Admin MFA code"
value={mfaCode}
onChange={(e) => setMfaCode(e.target.value)}
className={inputClass}
/>
</div>
<button type="submit" disabled={actionLoading} className={`${primaryButton} w-full bg-destructive hover:bg-destructive/90`}>
{actionLoading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : 'Kill all sessions'}
</button>
</form>
</div>
<div className={`${dataCardShell} p-4 flex flex-col border-destructive/30`}>
<div className="flex items-center gap-2 text-destructive mb-3">
<AlertTriangle className="w-4 h-4 shrink-0" />
<h3 className="text-[14px] font-semibold">Platform factory reset</h3>
</div>
<p className="text-[12px] text-muted-foreground mb-4 flex-1 leading-relaxed">
Wipes core, CRM and commerce tables plus uploads, then reseeds master roles.
</p>
<button
type="button"
onClick={() => {
setResetPassword('');
setResetPhrase('');
setShowResetDialog(true);
}}
disabled={actionLoading}
className={`${primaryButton} w-full bg-destructive hover:bg-destructive/90`}
>
Execute factory reset
</button>
</div>
<div className={`${dataCardShell} p-4 flex flex-col`}>
<div className="flex items-center gap-2 mb-3">
<Shield className="w-4 h-4 text-primary shrink-0" />
<h3 className="text-[14px] font-semibold text-foreground">Infrastructure</h3>
</div>
<div className="space-y-2 text-[12px] flex-1">
<div className="flex justify-between py-1.5 border-b border-border">
<span className="text-muted-foreground">Admin authority</span>
<span className="font-semibold text-success">{currentUserId ? 'Super Admin' : 'Unknown'}</span>
</div>
<div className="flex justify-between py-1.5 border-b border-border">
<span className="text-muted-foreground">Audit log engine</span>
<span className="font-semibold text-foreground">Operational</span>
</div>
<div className="flex justify-between py-1.5 border-b border-border">
<span className="text-muted-foreground">MFA router</span>
<span className="font-semibold text-foreground">SHA256 Standard</span>
</div>
</div>
<button type="button" onClick={fetchData} className={`${secondaryButton} w-full mt-4`}>
Refresh diagnostics
</button>
</div>
</div>
{showResetDialog && (
<div
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 p-4"
onClick={() => !actionLoading && setShowResetDialog(false)}
>
<div
role="dialog"
aria-modal="true"
aria-labelledby="factory-reset-title"
className="bg-card border border-border shadow-2xl w-full max-w-sm overflow-hidden crm-radius-none"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-start justify-between gap-3 px-5 pt-5 pb-2">
<div className="flex items-start gap-3 min-w-0">
<div className="w-10 h-10 rounded-full bg-destructive/10 text-destructive flex items-center justify-center shrink-0">
<AlertTriangle className="w-5 h-5" />
</div>
<div className="min-w-0">
<h3 id="factory-reset-title" className="text-[15px] font-semibold text-foreground">
Factory reset
</h3>
<p className="text-[13px] text-muted-foreground mt-1 leading-relaxed">
This permanently wipes platform data. Type the confirmation phrase to continue.
</p>
</div>
</div>
<button
type="button"
aria-label="Close factory reset dialog"
onClick={() => setShowResetDialog(false)}
disabled={actionLoading}
className="w-8 h-8 rounded-full border border-border text-muted-foreground hover:bg-muted cursor-pointer transition-colors flex items-center justify-center shrink-0 disabled:opacity-50"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="px-5 pb-2 space-y-3">
<div>
<label className={labelClass}>Admin password</label>
<input
type="password"
value={resetPassword}
onChange={(e) => setResetPassword(e.target.value)}
className="w-full h-9 px-3 crm-radius-control border border-border bg-card text-[13px] text-foreground focus:outline-none focus:border-primary"
/>
</div>
<div>
<label className={labelClass}>Confirmation phrase</label>
<input
type="text"
value={resetPhrase}
onChange={(e) => setResetPhrase(e.target.value)}
placeholder="CONFIRM_FACTORY_RESET_WIPE_2026"
className="w-full h-9 px-3 crm-radius-control border border-border bg-card text-[13px] text-foreground focus:outline-none focus:border-primary font-mono"
/>
</div>
</div>
<div className="flex items-center justify-end gap-2 px-5 pb-5 pt-3">
<button type="button" onClick={() => setShowResetDialog(false)} disabled={actionLoading} className={secondaryButton}>
Cancel
</button>
<button
type="button"
onClick={handleFactoryReset}
disabled={actionLoading}
className="inline-flex items-center justify-center gap-2 h-9 min-h-9 px-4 crm-radius-control bg-destructive hover:bg-destructive/90 text-white text-[13px] font-semibold cursor-pointer disabled:opacity-50 whitespace-nowrap"
>
{actionLoading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : 'Execute reset'}
</button>
</div>
</div>
</div>
)}
</div>
);
}