From a94325ac0df212c6306a813c2184120f1c74ffe7 Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:00:19 +0100 Subject: [PATCH 1/4] filter mfa locations --- .../steps/MethodStep/MethodStep.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx index ef6b9f085..1f9b176c5 100644 --- a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx +++ b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx @@ -52,10 +52,14 @@ export const MethodStep = () => { networks, modalSessionID, ], - queryFn: () => + queryFn: () => { + const nonMFAlocations = (networks as Network[]).filter( + (location) => location.location_mfa_mode === 'disabled', + ); getAvailableIp({ - locationId: (networks as Network[])[0].id, - }), + locationId: nonMFAlocations[0].id, + }); + }, enabled: networks !== undefined && Array.isArray(networks) && networks.length > 0, refetchOnMount: true, refetchOnReconnect: true, From d01f9b59681d3766bee424dfd3f6f1d6acfa5e47 Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:29:08 +0100 Subject: [PATCH 2/4] use .at(), trigger query only when there are existing non-mfa locations --- Cargo.lock | 14 +++++++++++--- Cargo.toml | 2 +- .../steps/MethodStep/MethodStep.tsx | 9 ++++++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7854ae81c..171fb3c4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2600,16 +2600,24 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "9.3.1" +version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ "base64 0.22.1", + "ed25519-dalek", + "getrandom 0.2.17", + "hmac", "js-sys", + "p256", + "p384", "pem", - "ring", + "rand 0.8.5", + "rsa", "serde", "serde_json", + "sha2", + "signature", "simple_asn1", ] diff --git a/Cargo.toml b/Cargo.toml index 3fc63ae0c..15e360933 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ humantime = "2.1" # match version used by sqlx ipnetwork = "0.20" jsonwebkey = { version = "0.3", features = ["pkcs-convert"] } -jsonwebtoken = "9.3" +jsonwebtoken = { version = "10.3", features = ["rust_crypto"] } ldap3 = { version = "0.11", default-features = false, features = ["tls"] } lettre = { version = "0.11", features = ["tokio1-native-tls"] } matches = "0.1" diff --git a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx index 1f9b176c5..72b751766 100644 --- a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx +++ b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx @@ -56,11 +56,14 @@ export const MethodStep = () => { const nonMFAlocations = (networks as Network[]).filter( (location) => location.location_mfa_mode === 'disabled', ); - getAvailableIp({ - locationId: nonMFAlocations[0].id, + return getAvailableIp({ + locationId: (nonMFAlocations.at(0) as Network).id, }); }, - enabled: networks !== undefined && Array.isArray(networks) && networks.length > 0, + enabled: + networks !== undefined && + Array.isArray(networks) && + networks.some((n) => n.location_mfa_mode === 'disabled'), refetchOnMount: true, refetchOnReconnect: true, refetchOnWindowFocus: false, From 9814ed4e0182184a053471f6e5c426e6d1182721 Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:58:00 +0100 Subject: [PATCH 3/4] apply suggestions --- .../steps/MethodStep/MethodStep.tsx | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx index 72b751766..e4f4adcb4 100644 --- a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx +++ b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx @@ -1,7 +1,7 @@ import './style.scss'; import { useQuery } from '@tanstack/react-query'; -import { useCallback, useEffect, useId } from 'react'; +import { useCallback, useEffect, useId, useMemo } from 'react'; import { shallow } from 'zustand/shallow'; import { useI18nContext } from '../../../../../../i18n/i18n-react'; @@ -13,6 +13,7 @@ import { } from '../../../../../../shared/defguard-ui/components/Layout/Button/types'; import type { SelectOption } from '../../../../../../shared/defguard-ui/components/Layout/Select/types'; import SvgIconOutsideLink from '../../../../../../shared/defguard-ui/components/svg/IconOutsideLink'; +import { isPresent } from '../../../../../../shared/defguard-ui/utils/isPresent'; import useApi from '../../../../../../shared/hooks/useApi'; import { externalLink } from '../../../../../../shared/links'; import { QueryKeys } from '../../../../../../shared/queries'; @@ -42,6 +43,13 @@ export const MethodStep = () => { refetchOnMount: true, }); + const nonMFALocations = useMemo(() => { + if (isPresent(networks)) { + return networks.filter((network) => network.location_mfa_mode === 'disabled'); + } + return []; + }, [networks]); + const { data: availableIpResponse, refetch: refetchAvailableIp, @@ -53,17 +61,16 @@ export const MethodStep = () => { modalSessionID, ], queryFn: () => { - const nonMFAlocations = (networks as Network[]).filter( - (location) => location.location_mfa_mode === 'disabled', - ); + const firstLocation = nonMFALocations.at(0); + if (!isPresent(firstLocation)) { + throw new Error("Couldn't find non-MFA locations"); + } return getAvailableIp({ - locationId: (nonMFAlocations.at(0) as Network).id, + locationId: firstLocation.id, }); }, enabled: - networks !== undefined && - Array.isArray(networks) && - networks.some((n) => n.location_mfa_mode === 'disabled'), + networks !== undefined && Array.isArray(networks) && nonMFALocations.length > 0, refetchOnMount: true, refetchOnReconnect: true, refetchOnWindowFocus: false, From 067a1e194ce0cdd5ee87559a9fc964133c42c62e Mon Sep 17 00:00:00 2001 From: jakub-tldr <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:06:28 +0100 Subject: [PATCH 4/4] apply suggestions, block "add new" button when there are no non-mfa locations --- web/src/pages/devices/DevicesPage.tsx | 19 ++++++++++++++++++- .../steps/MethodStep/MethodStep.tsx | 9 ++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/web/src/pages/devices/DevicesPage.tsx b/web/src/pages/devices/DevicesPage.tsx index f6e93fad3..79acfcf8d 100644 --- a/web/src/pages/devices/DevicesPage.tsx +++ b/web/src/pages/devices/DevicesPage.tsx @@ -1,7 +1,7 @@ import './style.scss'; import { useQuery } from '@tanstack/react-query'; -import { type PropsWithChildren, useEffect } from 'react'; +import { type PropsWithChildren, useEffect, useMemo } from 'react'; import { useI18nContext } from '../../i18n/i18n-react'; import { ManagementPageLayout } from '../../shared/components/Layout/ManagementPageLayout/ManagementPageLayout'; @@ -10,6 +10,7 @@ import { ButtonSize, ButtonStyleVariant, } from '../../shared/defguard-ui/components/Layout/Button/types'; +import { isPresent } from '../../shared/defguard-ui/utils/isPresent'; import { useAuthStore } from '../../shared/hooks/store/useAuthStore'; import useApi from '../../shared/hooks/useApi'; import { QueryKeys } from '../../shared/queries'; @@ -42,6 +43,21 @@ const PageContext = (props: PropsWithChildren) => { }; const PageActions = () => { + const { + network: { getNetworks }, + } = useApi(); + const { data: networks } = useQuery({ + queryKey: [QueryKeys.FETCH_NETWORKS], + queryFn: getNetworks, + refetchOnWindowFocus: false, + refetchOnMount: true, + }); + const nonMFALocations = useMemo(() => { + if (isPresent(networks)) { + return networks.filter((network) => network.location_mfa_mode === 'disabled'); + } + return []; + }, [networks]); const { LL } = useI18nContext(); const localLL = LL.devicesPage.bar.actions; const openStandaloneDeviceModal = useAddStandaloneDeviceModal((s) => s.open); @@ -50,6 +66,7 @@ const PageActions = () => { size={ButtonSize.SMALL} styleVariant={ButtonStyleVariant.PRIMARY} text={localLL.addNewDevice()} + disabled={nonMFALocations.length === 0} icon={} onClick={() => openStandaloneDeviceModal()} /> diff --git a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx index e4f4adcb4..dd4d46f78 100644 --- a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx +++ b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx @@ -17,7 +17,6 @@ import { isPresent } from '../../../../../../shared/defguard-ui/utils/isPresent' import useApi from '../../../../../../shared/hooks/useApi'; import { externalLink } from '../../../../../../shared/links'; import { QueryKeys } from '../../../../../../shared/queries'; -import type { Network } from '../../../../../../shared/types'; import { DeviceSetupMethodCard } from '../../../../../addDevice/steps/AddDeviceSetupMethodStep/components/DeviceSetupMethodCard/DeviceSetupMethodCard'; import { useAddStandaloneDeviceModal } from '../../store'; import { @@ -57,20 +56,20 @@ export const MethodStep = () => { } = useQuery({ queryKey: [ 'ADD_STANDALONE_DEVICE_MODAL_FETCH_INITIAL_AVAILABLE_IP', - networks, + nonMFALocations, modalSessionID, ], queryFn: () => { const firstLocation = nonMFALocations.at(0); if (!isPresent(firstLocation)) { - throw new Error("Couldn't find non-MFA locations"); + console.error("Couldn't find non-MFA locations"); + return; } return getAvailableIp({ locationId: firstLocation.id, }); }, - enabled: - networks !== undefined && Array.isArray(networks) && nonMFALocations.length > 0, + enabled: nonMFALocations.length > 0, refetchOnMount: true, refetchOnReconnect: true, refetchOnWindowFocus: false,