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 = () => {
)}
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 a19369d3..a2cf7c52 100644
--- a/new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/LocationCardPostureCheckFailView.tsx
+++ b/new-ui/src/shared/components/LocationCard/views/LocationCardPostureCheckFailView/LocationCardPostureCheckFailView.tsx
@@ -10,17 +10,11 @@ import { useLocationCardContext } from '../../context/context';
import { LocationCardViews } from '../../context/types';
export const LocationCardPostureCheckFailView = () => {
- const { postureError, previousView, setPostureError, setView } =
- useLocationCardContext();
+ const { postureError, setPostureError, setView } = useLocationCardContext();
- const retryView =
- previousView && previousView !== LocationCardViews.PostureCheckFail
- ? previousView
- : LocationCardViews.Default;
-
- const tryAgain = () => {
+ const backToLocation = () => {
setPostureError(null);
- setView(retryView);
+ setView(LocationCardViews.Default);
};
return (
@@ -34,7 +28,11 @@ export const LocationCardPostureCheckFailView = () => {
-
+
);
};
From 12f890064215def12cc7d49637ee11b8f63f228f Mon Sep 17 00:00:00 2001
From: Jacek Chmielewski
Date: Fri, 22 May 2026 09:49:42 +0200
Subject: [PATCH 11/14] fix errors in old ui
---
src/pages/client/clientAPI/clientApi.ts | 20 +++++++++++++++-----
1 file changed, 15 insertions(+), 5 deletions(-)
diff --git a/src/pages/client/clientAPI/clientApi.ts b/src/pages/client/clientAPI/clientApi.ts
index 4278415d..fdf1b1a9 100644
--- a/src/pages/client/clientAPI/clientApi.ts
+++ b/src/pages/client/clientAPI/clientApi.ts
@@ -27,6 +27,19 @@ import type {
UpdateInstanceRequest,
} from './types';
+const getInvokeErrorMessage = (command: TauriCommandKey, e: unknown): string => {
+ if (
+ typeof e === 'object' &&
+ e !== null &&
+ 'message' in e &&
+ typeof e.message === 'string'
+ ) {
+ return e.message;
+ }
+
+ return `Invoking "${command}" failed due to unknown error: ${JSON.stringify(e)}`;
+};
+
// Streamlines logging for invokes
async function invokeWrapper(
command: TauriCommandKey,
@@ -43,9 +56,7 @@ async function invokeWrapper(
return res;
// TODO: handle more error types ?
} catch (e) {
- let message: string = `Invoking "${command}" failed due to unknown error: ${JSON.stringify(
- e,
- )}`;
+ let message = getInvokeErrorMessage(command, e);
trace(message);
if (e instanceof TimeoutError) {
message = `Invoking "${command}" timed out after ${timeout / 1000} seconds`;
@@ -130,8 +141,7 @@ const stopGlobalLogWatcher = async (): Promise =>
const getAppConfig = async (): Promise =>
invokeWrapper('command_get_app_config');
-const getPostureData = async (): Promise =>
- invokeWrapper('get_posture_data');
+const getPostureData = async (): Promise => invokeWrapper('get_posture_data');
const getProvisioningConfig = async (): Promise =>
invokeWrapper('get_provisioning_config');
From d0e0f1de571a4376b4c2972b203d295505f2a8a3 Mon Sep 17 00:00:00 2001
From: Jacek Chmielewski
Date: Fri, 22 May 2026 10:01:41 +0200
Subject: [PATCH 12/14] docstrings
---
.../LocationCard/api/startClientMfaSession.ts | 11 +++++++++++
.../LocationCard/hooks/handleMfaStartError.ts | 1 +
src/pages/client/clientAPI/clientApi.ts | 1 +
3 files changed, 13 insertions(+)
diff --git a/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts b/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts
index 31eedd2b..44f30ff0 100644
--- a/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts
+++ b/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts
@@ -8,6 +8,7 @@ import type {
export const CLIENT_MFA_ENDPOINT = 'api/v1/client-mfa';
+/** Error raised when the MFA start request or its prerequisites fail. */
export class MfaStartError extends Error {
public readonly status?: number;
@@ -18,34 +19,44 @@ export class MfaStartError extends Error {
}
}
+/**
+ * MFA method identifiers expected by the desktop-client MFA API:
+ * 0 = TOTP, 1 = email, 2 = OIDC, 4 = mobile approval.
+ */
export type MfaStartMethod = 0 | 1 | 2 | 4;
+/** Successful MFA start response returned by the proxy. */
export type MfaStartResponse = {
token: string;
challenge?: string;
};
+/** Error response shape returned by the proxy for MFA start failures. */
type MfaStartErrorResponse = {
error?: string;
};
+/** Narrows MFA start errors that should open the posture failure view. */
export const shouldShowPostureError = (
err: unknown,
location: LocationInfo,
): err is MfaStartError =>
err instanceof MfaStartError && err.status === 403 && location.posture_check_required;
+/** Input required to start a desktop-client MFA session. */
type StartClientMfaSessionParams = {
instance: InstanceInfo;
location: LocationInfo;
method: MfaStartMethod;
};
+/** MFA start response plus request headers required by later MFA calls. */
type StartClientMfaSessionResult = {
response: MfaStartResponse;
headers: EdgeRequestHeaders;
};
+/** Starts an MFA session, including posture data when the location requires it. */
export const startClientMfaSession = async ({
instance,
location,
diff --git a/new-ui/src/shared/components/LocationCard/hooks/handleMfaStartError.ts b/new-ui/src/shared/components/LocationCard/hooks/handleMfaStartError.ts
index 62b2db56..fbc929db 100644
--- a/new-ui/src/shared/components/LocationCard/hooks/handleMfaStartError.ts
+++ b/new-ui/src/shared/components/LocationCard/hooks/handleMfaStartError.ts
@@ -9,6 +9,7 @@ type HandleMfaStartErrorParams = {
setView: (view: LocationCardViewsValue) => void;
};
+/** Handles MFA start posture failures and reports whether the error was consumed. */
export const handleMfaStartError = ({
err,
location,
diff --git a/src/pages/client/clientAPI/clientApi.ts b/src/pages/client/clientAPI/clientApi.ts
index fdf1b1a9..ac14da39 100644
--- a/src/pages/client/clientAPI/clientApi.ts
+++ b/src/pages/client/clientAPI/clientApi.ts
@@ -27,6 +27,7 @@ import type {
UpdateInstanceRequest,
} from './types';
+/** Extracts a human-readable message from Tauri command rejections. */
const getInvokeErrorMessage = (command: TauriCommandKey, e: unknown): string => {
if (
typeof e === 'object' &&
From b08957bb22d2a24efcbe30088ae5c09716d82baa Mon Sep 17 00:00:00 2001
From: Jacek Chmielewski
Date: Fri, 22 May 2026 11:26:10 +0200
Subject: [PATCH 13/14] fix error typing
---
.../LocationCard/api/connectError.ts | 37 ++++++++++---------
.../ConnectButton/ConnectButton.tsx | 11 ++++--
2 files changed, 27 insertions(+), 21 deletions(-)
diff --git a/new-ui/src/shared/components/LocationCard/api/connectError.ts b/new-ui/src/shared/components/LocationCard/api/connectError.ts
index 15ed972e..828f64bb 100644
--- a/new-ui/src/shared/components/LocationCard/api/connectError.ts
+++ b/new-ui/src/shared/components/LocationCard/api/connectError.ts
@@ -1,19 +1,20 @@
-export type ConnectError =
- | {
- kind: 'postureCheckFailed';
- message: string;
- }
- | {
- kind: 'other';
- message: string;
- };
+import z from 'zod';
-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';
+const connectErrorSchema = z.discriminatedUnion('kind', [
+ z.object({
+ kind: z.literal('postureCheckFailed'),
+ message: z.string(),
+ }),
+ z.object({
+ kind: z.literal('other'),
+ message: z.string(),
+ }),
+]);
+
+export type ConnectError = z.infer;
+
+export const parseConnectError = (err: unknown): ConnectError | null => {
+ const result = connectErrorSchema.safeParse(err);
+
+ return result.success ? result.data : null;
+};
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 7684b354..a430f323 100644
--- a/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx
+++ b/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx
@@ -3,7 +3,7 @@ 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 { parseConnectError } from '../../api/connectError';
import { useLocationCardContext } from '../../context/context';
import { LocationCardViews } from '../../context/types';
@@ -16,8 +16,13 @@ export const ConnectButton = () => {
setView(LocationCardViews.Connected);
},
onError: (err) => {
- if (location.posture_check_required && isPostureCheckFailedConnectError(err)) {
- setPostureError(err.message);
+ const connectError = parseConnectError(err);
+
+ if (
+ location.posture_check_required &&
+ connectError?.kind === 'postureCheckFailed'
+ ) {
+ setPostureError(connectError.message);
setView(LocationCardViews.PostureCheckFail);
}
},
From 56cbd893e8d96fae9a9c73ffa04fca8a7c331110 Mon Sep 17 00:00:00 2001
From: Jacek Chmielewski
Date: Fri, 22 May 2026 11:53:42 +0200
Subject: [PATCH 14/14] MfaMethod "enum"
---
.../LocationCard/api/startClientMfaSession.ts | 14 +++++++++-----
.../components/LocationCard/hooks/useMfaConnect.ts | 10 ++++++++--
.../LocationCard/hooks/useMfaMobileConnect.ts | 8 ++++++--
.../LocationCard/hooks/useMfaOidcConnect.ts | 8 ++++++--
.../LocationCardMfaEmailView.tsx | 6 ++++--
.../LocationCardMfaTotpView.tsx | 6 ++++--
.../LocationsList/modals/MFAModal/MFAModal.tsx | 2 +-
7 files changed, 38 insertions(+), 16 deletions(-)
diff --git a/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts b/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts
index 44f30ff0..21ac5f08 100644
--- a/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts
+++ b/new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts
@@ -19,11 +19,15 @@ export class MfaStartError extends Error {
}
}
-/**
- * MFA method identifiers expected by the desktop-client MFA API:
- * 0 = TOTP, 1 = email, 2 = OIDC, 4 = mobile approval.
- */
-export type MfaStartMethod = 0 | 1 | 2 | 4;
+/** MFA method identifiers expected by the desktop-client MFA API */
+export const MfaStartMethod = {
+ Totp: 0,
+ Email: 1,
+ Oidc: 2,
+ MobileApprove: 4,
+} as const;
+
+export type MfaStartMethod = (typeof MfaStartMethod)[keyof typeof MfaStartMethod];
/** Successful MFA start response returned by the proxy. */
export type MfaStartResponse = {
diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts
index f3139ade..67c52e72 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,
+ type MfaStartMethod,
+ startClientMfaSession,
+} from '../api/startClientMfaSession';
import { useLocationCardContext } from '../context/context';
import { LocationCardViews } from '../context/types';
import { handleMfaStartError } from './handleMfaStartError';
@@ -18,7 +22,9 @@ type MfaErrorResponse = {
error: string;
};
-export const useMfaConnect = (method: 0 | 1) => {
+type CodeMfaStartMethod = Extract;
+
+export const useMfaConnect = (method: CodeMfaStartMethod) => {
const { location, setPostureError, setView } = useLocationCardContext();
const [token, setToken] = useState(null);
diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts
index 0b311f94..3d4c1c4c 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,
+ MfaStartMethod,
+ startClientMfaSession,
+} from '../api/startClientMfaSession';
import { useLocationCardContext } from '../context/context';
import { LocationCardViews } from '../context/types';
import { handleMfaStartError } from './handleMfaStartError';
@@ -138,7 +142,7 @@ export const useMfaMobileConnect = () => {
const { response } = await startClientMfaSession({
instance,
location,
- method: 4,
+ method: MfaStartMethod.MobileApprove,
});
if (!response.challenge) {
setStartError('Unsupported response from proxy');
diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts
index f84ad726..938b6bb6 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,
+ MfaStartMethod,
+ startClientMfaSession,
+} from '../api/startClientMfaSession';
import { useLocationCardContext } from '../context/context';
import { LocationCardViews } from '../context/types';
import { handleMfaStartError } from './handleMfaStartError';
@@ -132,7 +136,7 @@ export const useMfaOidcConnect = () => {
const { response, headers } = await startClientMfaSession({
instance,
location,
- method: 2,
+ method: MfaStartMethod.Oidc,
});
await api.openLink(`${instance.proxy_url}openid/mfa?token=${response.token}`);
startPolling(response.token, instance.proxy_url, headers);
diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx
index 9b908c77..633578d8 100644
--- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx
+++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx
@@ -10,6 +10,7 @@ import { IconKind } from '../../../Icon';
import { IconButton } from '../../../IconButton/IconButton';
import { IconButtonVariant } from '../../../IconButton/types';
import { SizedBox } from '../../../SizedBox/SizedBox';
+import { MfaStartMethod } from '../../api/startClientMfaSession';
import { LocationViewHeader } from '../../components/LocationViewHeader/LocationViewHeader';
import { useLocationCardContext } from '../../context/context';
import { LocationCardViews } from '../../context/types';
@@ -17,8 +18,9 @@ import { useMfaConnect } from '../../hooks/useMfaConnect';
export const LocationCardMfaEmailView = () => {
const { setView } = useLocationCardContext();
- const { verifyCode, isVerifying, verifyError, isStarting, startError } =
- useMfaConnect(1);
+ const { verifyCode, isVerifying, verifyError, isStarting, startError } = useMfaConnect(
+ MfaStartMethod.Email,
+ );
const [emailCode, setEmailCode] = useState(null);
const [error, setError] = useState(null);
diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx
index fc64eec1..4bb2ff29 100644
--- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx
+++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx
@@ -10,6 +10,7 @@ import { IconKind } from '../../../Icon';
import { IconButton } from '../../../IconButton/IconButton';
import { IconButtonVariant } from '../../../IconButton/types';
import { SizedBox } from '../../../SizedBox/SizedBox';
+import { MfaStartMethod } from '../../api/startClientMfaSession';
import { LocationViewHeader } from '../../components/LocationViewHeader/LocationViewHeader';
import { useLocationCardContext } from '../../context/context';
import { LocationCardViews } from '../../context/types';
@@ -17,8 +18,9 @@ import { useMfaConnect } from '../../hooks/useMfaConnect';
export const LocationCardMfaTotpView = () => {
const { setView } = useLocationCardContext();
- const { verifyCode, isVerifying, verifyError, isStarting, startError } =
- useMfaConnect(0);
+ const { verifyCode, isVerifying, verifyError, isStarting, startError } = useMfaConnect(
+ MfaStartMethod.Totp,
+ );
const [totpCode, setTotpCode] = useState(null);
const [error, setError] = useState(null);
diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx
index 9d7a9181..c13d492e 100644
--- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx
+++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx
@@ -100,7 +100,7 @@ export const MFAModal = () => {
}
}, [location, resetAuthState]);
- // selectedMethod: 0 = authenticator app, 1 = email, 2 = OpenID, 3 = MobileApprove
+ // selectedMethod: 0 = authenticator app, 1 = email, 2 = OpenID, 4 = MobileApprove
const startMFA = useCallback(
async (method: number) => {
if (!location) return toaster.error(localLL.errors.locationNotSpecified());