Skip to content

Commit 118cf8a

Browse files
committed
startClientMfaSession helper, deduplicate mfa flow code
1 parent c561f9a commit 118cf8a

4 files changed

Lines changed: 132 additions & 155 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { fetch } from '@tauri-apps/plugin-http';
2+
import { api } from '../../../rust-api/api';
3+
import type {
4+
EdgeRequestHeaders,
5+
InstanceInfo,
6+
LocationInfo,
7+
} from '../../../rust-api/types';
8+
9+
export const CLIENT_MFA_ENDPOINT = 'api/v1/client-mfa';
10+
11+
export class MfaStartError extends Error {
12+
constructor(message: string) {
13+
super(message);
14+
this.name = 'MfaStartError';
15+
}
16+
}
17+
18+
export type MfaStartMethod = 0 | 1 | 2 | 4;
19+
20+
export type MfaStartResponse = {
21+
token: string;
22+
challenge?: string;
23+
};
24+
25+
type MfaStartErrorResponse = {
26+
error?: string;
27+
};
28+
29+
type StartClientMfaSessionParams = {
30+
instance: InstanceInfo;
31+
location: LocationInfo;
32+
method: MfaStartMethod;
33+
};
34+
35+
type StartClientMfaSessionResult = {
36+
response: MfaStartResponse;
37+
headers: EdgeRequestHeaders;
38+
};
39+
40+
export const startClientMfaSession = async ({
41+
instance,
42+
location,
43+
method,
44+
}: StartClientMfaSessionParams): Promise<StartClientMfaSessionResult> => {
45+
let headers: EdgeRequestHeaders;
46+
try {
47+
headers = await api.getEdgeRequestHeaders();
48+
} catch {
49+
throw new MfaStartError('Failed to load request headers');
50+
}
51+
52+
let posture_data: unknown;
53+
try {
54+
posture_data = location.posture_check_required
55+
? await api.getPostureData()
56+
: undefined;
57+
} catch {
58+
throw new MfaStartError('Failed to load posture data');
59+
}
60+
61+
try {
62+
const response = await fetch(`${instance.proxy_url}${CLIENT_MFA_ENDPOINT}/start`, {
63+
method: 'POST',
64+
headers: {
65+
'Content-Type': 'application/json',
66+
...headers,
67+
},
68+
body: JSON.stringify({
69+
method,
70+
pubkey: instance.pubkey,
71+
location_id: location.network_id,
72+
posture_data,
73+
}),
74+
});
75+
76+
if (!response.ok) {
77+
const data = (await response.json()) as MfaStartErrorResponse;
78+
throw new MfaStartError(data.error ?? 'Failed to start MFA');
79+
}
80+
81+
return {
82+
response: (await response.json()) as MfaStartResponse,
83+
headers,
84+
};
85+
} catch (err) {
86+
if (err instanceof MfaStartError) {
87+
throw err;
88+
}
89+
throw new MfaStartError('Failed to reach server');
90+
}
91+
};

new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts

Lines changed: 16 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,12 @@ import { fetch } from '@tauri-apps/plugin-http';
33
import { error } from '@tauri-apps/plugin-log';
44
import { useCallback, useEffect, useRef, useState } from 'react';
55
import { api } from '../../../rust-api/api';
6-
import {
7-
getInstancesQueryOptions,
8-
getPlatformHeaderQueryOptions,
9-
} from '../../../rust-api/query';
6+
import { getInstancesQueryOptions } from '../../../rust-api/query';
107
import type { EdgeRequestHeaders } from '../../../rust-api/types';
8+
import { CLIENT_MFA_ENDPOINT, startClientMfaSession } from '../api/startClientMfaSession';
119
import { useLocationCardContext } from '../context/context';
1210
import { LocationCardViews } from '../context/types';
1311

14-
const MFA_ENDPOINT = 'api/v1/client-mfa';
15-
16-
type MfaStartResponse = {
17-
token: string;
18-
challenge?: string;
19-
};
20-
2112
type MfaFinishResponse = {
2213
preshared_key: string;
2314
};
@@ -37,7 +28,6 @@ export const useMfaConnect = (method: 0 | 1) => {
3728
const [requestHeaders, setRequestHeaders] = useState<EdgeRequestHeaders | null>(null);
3829

3930
const { data: instances } = useQuery(getInstancesQueryOptions);
40-
const { data: platformHeader } = useQuery(getPlatformHeaderQueryOptions);
4131

4232
const instance = instances?.find((i) => i.id === location.instance_id);
4333

@@ -52,79 +42,44 @@ export const useMfaConnect = (method: 0 | 1) => {
5242
},
5343
});
5444

55-
// Fire the /start request exactly once when instance + platformHeader are ready.
45+
// Fire the /start request exactly once when instance data is ready.
5646
const startCalled = useRef(false);
5747

5848
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional one-shot trigger via startCalled ref
5949
useEffect(() => {
60-
if (!instance || !platformHeader || startCalled.current) return;
50+
if (!instance || startCalled.current) return;
6151
startCalled.current = true;
6252

6353
setIsStarting(true);
6454

6555
(async () => {
66-
let headers: EdgeRequestHeaders;
6756
try {
68-
headers = await api.getEdgeRequestHeaders();
69-
setRequestHeaders(headers);
70-
} catch {
71-
setStartError('Failed to load request headers');
72-
setIsStarting(false);
73-
return;
74-
}
75-
76-
let posture_data: unknown;
77-
try {
78-
posture_data = location.posture_check_required
79-
? await api.getPostureData()
80-
: undefined;
81-
} catch {
82-
setStartError('Failed to load posture data');
83-
setIsStarting(false);
84-
return;
85-
}
86-
87-
try {
88-
const res = await fetch(`${instance.proxy_url}${MFA_ENDPOINT}/start`, {
89-
method: 'POST',
90-
headers: {
91-
'Content-Type': 'application/json',
92-
...headers,
93-
},
94-
body: JSON.stringify({
95-
method,
96-
pubkey: instance.pubkey,
97-
location_id: location.network_id,
98-
posture_data,
99-
}),
57+
const { response, headers } = await startClientMfaSession({
58+
instance,
59+
location,
60+
method,
10061
});
101-
102-
if (res.ok) {
103-
const data = (await res.json()) as MfaStartResponse;
104-
setToken(data.token);
105-
} else {
106-
const data = (await res.json()) as MfaErrorResponse;
107-
setStartError(data.error ?? 'Failed to start MFA');
108-
}
109-
} catch {
110-
setStartError('Failed to reach server');
62+
setRequestHeaders(headers);
63+
setToken(response.token);
64+
} catch (err) {
65+
setStartError(err instanceof Error ? err.message : 'Failed to start MFA');
11166
} finally {
11267
setIsStarting(false);
11368
}
11469
})();
115-
}, [instance, platformHeader]);
70+
}, [instance]);
11671

11772
const verifyCode = useCallback(
11873
async (code: string) => {
119-
if (!token || !instance || !platformHeader || !requestHeaders) return;
74+
if (!token || !instance || !requestHeaders) return;
12075

12176
setIsVerifying(true);
12277
setVerifyError(null);
12378

12479
const body = JSON.stringify({ token, code });
12580

12681
try {
127-
const res = await fetch(`${instance.proxy_url}${MFA_ENDPOINT}/finish`, {
82+
const res = await fetch(`${instance.proxy_url}${CLIENT_MFA_ENDPOINT}/finish`, {
12883
method: 'POST',
12984
headers: {
13085
'Content-Type': 'application/json',
@@ -160,7 +115,7 @@ export const useMfaConnect = (method: 0 | 1) => {
160115
setIsVerifying(false);
161116
}
162117
},
163-
[token, instance, platformHeader, requestHeaders, location, connectMutate, setView],
118+
[token, instance, requestHeaders, location, connectMutate, setView],
164119
);
165120

166121
return { token, isStarting, startError, verifyCode, isVerifying, verifyError };

new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts

Lines changed: 14 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,12 @@
11
import { encode } from '@stablelib/base64';
22
import { useMutation } from '@tanstack/react-query';
3-
import { fetch } from '@tauri-apps/plugin-http';
43
import { error } from '@tauri-apps/plugin-log';
54
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
65
import { api } from '../../../rust-api/api';
6+
import { CLIENT_MFA_ENDPOINT, startClientMfaSession } from '../api/startClientMfaSession';
77
import { useLocationCardContext } from '../context/context';
88
import { LocationCardViews } from '../context/types';
99

10-
const MFA_ENDPOINT = 'api/v1/client-mfa';
11-
12-
type MfaStartResponse = {
13-
token: string;
14-
challenge: string;
15-
};
16-
17-
type MfaErrorResponse = {
18-
error: string;
19-
};
20-
2110
type TokenData = {
2211
token: string;
2312
challenge: string;
@@ -55,7 +44,7 @@ export const useMfaMobileConnect = () => {
5544
.replace(
5645
/^https:/,
5746
'wss:',
58-
)}${MFA_ENDPOINT}/remote?token=${encodeURIComponent(tokenData.token)}`;
47+
)}${CLIENT_MFA_ENDPOINT}/remote?token=${encodeURIComponent(tokenData.token)}`;
5948

6049
expectedCloseRef.current = false;
6150
const ws = new WebSocket(wsUrl);
@@ -144,48 +133,22 @@ export const useMfaMobileConnect = () => {
144133
// Clear previous token → triggers WS cleanup via effect
145134
setTokenData(null);
146135

147-
let headers: Record<string, string>;
148136
try {
149-
headers = await api.getEdgeRequestHeaders();
150-
} catch {
151-
setStartError('Failed to load request headers');
152-
setIsStarting(false);
153-
return;
154-
}
155-
156-
let posture_data: unknown;
157-
try {
158-
posture_data = location.posture_check_required
159-
? await api.getPostureData()
160-
: undefined;
161-
} catch {
162-
setStartError('Failed to load posture data');
163-
setIsStarting(false);
164-
return;
165-
}
166-
167-
try {
168-
const res = await fetch(`${instance.proxy_url}${MFA_ENDPOINT}/start`, {
169-
method: 'POST',
170-
headers: { 'Content-Type': 'application/json', ...headers },
171-
body: JSON.stringify({
172-
method: 4,
173-
pubkey: instance.pubkey,
174-
location_id: location.network_id,
175-
posture_data,
176-
}),
137+
const { response } = await startClientMfaSession({
138+
instance,
139+
location,
140+
method: 4,
177141
});
178-
179-
if (res.ok) {
180-
const data = (await res.json()) as MfaStartResponse;
181-
setTokenData({ token: data.token, challenge: data.challenge });
182-
} else {
183-
const data = (await res.json()) as MfaErrorResponse;
184-
setStartError(data.error ?? 'Failed to start mobile authentication');
185-
error(`Mobile MFA start failed for location ${location.id}: ${data.error}`);
142+
if (!response.challenge) {
143+
setStartError('Unsupported response from proxy');
144+
return;
186145
}
146+
147+
setTokenData({ token: response.token, challenge: response.challenge });
187148
} catch (e) {
188-
setStartError('Failed to reach server');
149+
setStartError(
150+
e instanceof Error ? e.message : 'Failed to start mobile authentication',
151+
);
189152
error(`Mobile MFA start network error for location ${location.id}: ${e}`);
190153
} finally {
191154
setIsStarting(false);

0 commit comments

Comments
 (0)