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/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 ef6b9f085..dd4d46f78 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,10 +13,10 @@ 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'; -import type { Network } from '../../../../../../shared/types'; import { DeviceSetupMethodCard } from '../../../../../addDevice/steps/AddDeviceSetupMethodStep/components/DeviceSetupMethodCard/DeviceSetupMethodCard'; import { useAddStandaloneDeviceModal } from '../../store'; import { @@ -42,6 +42,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, @@ -49,14 +56,20 @@ export const MethodStep = () => { } = useQuery({ queryKey: [ 'ADD_STANDALONE_DEVICE_MODAL_FETCH_INITIAL_AVAILABLE_IP', - networks, + nonMFALocations, modalSessionID, ], - queryFn: () => - getAvailableIp({ - locationId: (networks as Network[])[0].id, - }), - enabled: networks !== undefined && Array.isArray(networks) && networks.length > 0, + queryFn: () => { + const firstLocation = nonMFALocations.at(0); + if (!isPresent(firstLocation)) { + console.error("Couldn't find non-MFA locations"); + return; + } + return getAvailableIp({ + locationId: firstLocation.id, + }); + }, + enabled: nonMFALocations.length > 0, refetchOnMount: true, refetchOnReconnect: true, refetchOnWindowFocus: false,