From a82a72bcdc704f183493fe7fdb55c8ca6b72ceec Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 21 May 2026 09:47:34 +0200 Subject: [PATCH 01/14] handle posture check in new UI MFA flow --- .../src/shared/components/LocationCard/hooks/useMfaConnect.ts | 4 ++++ new-ui/src/shared/rust-api/api.ts | 4 ++++ new-ui/src/shared/rust-api/types.ts | 2 ++ 3 files changed, 10 insertions(+) diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts index 60a2e730..82c72c21 100644 --- a/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts +++ b/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts @@ -73,6 +73,9 @@ export const useMfaConnect = (method: 0 | 1) => { return; } + const posture_data = location.posture_check_required + ? await api.getPostureData() + : undefined; try { const res = await fetch(`${instance.proxy_url}${MFA_ENDPOINT}/start`, { method: 'POST', @@ -84,6 +87,7 @@ export const useMfaConnect = (method: 0 | 1) => { method, pubkey: instance.pubkey, location_id: location.network_id, + posture_data, }), }); diff --git a/new-ui/src/shared/rust-api/api.ts b/new-ui/src/shared/rust-api/api.ts index 4628ab90..b21b052f 100644 --- a/new-ui/src/shared/rust-api/api.ts +++ b/new-ui/src/shared/rust-api/api.ts @@ -126,6 +126,9 @@ const getEdgeRequestHeaders = async (): Promise => { }; }; +const getPostureData = async (): Promise => + invoke(TauriCommand.GetPostureData); + const swapToOldUi = async () => invoke(TauriCommand.SwapToOldUi); export const api = { @@ -166,6 +169,7 @@ export const api = { stopGlobalLogWatcher, getAllActiveConnections, disconnectLocations, + getPostureData, // Window swapToOldUi, }; diff --git a/new-ui/src/shared/rust-api/types.ts b/new-ui/src/shared/rust-api/types.ts index f82a6716..b61c1239 100644 --- a/new-ui/src/shared/rust-api/types.ts +++ b/new-ui/src/shared/rust-api/types.ts @@ -102,6 +102,7 @@ export const TauriCommand = { StopGlobalLogWatcher: 'stop_global_logwatcher', AllActiveConnections: 'all_active_connections', DisconnectLocations: 'disconnect_locations', + GetPostureData: 'get_posture_data', //Window SwapToOldUi: 'swap_to_old_ui', } as const; @@ -193,6 +194,7 @@ export type LocationInfo = { network_id: number; location_mfa_mode: LocationMfaMode; mfa_method?: MfaMethodValue; + posture_check_required: boolean; }; export type LocationStats = { From 3f810fc119a9fee3ce0ff629a93c4df7b15c79ea Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 21 May 2026 10:07:07 +0200 Subject: [PATCH 02/14] fix formatting --- new-ui/src/shared/rust-api/api.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/new-ui/src/shared/rust-api/api.ts b/new-ui/src/shared/rust-api/api.ts index b21b052f..f7186a00 100644 --- a/new-ui/src/shared/rust-api/api.ts +++ b/new-ui/src/shared/rust-api/api.ts @@ -126,8 +126,7 @@ const getEdgeRequestHeaders = async (): Promise => { }; }; -const getPostureData = async (): Promise => - invoke(TauriCommand.GetPostureData); +const getPostureData = async (): Promise => invoke(TauriCommand.GetPostureData); const swapToOldUi = async () => invoke(TauriCommand.SwapToOldUi); From c561f9ac0633a888da92f007ec179ac3ab692897 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 21 May 2026 10:28:36 +0200 Subject: [PATCH 03/14] add posture data to all MFA connection requests --- .../components/LocationCard/hooks/useMfaConnect.ts | 14 +++++++++++--- .../LocationCard/hooks/useMfaMobileConnect.ts | 12 ++++++++++++ .../LocationCard/hooks/useMfaOidcConnect.ts | 12 ++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts index 82c72c21..8436f03a 100644 --- a/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts +++ b/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts @@ -73,9 +73,17 @@ export const useMfaConnect = (method: 0 | 1) => { return; } - const posture_data = location.posture_check_required - ? await api.getPostureData() - : undefined; + let posture_data: unknown; + try { + posture_data = location.posture_check_required + ? await api.getPostureData() + : undefined; + } catch { + setStartError('Failed to load posture data'); + setIsStarting(false); + return; + } + try { const res = await fetch(`${instance.proxy_url}${MFA_ENDPOINT}/start`, { method: 'POST', diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts index 5923f631..4c7e5dbf 100644 --- a/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts +++ b/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts @@ -153,6 +153,17 @@ export const useMfaMobileConnect = () => { return; } + let posture_data: unknown; + try { + posture_data = location.posture_check_required + ? await api.getPostureData() + : undefined; + } catch { + setStartError('Failed to load posture data'); + setIsStarting(false); + return; + } + try { const res = await fetch(`${instance.proxy_url}${MFA_ENDPOINT}/start`, { method: 'POST', @@ -161,6 +172,7 @@ export const useMfaMobileConnect = () => { method: 4, pubkey: instance.pubkey, location_id: location.network_id, + posture_data, }), }); diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts index 8b6e8a35..9c09fae6 100644 --- a/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts +++ b/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts @@ -137,6 +137,17 @@ export const useMfaOidcConnect = () => { return; } + let posture_data: unknown; + try { + posture_data = location.posture_check_required + ? await api.getPostureData() + : undefined; + } catch { + setStartError('Failed to load posture data'); + setIsStarting(false); + return; + } + try { const res = await fetch(`${instance.proxy_url}${MFA_ENDPOINT}/start`, { method: 'POST', @@ -145,6 +156,7 @@ export const useMfaOidcConnect = () => { method: 2, pubkey: instance.pubkey, location_id: location.network_id, + posture_data, }), }); From 118cf8ad45faf15fb1c7ad437d07e64d58932a8e Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 21 May 2026 10:54:35 +0200 Subject: [PATCH 04/14] startClientMfaSession helper, deduplicate mfa flow code --- .../LocationCard/api/startClientMfaSession.ts | 91 +++++++++++++++++++ .../LocationCard/hooks/useMfaConnect.ts | 77 ++++------------ .../LocationCard/hooks/useMfaMobileConnect.ts | 65 +++---------- .../LocationCard/hooks/useMfaOidcConnect.ts | 54 +++-------- 4 files changed, 132 insertions(+), 155 deletions(-) create mode 100644 new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts diff --git a/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts b/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts new file mode 100644 index 00000000..542fdc3c --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts @@ -0,0 +1,91 @@ +import { fetch } from '@tauri-apps/plugin-http'; +import { api } from '../../../rust-api/api'; +import type { + EdgeRequestHeaders, + InstanceInfo, + LocationInfo, +} from '../../../rust-api/types'; + +export const CLIENT_MFA_ENDPOINT = 'api/v1/client-mfa'; + +export class MfaStartError extends Error { + constructor(message: string) { + super(message); + this.name = 'MfaStartError'; + } +} + +export type MfaStartMethod = 0 | 1 | 2 | 4; + +export type MfaStartResponse = { + token: string; + challenge?: string; +}; + +type MfaStartErrorResponse = { + error?: string; +}; + +type StartClientMfaSessionParams = { + instance: InstanceInfo; + location: LocationInfo; + method: MfaStartMethod; +}; + +type StartClientMfaSessionResult = { + response: MfaStartResponse; + headers: EdgeRequestHeaders; +}; + +export const startClientMfaSession = async ({ + instance, + location, + method, +}: StartClientMfaSessionParams): Promise => { + let headers: EdgeRequestHeaders; + try { + headers = await api.getEdgeRequestHeaders(); + } catch { + throw new MfaStartError('Failed to load request headers'); + } + + let posture_data: unknown; + try { + posture_data = location.posture_check_required + ? await api.getPostureData() + : undefined; + } catch { + throw new MfaStartError('Failed to load posture data'); + } + + try { + const response = await fetch(`${instance.proxy_url}${CLIENT_MFA_ENDPOINT}/start`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + body: JSON.stringify({ + method, + pubkey: instance.pubkey, + location_id: location.network_id, + posture_data, + }), + }); + + if (!response.ok) { + const data = (await response.json()) as MfaStartErrorResponse; + throw new MfaStartError(data.error ?? 'Failed to start MFA'); + } + + return { + response: (await response.json()) as MfaStartResponse, + headers, + }; + } catch (err) { + if (err instanceof MfaStartError) { + throw err; + } + throw new MfaStartError('Failed to reach server'); + } +}; diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts index 8436f03a..46e7d4f8 100644 --- a/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts +++ b/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts @@ -3,21 +3,12 @@ import { fetch } from '@tauri-apps/plugin-http'; import { error } from '@tauri-apps/plugin-log'; import { useCallback, useEffect, useRef, useState } from 'react'; import { api } from '../../../rust-api/api'; -import { - getInstancesQueryOptions, - getPlatformHeaderQueryOptions, -} from '../../../rust-api/query'; +import { getInstancesQueryOptions } from '../../../rust-api/query'; import type { EdgeRequestHeaders } from '../../../rust-api/types'; +import { CLIENT_MFA_ENDPOINT, startClientMfaSession } from '../api/startClientMfaSession'; import { useLocationCardContext } from '../context/context'; import { LocationCardViews } from '../context/types'; -const MFA_ENDPOINT = 'api/v1/client-mfa'; - -type MfaStartResponse = { - token: string; - challenge?: string; -}; - type MfaFinishResponse = { preshared_key: string; }; @@ -37,7 +28,6 @@ export const useMfaConnect = (method: 0 | 1) => { const [requestHeaders, setRequestHeaders] = useState(null); const { data: instances } = useQuery(getInstancesQueryOptions); - const { data: platformHeader } = useQuery(getPlatformHeaderQueryOptions); const instance = instances?.find((i) => i.id === location.instance_id); @@ -52,71 +42,36 @@ export const useMfaConnect = (method: 0 | 1) => { }, }); - // Fire the /start request exactly once when instance + platformHeader are ready. + // Fire the /start request exactly once when instance data is ready. const startCalled = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: intentional one-shot trigger via startCalled ref useEffect(() => { - if (!instance || !platformHeader || startCalled.current) return; + if (!instance || startCalled.current) return; startCalled.current = true; setIsStarting(true); (async () => { - let headers: EdgeRequestHeaders; try { - headers = await api.getEdgeRequestHeaders(); - setRequestHeaders(headers); - } catch { - setStartError('Failed to load request headers'); - setIsStarting(false); - return; - } - - let posture_data: unknown; - try { - posture_data = location.posture_check_required - ? await api.getPostureData() - : undefined; - } catch { - setStartError('Failed to load posture data'); - setIsStarting(false); - return; - } - - try { - const res = await fetch(`${instance.proxy_url}${MFA_ENDPOINT}/start`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...headers, - }, - body: JSON.stringify({ - method, - pubkey: instance.pubkey, - location_id: location.network_id, - posture_data, - }), + const { response, headers } = await startClientMfaSession({ + instance, + location, + method, }); - - if (res.ok) { - const data = (await res.json()) as MfaStartResponse; - setToken(data.token); - } else { - const data = (await res.json()) as MfaErrorResponse; - setStartError(data.error ?? 'Failed to start MFA'); - } - } catch { - setStartError('Failed to reach server'); + setRequestHeaders(headers); + setToken(response.token); + } catch (err) { + setStartError(err instanceof Error ? err.message : 'Failed to start MFA'); } finally { setIsStarting(false); } })(); - }, [instance, platformHeader]); + }, [instance]); const verifyCode = useCallback( async (code: string) => { - if (!token || !instance || !platformHeader || !requestHeaders) return; + if (!token || !instance || !requestHeaders) return; setIsVerifying(true); setVerifyError(null); @@ -124,7 +79,7 @@ export const useMfaConnect = (method: 0 | 1) => { const body = JSON.stringify({ token, code }); try { - const res = await fetch(`${instance.proxy_url}${MFA_ENDPOINT}/finish`, { + const res = await fetch(`${instance.proxy_url}${CLIENT_MFA_ENDPOINT}/finish`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -160,7 +115,7 @@ export const useMfaConnect = (method: 0 | 1) => { setIsVerifying(false); } }, - [token, instance, platformHeader, requestHeaders, location, connectMutate, setView], + [token, instance, requestHeaders, location, connectMutate, setView], ); return { token, isStarting, startError, verifyCode, isVerifying, verifyError }; diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts index 4c7e5dbf..4acd490f 100644 --- a/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts +++ b/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts @@ -1,23 +1,12 @@ import { encode } from '@stablelib/base64'; import { useMutation } from '@tanstack/react-query'; -import { fetch } from '@tauri-apps/plugin-http'; import { error } from '@tauri-apps/plugin-log'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { api } from '../../../rust-api/api'; +import { CLIENT_MFA_ENDPOINT, startClientMfaSession } from '../api/startClientMfaSession'; import { useLocationCardContext } from '../context/context'; import { LocationCardViews } from '../context/types'; -const MFA_ENDPOINT = 'api/v1/client-mfa'; - -type MfaStartResponse = { - token: string; - challenge: string; -}; - -type MfaErrorResponse = { - error: string; -}; - type TokenData = { token: string; challenge: string; @@ -55,7 +44,7 @@ export const useMfaMobileConnect = () => { .replace( /^https:/, 'wss:', - )}${MFA_ENDPOINT}/remote?token=${encodeURIComponent(tokenData.token)}`; + )}${CLIENT_MFA_ENDPOINT}/remote?token=${encodeURIComponent(tokenData.token)}`; expectedCloseRef.current = false; const ws = new WebSocket(wsUrl); @@ -144,48 +133,22 @@ export const useMfaMobileConnect = () => { // Clear previous token → triggers WS cleanup via effect setTokenData(null); - let headers: Record; try { - headers = await api.getEdgeRequestHeaders(); - } catch { - setStartError('Failed to load request headers'); - setIsStarting(false); - return; - } - - let posture_data: unknown; - try { - posture_data = location.posture_check_required - ? await api.getPostureData() - : undefined; - } catch { - setStartError('Failed to load posture data'); - setIsStarting(false); - return; - } - - try { - const res = await fetch(`${instance.proxy_url}${MFA_ENDPOINT}/start`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...headers }, - body: JSON.stringify({ - method: 4, - pubkey: instance.pubkey, - location_id: location.network_id, - posture_data, - }), + const { response } = await startClientMfaSession({ + instance, + location, + method: 4, }); - - if (res.ok) { - const data = (await res.json()) as MfaStartResponse; - setTokenData({ token: data.token, challenge: data.challenge }); - } else { - const data = (await res.json()) as MfaErrorResponse; - setStartError(data.error ?? 'Failed to start mobile authentication'); - error(`Mobile MFA start failed for location ${location.id}: ${data.error}`); + if (!response.challenge) { + setStartError('Unsupported response from proxy'); + return; } + + setTokenData({ token: response.token, challenge: response.challenge }); } catch (e) { - setStartError('Failed to reach server'); + setStartError( + e instanceof Error ? e.message : 'Failed to start mobile authentication', + ); error(`Mobile MFA start network error for location ${location.id}: ${e}`); } finally { setIsStarting(false); diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts index 9c09fae6..03a8c90f 100644 --- a/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts +++ b/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts @@ -4,14 +4,13 @@ import { error } from '@tauri-apps/plugin-log'; import { useCallback, useEffect, useRef, useState } from 'react'; import { api } from '../../../rust-api/api'; import { getInstancesQueryOptions } from '../../../rust-api/query'; +import { CLIENT_MFA_ENDPOINT, startClientMfaSession } from '../api/startClientMfaSession'; import { useLocationCardContext } from '../context/context'; import { LocationCardViews } from '../context/types'; -const MFA_ENDPOINT = 'api/v1/client-mfa'; const POLL_INTERVAL_MS = 5_000; const POLL_TIMEOUT_MS = 5 * 60 * 1_000; // 5 minutes -type MfaStartResponse = { token: string }; type MfaFinishResponse = { preshared_key: string }; type MfaErrorResponse = { error: string }; @@ -65,7 +64,7 @@ export const useMfaOidcConnect = () => { const poll = async () => { try { - const res = await fetch(`${proxyUrl}${MFA_ENDPOINT}/finish`, { + const res = await fetch(`${proxyUrl}${CLIENT_MFA_ENDPOINT}/finish`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify({ token }), @@ -128,49 +127,18 @@ export const useMfaOidcConnect = () => { setPollError(null); stopPolling(); - let headers: Record; try { - headers = await api.getEdgeRequestHeaders(); - } catch { - setStartError('Failed to load request headers'); - setIsStarting(false); - return; - } - - let posture_data: unknown; - try { - posture_data = location.posture_check_required - ? await api.getPostureData() - : undefined; - } catch { - setStartError('Failed to load posture data'); - setIsStarting(false); - return; - } - - try { - const res = await fetch(`${instance.proxy_url}${MFA_ENDPOINT}/start`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...headers }, - body: JSON.stringify({ - method: 2, - pubkey: instance.pubkey, - location_id: location.network_id, - posture_data, - }), + const { response, headers } = await startClientMfaSession({ + instance, + location, + method: 2, }); - - if (res.ok) { - const data = (await res.json()) as MfaStartResponse; - await api.openLink(`${instance.proxy_url}openid/mfa?token=${data.token}`); - startPolling(data.token, instance.proxy_url, headers); - } else { - const data = (await res.json()) as MfaErrorResponse; - setStartError(data.error ?? 'Failed to start OIDC authentication'); - error(`OIDC MFA start failed for location ${location.id}: ${data.error}`); - } + await api.openLink(`${instance.proxy_url}openid/mfa?token=${response.token}`); + startPolling(response.token, instance.proxy_url, headers); } catch (e) { - setStartError('Failed to reach server'); + setStartError( + e instanceof Error ? e.message : 'Failed to start OIDC authentication', + ); error(`OIDC MFA start network error for location ${location.id}: ${e}`); } finally { setIsStarting(false); From 86483546f1a0d428e9d06a4d107e42b411afd975 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 21 May 2026 11:41:37 +0200 Subject: [PATCH 05/14] display posture check errors --- .../components/LocationCard/LocationCard.tsx | 3 +- .../LocationCard/api/startClientMfaSession.ts | 18 +++++- .../LocationCard/context/context.tsx | 5 ++ .../LocationCard/hooks/useMfaConnect.ts | 13 ++++- .../LocationCard/hooks/useMfaMobileConnect.ts | 15 ++++- .../LocationCard/hooks/useMfaOidcConnect.ts | 15 ++++- .../LocationCardPostureCheckFailView.tsx | 56 +++++++++++++++++++ .../style.scss | 7 +++ 8 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/LocationCardPostureCheckFailView.tsx create mode 100644 new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/style.scss diff --git a/new-ui/src/shared/components/LocationCard/LocationCard.tsx b/new-ui/src/shared/components/LocationCard/LocationCard.tsx index 72b6cbc9..ff74c328 100644 --- a/new-ui/src/shared/components/LocationCard/LocationCard.tsx +++ b/new-ui/src/shared/components/LocationCard/LocationCard.tsx @@ -21,6 +21,7 @@ import { LocationCardMfaMobileView } from './views/LocationCardMfaMobileView/Loc import { LocationCardMfaOidcView } from './views/LocationCardMfaOidcView/LocationCardMfaOidcView'; import { LocationCardMfaSettings } from './views/LocationCardMfaSettings/LocationCardMfaSettings'; import { LocationCardMfaTotpView } from './views/LocationCardMfaTotpView/LocationCardMfaTotpView'; +import { LocationCardPostureCheckFailView } from './views/LocationCardPostureCheckFailView/LocationCardPostureCheckFailView'; interface Props { location: LocationInfo; @@ -39,7 +40,7 @@ const views: Record = { [LocationCardViews.MfaSettings]: , [LocationCardViews.Connecting]: null, [LocationCardViews.Connected]: , - [LocationCardViews.PostureCheckFail]: null, + [LocationCardViews.PostureCheckFail]: , }; interface InnerProps { diff --git a/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts b/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts index 542fdc3c..5a3a2f57 100644 --- a/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts +++ b/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts @@ -9,9 +9,12 @@ import type { export const CLIENT_MFA_ENDPOINT = 'api/v1/client-mfa'; export class MfaStartError extends Error { - constructor(message: string) { + public readonly status?: number; + + constructor(message: string, status?: number) { super(message); this.name = 'MfaStartError'; + this.status = status; } } @@ -26,6 +29,9 @@ type MfaStartErrorResponse = { error?: string; }; +export const shouldShowPostureError = (err: unknown, location: LocationInfo): boolean => + err instanceof MfaStartError && err.status === 403 && location.posture_check_required; + type StartClientMfaSessionParams = { instance: InstanceInfo; location: LocationInfo; @@ -74,8 +80,14 @@ export const startClientMfaSession = async ({ }); if (!response.ok) { - const data = (await response.json()) as MfaStartErrorResponse; - throw new MfaStartError(data.error ?? 'Failed to start MFA'); + let message = 'Failed to start MFA'; + try { + const data = (await response.json()) as MfaStartErrorResponse; + message = data.error ?? message; + } catch { + // Keep the response status even if the proxy sends a malformed error body. + } + throw new MfaStartError(message, response.status); } return { diff --git a/new-ui/src/shared/components/LocationCard/context/context.tsx b/new-ui/src/shared/components/LocationCard/context/context.tsx index b2e7e8b4..cc9887fb 100644 --- a/new-ui/src/shared/components/LocationCard/context/context.tsx +++ b/new-ui/src/shared/components/LocationCard/context/context.tsx @@ -8,7 +8,9 @@ interface LocationCardContextValue { instance: InstanceInfo; currentView: LocationCardViewsValue; previousView: LocationCardViewsValue | null; + postureError: string | null; setView: (view: LocationCardViewsValue) => void; + setPostureError: (error: string | null) => void; startMfa: () => void; } @@ -34,6 +36,7 @@ export const LocationCardProvider = ({ children, }: LocationCardProviderProps) => { const [previousView, setPreviousView] = useState(null); + const [postureError, setPostureError] = useState(null); const [currentView, setCurrentView] = useState( location.active ? LocationCardViews.Connected : LocationCardViews.Default, ); @@ -68,7 +71,9 @@ export const LocationCardProvider = ({ value={{ currentView, previousView, + postureError, setView, + setPostureError, location, instance, startMfa, diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts index 46e7d4f8..83c1a20c 100644 --- a/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts +++ b/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts @@ -5,7 +5,11 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { api } from '../../../rust-api/api'; import { getInstancesQueryOptions } from '../../../rust-api/query'; import type { EdgeRequestHeaders } from '../../../rust-api/types'; -import { CLIENT_MFA_ENDPOINT, startClientMfaSession } from '../api/startClientMfaSession'; +import { + CLIENT_MFA_ENDPOINT, + shouldShowPostureError, + startClientMfaSession, +} from '../api/startClientMfaSession'; import { useLocationCardContext } from '../context/context'; import { LocationCardViews } from '../context/types'; @@ -18,7 +22,7 @@ type MfaErrorResponse = { }; export const useMfaConnect = (method: 0 | 1) => { - const { location, setView } = useLocationCardContext(); + const { location, setPostureError, setView } = useLocationCardContext(); const [token, setToken] = useState(null); const [isStarting, setIsStarting] = useState(false); @@ -62,6 +66,11 @@ export const useMfaConnect = (method: 0 | 1) => { setRequestHeaders(headers); setToken(response.token); } catch (err) { + if (shouldShowPostureError(err, location)) { + setPostureError(err.message); + setView(LocationCardViews.PostureCheckFail); + return; + } setStartError(err instanceof Error ? err.message : 'Failed to start MFA'); } finally { setIsStarting(false); diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts index 4acd490f..af930a05 100644 --- a/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts +++ b/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts @@ -3,7 +3,11 @@ import { useMutation } from '@tanstack/react-query'; import { error } from '@tauri-apps/plugin-log'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { api } from '../../../rust-api/api'; -import { CLIENT_MFA_ENDPOINT, startClientMfaSession } from '../api/startClientMfaSession'; +import { + CLIENT_MFA_ENDPOINT, + shouldShowPostureError, + startClientMfaSession, +} from '../api/startClientMfaSession'; import { useLocationCardContext } from '../context/context'; import { LocationCardViews } from '../context/types'; @@ -13,7 +17,7 @@ type TokenData = { }; export const useMfaMobileConnect = () => { - const { location, instance, setView } = useLocationCardContext(); + const { location, instance, setPostureError, setView } = useLocationCardContext(); const [isStarting, setIsStarting] = useState(false); const [startError, setStartError] = useState(null); @@ -146,6 +150,11 @@ export const useMfaMobileConnect = () => { setTokenData({ token: response.token, challenge: response.challenge }); } catch (e) { + if (shouldShowPostureError(e, location)) { + setPostureError(e.message); + setView(LocationCardViews.PostureCheckFail); + return; + } setStartError( e instanceof Error ? e.message : 'Failed to start mobile authentication', ); @@ -153,7 +162,7 @@ export const useMfaMobileConnect = () => { } finally { setIsStarting(false); } - }, [instance, location]); + }, [instance, location, setPostureError, setView]); const reset = useCallback(() => { if (wsRef.current) { diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts index 03a8c90f..5a5d49c5 100644 --- a/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts +++ b/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts @@ -4,7 +4,11 @@ import { error } from '@tauri-apps/plugin-log'; import { useCallback, useEffect, useRef, useState } from 'react'; import { api } from '../../../rust-api/api'; import { getInstancesQueryOptions } from '../../../rust-api/query'; -import { CLIENT_MFA_ENDPOINT, startClientMfaSession } from '../api/startClientMfaSession'; +import { + CLIENT_MFA_ENDPOINT, + shouldShowPostureError, + startClientMfaSession, +} from '../api/startClientMfaSession'; import { useLocationCardContext } from '../context/context'; import { LocationCardViews } from '../context/types'; @@ -15,7 +19,7 @@ type MfaFinishResponse = { preshared_key: string }; type MfaErrorResponse = { error: string }; export const useMfaOidcConnect = () => { - const { location, setView } = useLocationCardContext(); + const { location, setPostureError, setView } = useLocationCardContext(); const [isStarting, setIsStarting] = useState(false); const [startError, setStartError] = useState(null); @@ -136,6 +140,11 @@ export const useMfaOidcConnect = () => { await api.openLink(`${instance.proxy_url}openid/mfa?token=${response.token}`); startPolling(response.token, instance.proxy_url, headers); } catch (e) { + if (shouldShowPostureError(e, location)) { + setPostureError(e.message); + setView(LocationCardViews.PostureCheckFail); + return; + } setStartError( e instanceof Error ? e.message : 'Failed to start OIDC authentication', ); @@ -143,7 +152,7 @@ export const useMfaOidcConnect = () => { } finally { setIsStarting(false); } - }, [instance, location, startPolling, stopPolling]); + }, [instance, location, setPostureError, setView, startPolling, stopPolling]); return { start, isStarting, startError, isPolling, pollError }; }; diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/LocationCardPostureCheckFailView.tsx b/new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/LocationCardPostureCheckFailView.tsx new file mode 100644 index 00000000..62690530 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/LocationCardPostureCheckFailView.tsx @@ -0,0 +1,56 @@ +import './style.scss'; +import { ThemeSpacing } from '../../../../types'; +import { Button } from '../../../Button/Button'; +import { ButtonVariant } from '../../../Button/types'; +import { Controls } from '../../../Controls/Controls'; +import { Divider } from '../../../Divider/Divider'; +import { IconKind } from '../../../Icon'; +import { IconButton } from '../../../IconButton/IconButton'; +import { IconButtonVariant } from '../../../IconButton/types'; +import { SizedBox } from '../../../SizedBox/SizedBox'; +import { LocationViewHeader } from '../../components/LocationViewHeader/LocationViewHeader'; +import { useLocationCardContext } from '../../context/context'; +import { LocationCardViews } from '../../context/types'; + +export const LocationCardPostureCheckFailView = () => { + const { postureError, previousView, setPostureError, setView } = + useLocationCardContext(); + + const retryView = + previousView && previousView !== LocationCardViews.PostureCheckFail + ? previousView + : LocationCardViews.Default; + + const goToDefault = () => { + setPostureError(null); + setView(LocationCardViews.Default); + }; + + const tryAgain = () => { + setPostureError(null); + setView(retryView); + }; + + return ( +
+ + +

+ {postureError ?? 'Your device did not pass posture check.'} +

+
+ + + +
+
+
+
+ ); +}; diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/style.scss b/new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/style.scss new file mode 100644 index 00000000..d1f061d9 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/style.scss @@ -0,0 +1,7 @@ +.location-card-posture-check-fail-view { + .location-card-view-header { + p.error { + color: var(--fg-critical); + } + } +} From ec6f2ed4619b82713cf1627a4e6d52c5c3145b6c Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 21 May 2026 11:45:34 +0200 Subject: [PATCH 06/14] handleMfaStartError helper --- .../LocationCard/hooks/handleMfaStartError.ts | 25 +++++++++++++++++++ .../LocationCard/hooks/useMfaConnect.ts | 11 +++----- .../LocationCard/hooks/useMfaMobileConnect.ts | 11 +++----- .../LocationCard/hooks/useMfaOidcConnect.ts | 11 +++----- 4 files changed, 34 insertions(+), 24 deletions(-) create mode 100644 new-ui/src/shared/components/LocationCard/hooks/handleMfaStartError.ts diff --git a/new-ui/src/shared/components/LocationCard/hooks/handleMfaStartError.ts b/new-ui/src/shared/components/LocationCard/hooks/handleMfaStartError.ts new file mode 100644 index 00000000..62b2db56 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/hooks/handleMfaStartError.ts @@ -0,0 +1,25 @@ +import type { LocationInfo } from '../../../rust-api/types'; +import { shouldShowPostureError } from '../api/startClientMfaSession'; +import { LocationCardViews, type LocationCardViewsValue } from '../context/types'; + +type HandleMfaStartErrorParams = { + err: unknown; + location: LocationInfo; + setPostureError: (error: string | null) => void; + setView: (view: LocationCardViewsValue) => void; +}; + +export const handleMfaStartError = ({ + err, + location, + setPostureError, + setView, +}: HandleMfaStartErrorParams): boolean => { + if (!shouldShowPostureError(err, location)) { + return false; + } + + setPostureError(err.message); + setView(LocationCardViews.PostureCheckFail); + return true; +}; diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts index 83c1a20c..f3139ade 100644 --- a/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts +++ b/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts @@ -5,13 +5,10 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { api } from '../../../rust-api/api'; import { getInstancesQueryOptions } from '../../../rust-api/query'; import type { EdgeRequestHeaders } from '../../../rust-api/types'; -import { - CLIENT_MFA_ENDPOINT, - shouldShowPostureError, - startClientMfaSession, -} from '../api/startClientMfaSession'; +import { CLIENT_MFA_ENDPOINT, startClientMfaSession } from '../api/startClientMfaSession'; import { useLocationCardContext } from '../context/context'; import { LocationCardViews } from '../context/types'; +import { handleMfaStartError } from './handleMfaStartError'; type MfaFinishResponse = { preshared_key: string; @@ -66,9 +63,7 @@ export const useMfaConnect = (method: 0 | 1) => { setRequestHeaders(headers); setToken(response.token); } catch (err) { - if (shouldShowPostureError(err, location)) { - setPostureError(err.message); - setView(LocationCardViews.PostureCheckFail); + if (handleMfaStartError({ err, location, setPostureError, setView })) { return; } setStartError(err instanceof Error ? err.message : 'Failed to start MFA'); diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts index af930a05..0b311f94 100644 --- a/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts +++ b/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts @@ -3,13 +3,10 @@ import { useMutation } from '@tanstack/react-query'; import { error } from '@tauri-apps/plugin-log'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { api } from '../../../rust-api/api'; -import { - CLIENT_MFA_ENDPOINT, - shouldShowPostureError, - startClientMfaSession, -} from '../api/startClientMfaSession'; +import { CLIENT_MFA_ENDPOINT, startClientMfaSession } from '../api/startClientMfaSession'; import { useLocationCardContext } from '../context/context'; import { LocationCardViews } from '../context/types'; +import { handleMfaStartError } from './handleMfaStartError'; type TokenData = { token: string; @@ -150,9 +147,7 @@ export const useMfaMobileConnect = () => { setTokenData({ token: response.token, challenge: response.challenge }); } catch (e) { - if (shouldShowPostureError(e, location)) { - setPostureError(e.message); - setView(LocationCardViews.PostureCheckFail); + if (handleMfaStartError({ err: e, location, setPostureError, setView })) { return; } setStartError( diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts index 5a5d49c5..f84ad726 100644 --- a/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts +++ b/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts @@ -4,13 +4,10 @@ import { error } from '@tauri-apps/plugin-log'; import { useCallback, useEffect, useRef, useState } from 'react'; import { api } from '../../../rust-api/api'; import { getInstancesQueryOptions } from '../../../rust-api/query'; -import { - CLIENT_MFA_ENDPOINT, - shouldShowPostureError, - startClientMfaSession, -} from '../api/startClientMfaSession'; +import { CLIENT_MFA_ENDPOINT, startClientMfaSession } from '../api/startClientMfaSession'; import { useLocationCardContext } from '../context/context'; import { LocationCardViews } from '../context/types'; +import { handleMfaStartError } from './handleMfaStartError'; const POLL_INTERVAL_MS = 5_000; const POLL_TIMEOUT_MS = 5 * 60 * 1_000; // 5 minutes @@ -140,9 +137,7 @@ export const useMfaOidcConnect = () => { await api.openLink(`${instance.proxy_url}openid/mfa?token=${response.token}`); startPolling(response.token, instance.proxy_url, headers); } catch (e) { - if (shouldShowPostureError(e, location)) { - setPostureError(e.message); - setView(LocationCardViews.PostureCheckFail); + if (handleMfaStartError({ err: e, location, setPostureError, setView })) { return; } setStartError( From 6c3f81ecc7f97061140ec582520c7f7422fed3dc Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 21 May 2026 11:52:50 +0200 Subject: [PATCH 07/14] make shouldShowPostureError a type guard --- .../components/LocationCard/api/startClientMfaSession.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts b/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts index 5a3a2f57..31eedd2b 100644 --- a/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts +++ b/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts @@ -29,7 +29,10 @@ type MfaStartErrorResponse = { error?: string; }; -export const shouldShowPostureError = (err: unknown, location: LocationInfo): boolean => +export const shouldShowPostureError = ( + err: unknown, + location: LocationInfo, +): err is MfaStartError => err instanceof MfaStartError && err.status === 403 && location.posture_check_required; type StartClientMfaSessionParams = { From ef068a68bfbc150dbb41cc28e0ac417cf3db4b6b Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 21 May 2026 14:36:15 +0200 Subject: [PATCH 08/14] handle posture errors during posture-only connect flow --- .../LocationCard/api/connectError.ts | 19 ++++++++++++ .../ConnectButton/ConnectButton.tsx | 9 +++++- src-tauri/src/commands.rs | 30 +++++++++++++++++-- 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 new-ui/src/shared/components/LocationCard/api/connectError.ts diff --git a/new-ui/src/shared/components/LocationCard/api/connectError.ts b/new-ui/src/shared/components/LocationCard/api/connectError.ts new file mode 100644 index 00000000..15ed972e --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/api/connectError.ts @@ -0,0 +1,19 @@ +export type ConnectError = + | { + kind: 'postureCheckFailed'; + message: string; + } + | { + kind: 'other'; + message: string; + }; + +export const isPostureCheckFailedConnectError = ( + err: unknown, +): err is Extract => + typeof err === 'object' && + err !== null && + 'kind' in err && + 'message' in err && + err.kind === 'postureCheckFailed' && + typeof err.message === 'string'; diff --git a/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx b/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx index 19a583b8..7684b354 100644 --- a/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx +++ b/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx @@ -3,17 +3,24 @@ import { useMutation } from '@tanstack/react-query'; import clsx from 'clsx'; import { api } from '../../../../rust-api/api'; import { LocationMfaMode } from '../../../../rust-api/types'; +import { isPostureCheckFailedConnectError } from '../../api/connectError'; import { useLocationCardContext } from '../../context/context'; import { LocationCardViews } from '../../context/types'; export const ConnectButton = () => { - const { location, setView, startMfa } = useLocationCardContext(); + const { location, setPostureError, setView, startMfa } = useLocationCardContext(); const { mutate: connect } = useMutation({ mutationFn: api.connect, onSuccess: () => { setView(LocationCardViews.Connected); }, + onError: (err) => { + if (location.posture_check_required && isPostureCheckFailedConnectError(err)) { + setPostureError(err.message); + setView(LocationCardViews.PostureCheckFail); + } + }, meta: { invalidate: ['locations'], }, diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index af70e7ba..e829ce1e 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -61,6 +61,30 @@ use crate::{ utils::execute_command, }; +#[derive(Debug, Serialize, thiserror::Error)] +#[serde(tag = "kind", content = "message", rename_all = "camelCase")] +pub enum ConnectError { + #[error("Posture check failed: {0}")] + PostureCheckFailed(String), + #[error("{0}")] + Other(String), +} + +impl From for ConnectError { + fn from(error: Error) -> Self { + match error { + Error::PostureCheckFailed(message) => Self::PostureCheckFailed(message), + error => Self::Other(error.to_string()), + } + } +} + +impl From for ConnectError { + fn from(error: sqlx::Error) -> Self { + Error::from(error).into() + } +} + /// Open new WireGuard connection. #[tauri::command(async)] pub async fn connect( @@ -68,7 +92,7 @@ pub async fn connect( connection_type: ConnectionType, preshared_key: Option, handle: AppHandle, -) -> Result<(), Error> { +) -> Result<(), ConnectError> { debug!("Received a command to connect to a {connection_type} with ID {location_id}"); if connection_type == ConnectionType::Location { if let Some(location) = Location::find_by_id(&*DB_POOL, location_id).await? { @@ -89,7 +113,7 @@ pub async fn connect( "Location with ID {location_id} not found in the database, aborting connection \ attempt" ); - return Err(Error::NotFound); + return Err(Error::NotFound.into()); } } else if let Some(tunnel) = Tunnel::find_by_id(&*DB_POOL, location_id).await? { debug!( @@ -100,7 +124,7 @@ pub async fn connect( info!("Successfully connected to tunnel {tunnel}"); } else { error!("Tunnel {location_id} not found"); - return Err(Error::NotFound); + return Err(Error::NotFound.into()); } // Update tray icon to reflect connection state. From f3fbb6db5404261e8735cb81431a5dc5041030ac Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 22 May 2026 09:22:24 +0200 Subject: [PATCH 09/14] posture check error styling --- .../LocationCardPostureCheckFailView.tsx | 24 +++-------------- .../style.scss | 26 ++++++++++++++++++- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/LocationCardPostureCheckFailView.tsx b/new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/LocationCardPostureCheckFailView.tsx index 62690530..a19369d3 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/LocationCardPostureCheckFailView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/LocationCardPostureCheckFailView.tsx @@ -2,11 +2,8 @@ import './style.scss'; import { ThemeSpacing } from '../../../../types'; import { Button } from '../../../Button/Button'; import { ButtonVariant } from '../../../Button/types'; -import { Controls } from '../../../Controls/Controls'; import { Divider } from '../../../Divider/Divider'; -import { IconKind } from '../../../Icon'; -import { IconButton } from '../../../IconButton/IconButton'; -import { IconButtonVariant } from '../../../IconButton/types'; +import { Icon, IconKind } from '../../../Icon'; import { SizedBox } from '../../../SizedBox/SizedBox'; import { LocationViewHeader } from '../../components/LocationViewHeader/LocationViewHeader'; import { useLocationCardContext } from '../../context/context'; @@ -21,11 +18,6 @@ export const LocationCardPostureCheckFailView = () => { ? previousView : LocationCardViews.Default; - const goToDefault = () => { - setPostureError(null); - setView(LocationCardViews.Default); - }; - const tryAgain = () => { setPostureError(null); setView(retryView); @@ -34,23 +26,15 @@ export const LocationCardPostureCheckFailView = () => { return (
+ +

{postureError ?? 'Your device did not pass posture check.'}

- - -
-
-
+
); }; diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/style.scss b/new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/style.scss index d1f061d9..c42ff893 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/style.scss +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/style.scss @@ -1,7 +1,31 @@ .location-card-posture-check-fail-view { + display: flex; + flex-direction: column; + align-items: center; + + .posture-warning-icon { + display: block; + + svg path { + fill: var(--fg-white-70); + } + } + .location-card-view-header { + align-items: center; + text-align: center; + + > .title { + color: var(--fg-white-70); + } + p.error { - color: var(--fg-critical); + color: var(--fg-white-70); } } + + .btn-wrap, + .btn { + width: 100%; + } } From 768b8ee271f098f9dcd139482d97848c75bb428c Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 22 May 2026 09:34:52 +0200 Subject: [PATCH 10/14] fix "Try again" button behavior --- .../LocationCardMfaMobileView.tsx | 17 ++++++++++------- .../LocationCardMfaOidcView.tsx | 9 +++++---- .../LocationCardPostureCheckFailView.tsx | 18 ++++++++---------- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaMobileView/LocationCardMfaMobileView.tsx b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaMobileView/LocationCardMfaMobileView.tsx index b0e09244..34900b04 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaMobileView/LocationCardMfaMobileView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaMobileView/LocationCardMfaMobileView.tsx @@ -17,8 +17,8 @@ import { useMfaMobileConnect } from '../../hooks/useMfaMobileConnect'; type Screen = 'loading' | 'qr' | 'error'; export const LocationCardMfaMobileView = () => { - const { setView } = useLocationCardContext(); - const { start, startError, qrValue, connectionError, reset } = useMfaMobileConnect(); + const { setView, setPostureError } = useLocationCardContext(); + const { start, startError, qrValue, connectionError } = useMfaMobileConnect(); const [screen, setScreen] = useState('loading'); const startedRef = useRef(false); @@ -37,10 +37,9 @@ export const LocationCardMfaMobileView = () => { } }, [startError, connectionError, qrValue]); - const retry = () => { - reset(); - setScreen('loading'); - void start(); + const backToLocation = () => { + setPostureError(null); + setView(LocationCardViews.Default); }; const errorMessage = startError ?? connectionError; @@ -69,7 +68,11 @@ export const LocationCardMfaMobileView = () => { />
{screen === 'error' && ( -
diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaOidcView/LocationCardMfaOidcView.tsx b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaOidcView/LocationCardMfaOidcView.tsx index 6f7aed4a..8e3fe4eb 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaOidcView/LocationCardMfaOidcView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaOidcView/LocationCardMfaOidcView.tsx @@ -16,7 +16,7 @@ import { useMfaOidcConnect } from '../../hooks/useMfaOidcConnect'; type Screen = 'idle' | 'polling' | 'error'; export const LocationCardMfaOidcView = () => { - const { setView } = useLocationCardContext(); + const { setView, setPostureError } = useLocationCardContext(); const { start, isStarting, startError, isPolling, pollError } = useMfaOidcConnect(); const [screen, setScreen] = useState('idle'); @@ -35,8 +35,9 @@ export const LocationCardMfaOidcView = () => { const errorMessage = startError ?? pollError; - const resetToIdle = () => { - setScreen('idle'); + const backToLocation = () => { + setPostureError(null); + setView(LocationCardViews.Default); }; return ( @@ -76,7 +77,7 @@ export const LocationCardMfaOidcView = () => {