diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 2392e62..6e052ec 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -1959,6 +1959,7 @@ export default function App() { lockTimeoutMinutes, sessionTimeoutAction, authorizedDevices: authorizedDevicesQuery.data || [], + currentDeviceIdentifier: getCurrentDeviceIdentifier(), authorizedDevicesLoading: authorizedDevicesQuery.isFetching, authorizedDevicesError: authorizedDevicesQuery.isError && !authorizedDevicesQuery.data ? t('txt_load_devices_failed') : '', domainRules: IS_DEMO_MODE ? demoDomainRules : domainRulesQuery.data || null, @@ -2031,6 +2032,7 @@ export default function App() { onRevokeDeviceTrust: accountSecurityActions.openRevokeDeviceTrust, onTrustDevicePermanently: accountSecurityActions.openTrustDevicePermanently, onRemoveDevice: accountSecurityActions.openRemoveDevice, + onRemoveSelectedDevices: accountSecurityActions.openRemoveSelectedDevices, onRevokeAllDeviceTrust: accountSecurityActions.openRevokeAllDeviceTrust, onRemoveAllDevices: accountSecurityActions.openRemoveAllDevices, onRefreshAdmin: adminActions.refreshAdmin, diff --git a/webapp/src/components/AppMainRoutes.tsx b/webapp/src/components/AppMainRoutes.tsx index 4c95a69..74bbc4f 100644 --- a/webapp/src/components/AppMainRoutes.tsx +++ b/webapp/src/components/AppMainRoutes.tsx @@ -57,6 +57,7 @@ export interface AppMainRoutesProps { lockTimeoutMinutes: 0 | 1 | 5 | 15 | 30; sessionTimeoutAction: 'lock' | 'logout'; authorizedDevices: AuthorizedDevice[]; + currentDeviceIdentifier: string; authorizedDevicesLoading: boolean; authorizedDevicesError: string; domainRules: DomainRules | null; @@ -130,6 +131,7 @@ export interface AppMainRoutesProps { onRevokeDeviceTrust: (device: AuthorizedDevice) => void; onTrustDevicePermanently: (device: AuthorizedDevice) => void; onRemoveDevice: (device: AuthorizedDevice) => void; + onRemoveSelectedDevices: (devices: AuthorizedDevice[]) => void; onRevokeAllDeviceTrust: () => void; onRemoveAllDevices: () => void; onCreateInvite: (hours: number) => Promise; @@ -347,6 +349,7 @@ export default function AppMainRoutes(props: AppMainRoutesProps) { }> diff --git a/webapp/src/components/SecurityDevicesPage.tsx b/webapp/src/components/SecurityDevicesPage.tsx index d0916e5..583bdc4 100644 --- a/webapp/src/components/SecurityDevicesPage.tsx +++ b/webapp/src/components/SecurityDevicesPage.tsx @@ -1,5 +1,5 @@ import { useState } from 'preact/hooks'; -import { Clock3, Pencil, RefreshCw, ShieldCheck, ShieldOff, Trash2 } from 'lucide-preact'; +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'; @@ -8,6 +8,7 @@ import { t } from '@/lib/i18n'; interface SecurityDevicesPageProps { devices: AuthorizedDevice[]; + currentDeviceIdentifier: string; loading: boolean; error: string; pendingAuthRequests: AuthRequest[]; @@ -20,6 +21,7 @@ interface SecurityDevicesPageProps { onRevokeTrust: (device: AuthorizedDevice) => void; onTrustPermanently: (device: AuthorizedDevice) => void; onRemoveDevice: (device: AuthorizedDevice) => void; + onRemoveSelectedDevices: (devices: AuthorizedDevice[]) => void; onRevokeAll: () => void; onRemoveAll: () => void; } @@ -62,6 +64,14 @@ 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; @@ -75,6 +85,19 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) { } } + 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 ( <>
@@ -101,6 +124,27 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) { {t('txt_refresh')} + +