import { useState } from 'preact/hooks'; import { Clock3, Pencil, RefreshCw, ShieldCheck, ShieldOff, Trash2 } from 'lucide-preact'; import ConfirmDialog from '@/components/ConfirmDialog'; import LoadingState from '@/components/LoadingState'; import type { AuthorizedDevice } from '@/lib/types'; import { t } from '@/lib/i18n'; interface SecurityDevicesPageProps { devices: AuthorizedDevice[]; loading: boolean; error: string; onRefresh: () => void; onRenameDevice: (device: AuthorizedDevice, name: string) => Promise; onRevokeTrust: (device: AuthorizedDevice) => void; onTrustPermanently: (device: AuthorizedDevice) => void; onRemoveDevice: (device: 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); async function handleSaveDeviceNote(): Promise { if (!editingDevice || savingNote) return; setSavingNote(true); try { await props.onRenameDevice(editingDevice, deviceNote); setEditingDevice(null); setDeviceNote(''); } finally { setSavingNote(false); } } return ( <>

{t('txt_device_management')}

{t('txt_manage_device_sessions_and_30_day_totp_trusted_sessions')}

{t('txt_authorized_devices')}

{!!props.error && (
{props.error}
)} {props.devices.map((device) => ( ))} {props.loading && props.devices.length === 0 && ( )} {!props.loading && props.devices.length === 0 && ( )}
{t('txt_device')} {t('txt_type')} {t('txt_status')} {t('txt_added')} {t('txt_last_seen')} {t('txt_trusted_until')} {t('txt_actions')}
{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(''); }} > ); }