import { useState } from 'preact/hooks'; import { CheckSquare, Clock3, Pencil, RefreshCw, ShieldCheck, ShieldOff, Trash2 } from 'lucide-preact'; import ConfirmDialog from '@/components/ConfirmDialog'; import LoadingState from '@/components/LoadingState'; import PendingAuthRequestsPanel from '@/components/PendingAuthRequestsPanel'; import type { AuthRequest, AuthorizedDevice } from '@/lib/types'; import { t } from '@/lib/i18n'; interface SecurityDevicesPageProps { devices: AuthorizedDevice[]; currentDeviceIdentifier: string; loading: boolean; error: string; pendingAuthRequests: AuthRequest[]; pendingAuthRequestsLoading: boolean; pendingAuthRequestsRefreshing: boolean; onRefresh: () => void; onRefreshPendingAuthRequests: () => Promise; onApproveAuthRequest: (request: AuthRequest) => Promise; onDenyAuthRequest: (request: AuthRequest) => Promise; onRenameDevice: (device: AuthorizedDevice, name: string) => Promise; onRevokeTrust: (device: AuthorizedDevice) => void; onTrustPermanently: (device: AuthorizedDevice) => void; onRemoveDevice: (device: AuthorizedDevice) => void; onRemoveSelectedDevices: (devices: AuthorizedDevice[]) => void; onRevokeAll: () => void; onRemoveAll: () => void; } function formatDateTime(value: string | null | undefined): string { if (!value) return t('txt_dash'); const date = new Date(value); if (Number.isNaN(date.getTime())) return t('txt_dash'); return date.toLocaleString(); } function isPermanentTrust(value: string | null | undefined): boolean { if (!value) return false; const date = new Date(value); return !Number.isNaN(date.getTime()) && date.getUTCFullYear() >= 2099; } function mapDeviceTypeName(type: number): string { switch (type) { case 0: return t('txt_android'); case 1: return t('txt_ios'); case 2: return t('txt_chrome_extension'); case 3: return t('txt_firefox_extension'); case 4: return t('txt_opera_extension'); case 5: return t('txt_edge_extension'); case 6: return t('txt_windows_desktop'); case 7: return t('txt_macos_desktop'); case 8: return t('txt_linux_desktop'); case 9: return t('txt_chrome_browser'); case 10: return t('txt_firefox_browser'); case 11: return t('txt_opera_browser'); case 12: return t('txt_edge_browser'); case 13: return t('txt_ie_browser'); case 14: return t('txt_web'); default: return t('txt_type_type', { type }); } } export default function SecurityDevicesPage(props: SecurityDevicesPageProps) { const [editingDevice, setEditingDevice] = useState(null); const [deviceNote, setDeviceNote] = useState(''); const [savingNote, setSavingNote] = useState(false); const [selectedDeviceIds, setSelectedDeviceIds] = useState([]); const currentDeviceIdentifier = props.currentDeviceIdentifier; const selectableDevices = props.devices.filter((device) => ( device.identifier !== currentDeviceIdentifier )); const selectedDeviceIdSet = new Set(selectedDeviceIds); const selectedDevices = selectableDevices.filter((device) => selectedDeviceIdSet.has(device.identifier)); const allSelectableSelected = selectableDevices.length > 0 && selectedDevices.length === selectableDevices.length; async function handleSaveDeviceNote(): Promise { if (!editingDevice || savingNote) return; setSavingNote(true); try { await props.onRenameDevice(editingDevice, deviceNote); setEditingDevice(null); setDeviceNote(''); } finally { setSavingNote(false); } } function toggleSelectAllDevices(): void { setSelectedDeviceIds(allSelectableSelected ? [] : selectableDevices.map((device) => device.identifier)); } function toggleSelectedDevice(device: AuthorizedDevice): void { if (device.identifier === currentDeviceIdentifier) return; setSelectedDeviceIds((current) => ( current.includes(device.identifier) ? current.filter((id) => id !== device.identifier) : [...current, device.identifier] )); } return ( <>

{t('txt_authorized_devices')}

{t('txt_manage_device_sessions_and_30_day_totp_trusted_sessions')}
{!!props.error && (
{props.error}
)} {props.devices.map((device) => ( ))} {props.loading && props.devices.length === 0 && ( )} {!props.loading && props.devices.length === 0 && ( )}
{t('txt_select')} {t('txt_device')} {t('txt_type')} {t('txt_status')} {t('txt_added')} {t('txt_last_seen')} {t('txt_trusted_until')} {t('txt_actions')}
toggleSelectedDevice(device)} />
{device.name || t('txt_unknown_device')}
{!!device.deviceNote && !!device.systemName && device.systemName !== device.name && (
{device.systemName}
)}
{device.identifier}
{mapDeviceTypeName(device.type)} {device.online ? t('txt_online') : t('txt_offline')} {formatDateTime(device.creationDate)} {formatDateTime(device.lastSeenAt || device.revisionDate)} {device.trusted ? (
{isPermanentTrust(device.trustedUntil) ? t('txt_permanent_trust') : formatDateTime(device.trustedUntil)}
) : ( {t('txt_not_trusted')} )}
{t('txt_no_devices_found')}
void handleSaveDeviceNote()} onCancel={() => { if (savingNote) return; setEditingDevice(null); setDeviceNote(''); }} > ); }