From ff4ba1164c26877798b3b37b71e37791b839d3d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Mon, 25 Aug 2025 08:27:14 +0200 Subject: [PATCH 1/9] add setup totp mfa to enrollment --- package.json | 1 + src-tauri/Cargo.toml | 2 +- src/i18n/en/index.ts | 1 + src/i18n/i18n-types.ts | 8 + .../modals/MFAModal/MFAModal.tsx | 5 +- src/pages/enrollment/EnrollmentPage.tsx | 37 ++++- .../EnrollmentSideBar/EnrollmentSideBar.tsx | 67 ++++++--- .../EnrollmentStepIndicator.tsx | 14 +- src/pages/enrollment/components/types.ts | 5 + .../hooks/store/useEnrollmentStore.tsx | 22 ++- .../enrollment/hooks/useEnrollmentApi.tsx | 34 +++++ .../components/DesktopSetup/DesktopSetup.tsx | 92 ++---------- .../steps/SendFinishStep/SendFinishStep.tsx | 139 +++++++++++++++++ .../steps/SendFinishStep/style.scss | 9 ++ .../steps/Totp/TotpEnrollmentStep.tsx | 140 ++++++++++++++++++ src/pages/enrollment/steps/Totp/style.scss | 46 ++++++ src/shared/hooks/api/types.ts | 17 +++ src/shared/types.ts | 7 + 18 files changed, 527 insertions(+), 119 deletions(-) create mode 100644 src/pages/enrollment/components/types.ts create mode 100644 src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx create mode 100644 src/pages/enrollment/steps/SendFinishStep/style.scss create mode 100644 src/pages/enrollment/steps/Totp/TotpEnrollmentStep.tsx create mode 100644 src/pages/enrollment/steps/Totp/style.scss create mode 100644 src/shared/types.ts diff --git a/package.json b/package.json index 2a4ca784..dd3d1c7f 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "typesafe-i18n": "typesafe-i18n", "generate-translation-types": "typesafe-i18n --no-watch", "fix": "biome check --fix && prettier src/**/*.scss -w --log-level silent", + "fix-unsafe": "biome check --fix --unsafe && prettier src/**/*.scss -w --log-level silent", "lint": "biome lint && pnpm run typecheck && prettier src/**/*.scss --check --log-level error", "lint-ci": "biome ci && pnpm run typecheck && prettier src/**/*.scss --check --log-level error", "vite": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4812a5a9..844fc46e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -67,7 +67,7 @@ tauri-plugin-clipboard-manager = "2" tauri-plugin-deep-link = "2" tauri-plugin-dialog = "2" tauri-plugin-fs = "2" -tauri-plugin-http = "2" +tauri-plugin-http = { version = "2", features = ["unsafe-headers"] } tauri-plugin-log = "2" tauri-plugin-notification = "2" tauri-plugin-single-instance = { version = "2", features = ["deep-link"] } diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index 81b50df7..ccef98e6 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -490,6 +490,7 @@ If this will not change, please contact your administrator.`, password: 'Create password', vpn: 'Configure VPN', finish: 'Finish', + mfa: 'Configure mfa', }, appVersion: 'Application version', }, diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index da6d2feb..ec907a19 100644 --- a/src/i18n/i18n-types.ts +++ b/src/i18n/i18n-types.ts @@ -1126,6 +1126,10 @@ type RootTranslation = { * F​i​n​i​s​h */ finish: string + /** + * C​o​n​f​i​g​u​r​e​ ​m​f​a + */ + mfa: string } /** * A​p​p​l​i​c​a​t​i​o​n​ ​v​e​r​s​i​o​n @@ -2790,6 +2794,10 @@ export type TranslationFunctions = { * Finish */ finish: () => LocalizedString + /** + * Configure mfa + */ + mfa: () => LocalizedString } /** * Application version 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 7ca1e1b9..cc6a6f15 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 @@ -26,10 +26,7 @@ import { useToaster } from '../../../../../../../../shared/defguard-ui/hooks/toa import { isPresent } from '../../../../../../../../shared/defguard-ui/utils/isPresent'; import { clientApi } from '../../../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../../../hooks/useClientStore'; -import { - type DefguardInstance, - LocationMfaType, -} from '../../../../../../types'; +import { type DefguardInstance, LocationMfaType } from '../../../../../../types'; import { MfaMobileApprove } from './components/MfaMobileApprove/MfaMobileApprove'; import { BrowserErrorIcon, BrowserPendingIcon, GoToBrowserIcon } from './Icons'; import { useMFAModal } from './useMFAModal'; diff --git a/src/pages/enrollment/EnrollmentPage.tsx b/src/pages/enrollment/EnrollmentPage.tsx index 397ab51f..a4a94d8c 100644 --- a/src/pages/enrollment/EnrollmentPage.tsx +++ b/src/pages/enrollment/EnrollmentPage.tsx @@ -29,6 +29,8 @@ import { DataVerificationStep } from './steps/DataVerificationStep/DataVerificat import { DeviceStep } from './steps/DeviceStep/DeviceStep'; import { FinishStep } from './steps/FinishStep/FinishStep'; import { PasswordStep } from './steps/PasswordStep/PasswordStep'; +import { SendFinishStep } from './steps/SendFinishStep/SendFinishStep'; +import { TotpEnrollmentStep } from './steps/Totp/TotpEnrollmentStep'; import { WelcomeStep } from './steps/WelcomeStep/WelcomeStep'; export const EnrollmentPage = () => { @@ -51,10 +53,17 @@ export const EnrollmentPage = () => { // ensure number of steps is correct useEffect(() => { - if (stepsMax !== steps.length - 1) { - setEnrollmentState({ stepsMax: steps.length - 1 }); - } - }, [setEnrollmentState, stepsMax]); + const stepsIgnored: number[] = []; + steps.forEach((step, index) => { + if (step.ignoreCount) { + stepsIgnored.push(index); + } + }); + setEnrollmentState({ + stepsIgnored, + stepsMax: steps.length - 1, + }); + }, [setEnrollmentState]); useEffect(() => { if (!enrollmentFinished.current) { @@ -64,7 +73,7 @@ export const EnrollmentPage = () => { if (diff > 0) { const timeout = setTimeout(() => { if (!enrollmentFinished.current) { - debug('Enrollment session time ended, navigatig to timeout page.'); + debug('Enrollment session time ended, navigating to timeout page.'); navigate(routes.timeout, { replace: true }); } }, diff); @@ -72,11 +81,11 @@ export const EnrollmentPage = () => { clearTimeout(timeout); }; } else { - debug('Enrollment session time ended, navigatig to timeout page.'); + debug('Enrollment session time ended, navigating to timeout page.'); navigate(routes.timeout, { replace: true }); } } else { - error('Seesion end time not found, navigating to timeout page.'); + error('Session end time not found, navigating to timeout page.'); navigate(routes.timeout, { replace: true }); } } @@ -139,7 +148,18 @@ const steps: EnrollmentStep[] = [ }, { key: 4, - step: , + step: , + backDisabled: true, + }, + { + key: 5, + step: , + backDisabled: true, + ignoreCount: true, + }, + { + key: 6, + step: , backDisabled: true, }, ]; @@ -147,5 +167,6 @@ const steps: EnrollmentStep[] = [ type EnrollmentStep = { backDisabled?: boolean; key: string | number; + ignoreCount?: boolean; step: ReactNode; }; diff --git a/src/pages/enrollment/components/EnrollmentSideBar/EnrollmentSideBar.tsx b/src/pages/enrollment/components/EnrollmentSideBar/EnrollmentSideBar.tsx index 5f1df308..342fcf09 100644 --- a/src/pages/enrollment/components/EnrollmentSideBar/EnrollmentSideBar.tsx +++ b/src/pages/enrollment/components/EnrollmentSideBar/EnrollmentSideBar.tsx @@ -2,14 +2,14 @@ import './style.scss'; import { getVersion } from '@tauri-apps/api/app'; import classNames from 'classnames'; +import dayjs from 'dayjs'; import { useEffect, useMemo, useState } from 'react'; -import type { LocalizedString } from 'typesafe-i18n'; - import { useI18nContext } from '../../../../i18n/i18n-react'; import { Divider } from '../../../../shared/defguard-ui/components/Layout/Divider/Divider.tsx'; import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; import { AdminInfo } from '../AdminInfo/AdminInfo'; import { TimeLeft } from '../TimeLeft/TimeLeft'; +import type { EnrollmentSideBarData } from '../types.ts'; export const EnrollmentSideBar = () => { const { LL } = useI18nContext(); @@ -23,15 +23,40 @@ export const EnrollmentSideBar = () => { const [appVersion, setAppVersion] = useState(undefined); - const steps = useMemo((): LocalizedString[] => { + const steps = useMemo((): EnrollmentSideBarData[] => { const steps = LL.pages.enrollment.sideBar.steps; const vpnStep = vpnOptional ? `${steps.vpn()}*` : steps.vpn(); return [ - steps.welcome(), - steps.verification(), - steps.password(), - vpnStep as LocalizedString, - steps.finish(), + { + label: steps.welcome(), + stepDisplayNumber: 1, + stepIndex: 0, + }, + { + label: steps.verification(), + stepDisplayNumber: 2, + stepIndex: 1, + }, + { + label: steps.password(), + stepDisplayNumber: 3, + stepIndex: 2, + }, + { + label: vpnStep, + stepDisplayNumber: 4, + stepIndex: 3, + }, + { + label: `${steps.mfa()}*`, + stepDisplayNumber: 5, + stepIndex: 4, + }, + { + label: steps.finish(), + stepDisplayNumber: 6, + stepIndex: 5, + }, ]; }, [LL.pages.enrollment.sideBar.steps, vpnOptional]); @@ -53,8 +78,11 @@ export const EnrollmentSideBar = () => {
- {steps.map((text, index) => ( - + {steps.map((data) => ( + ))}
{currentStep !== stepsMax && ( @@ -68,9 +96,9 @@ export const EnrollmentSideBar = () => {

- Copyright © 2023{' '} - - teonite + Copyright © {`${dayjs().year}`} + + defguard

@@ -82,14 +110,15 @@ export const EnrollmentSideBar = () => { }; type StepProps = { - text: LocalizedString; - index: number; + data: EnrollmentSideBarData; }; -const Step = ({ index, text }: StepProps) => { +const Step = ({ data: { label, stepIndex, stepDisplayNumber } }: StepProps) => { const currentStep = useEnrollmentStore((state) => state.step); - const active = currentStep === index; + const active = Array.isArray(stepIndex) + ? stepIndex.includes(currentStep) + : stepIndex === currentStep; const cn = classNames('step', { active, @@ -98,8 +127,8 @@ const Step = ({ index, text }: StepProps) => { return (

- {index + 1}.{' '} - {text} + {stepDisplayNumber}.{' '} + {label}

{active &&
}
diff --git a/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx b/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx index 241e72f5..41b8a895 100644 --- a/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx +++ b/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx @@ -1,24 +1,22 @@ import './style.scss'; -import { shallow } from 'zustand/shallow'; - import { useI18nContext } from '../../../../i18n/i18n-react'; import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; export const EnrollmentStepIndicator = () => { const { LL } = useI18nContext(); - const [step, maxStep] = useEnrollmentStore( - (state) => [state.step, state.stepsMax], - shallow, - ); + const [step, maxStep] = useEnrollmentStore((state) => [ + state.step + 1, + state.stepsMax - state.stepsIgnored.length + 1, + ]); return (

- {LL.pages.enrollment.stepsIndicator.step()} {step + 1}{' '} + {LL.pages.enrollment.stepsIndicator.step()} {step}{' '} - {LL.pages.enrollment.stepsIndicator.of()} {maxStep + 1} + {LL.pages.enrollment.stepsIndicator.of()} {maxStep}

diff --git a/src/pages/enrollment/components/types.ts b/src/pages/enrollment/components/types.ts new file mode 100644 index 00000000..3ee16633 --- /dev/null +++ b/src/pages/enrollment/components/types.ts @@ -0,0 +1,5 @@ +export type EnrollmentSideBarData = { + stepIndex: number | number[]; + label: string; + stepDisplayNumber: number; +}; diff --git a/src/pages/enrollment/hooks/store/useEnrollmentStore.tsx b/src/pages/enrollment/hooks/store/useEnrollmentStore.tsx index 9c5f49f3..05f0b4eb 100644 --- a/src/pages/enrollment/hooks/store/useEnrollmentStore.tsx +++ b/src/pages/enrollment/hooks/store/useEnrollmentStore.tsx @@ -2,15 +2,19 @@ import { pick } from 'lodash-es'; import { Subject } from 'rxjs'; import { createJSONStorage, persist } from 'zustand/middleware'; import { createWithEqualityFn } from 'zustand/traditional'; - -import type { AdminInfo, UserInfo } from '../../../../shared/hooks/api/types'; +import type { + AdminInfo, + CreateDeviceResponse, + UserInfo, +} from '../../../../shared/hooks/api/types'; const defaultValues: StoreValues = { // assume default dev proxy_url: '/api/v1/', loading: false, step: 0, - stepsMax: 4, + stepsMax: 6, + stepsIgnored: [], sessionStart: undefined, sessionEnd: undefined, userInfo: undefined, @@ -19,10 +23,14 @@ const defaultValues: StoreValues = { userPassword: undefined, cookie: undefined, nextSubject: new Subject(), + deviceKeys: undefined, + deviceResponse: undefined, }; const persistKeys: Array = [ 'step', + 'stepsMax', + 'stepsIgnored', 'userInfo', 'userPassword', 'sessionEnd', @@ -31,6 +39,8 @@ const persistKeys: Array = [ 'deviceName', 'endContent', 'vpnOptional', + 'deviceKeys', + 'deviceResponse', ]; export const useEnrollmentStore = createWithEqualityFn()( @@ -73,6 +83,7 @@ type StoreValues = { loading: boolean; step: number; stepsMax: number; + stepsIgnored: number[]; nextSubject: Subject; // Date proxy_url: string; @@ -86,6 +97,11 @@ type StoreValues = { endContent?: string; deviceName?: string; cookie?: string; + deviceKeys?: { + public: string; + private: string; + }; + deviceResponse?: CreateDeviceResponse; }; type StoreMethods = { diff --git a/src/pages/enrollment/hooks/useEnrollmentApi.tsx b/src/pages/enrollment/hooks/useEnrollmentApi.tsx index dece7d54..6c76adf0 100644 --- a/src/pages/enrollment/hooks/useEnrollmentApi.tsx +++ b/src/pages/enrollment/hooks/useEnrollmentApi.tsx @@ -2,6 +2,7 @@ import { fetch } from '@tauri-apps/plugin-http'; import { useEnrollmentStore } from '../../../pages/enrollment/hooks/store/useEnrollmentStore'; import type { UseApi } from '../../../shared/hooks/api/types'; +import { MfaMethod } from '../../../shared/types'; export const useEnrollmentApi = (): UseApi => { const [proxyUrl, cookie] = useEnrollmentStore((state) => [ @@ -9,6 +10,37 @@ export const useEnrollmentApi = (): UseApi => { state.cookie, ]); + const registerCodeMfaStart: UseApi['enrollment']['registerCodeMfaStart'] = async ( + method, + ) => { + console.log(cookie); + const response = await fetch(`${proxyUrl}/enrollment/register-mfa/start`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Cookie: cookie, + } as Record, + body: JSON.stringify({ + method: method.valueOf(), + }), + }); + return await response.json(); + }; + + const registerCodeMfaFinish: UseApi['enrollment']['registerCodeMfaFinish'] = async ( + data, + ) => { + const response = await fetch(`${proxyUrl}/enrollment/register-mfa/finish`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Cookie: cookie, + } as Record, + body: JSON.stringify({ ...data, method: MfaMethod.TOTP.valueOf() }), + }); + return await response.json(); + }; + const start: UseApi['enrollment']['start'] = async (data) => { const response = await fetch(`${proxyUrl}/enrollment/start`, { method: 'POST', @@ -64,6 +96,8 @@ export const useEnrollmentApi = (): UseApi => { start, activateUser, createDevice, + registerCodeMfaStart, + registerCodeMfaFinish, }, getAppInfo, }; diff --git a/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx b/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx index 783debc0..d3042924 100644 --- a/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx +++ b/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx @@ -1,8 +1,8 @@ import './style.scss'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { debug, error, info } from '@tauri-apps/plugin-log'; +import { useMutation } from '@tanstack/react-query'; +import { error } from '@tauri-apps/plugin-log'; import { isUndefined } from 'lodash-es'; import { useMemo, useState } from 'react'; import { type SubmitHandler, useForm } from 'react-hook-form'; @@ -19,24 +19,19 @@ import { Card } from '../../../../../../shared/defguard-ui/components/Layout/Car import { useToaster } from '../../../../../../shared/defguard-ui/hooks/toasts/useToaster'; import type { CreateDeviceResponse } from '../../../../../../shared/hooks/api/types'; import { generateWGKeys } from '../../../../../../shared/utils/generateWGKeys'; -import { clientApi } from '../../../../../client/clientAPI/clientApi'; -import { clientQueryKeys } from '../../../../../client/query'; import { useEnrollmentStore } from '../../../../hooks/store/useEnrollmentStore'; import { useEnrollmentApi } from '../../../../hooks/useEnrollmentApi'; -const { saveConfig } = clientApi; - type FormFields = { name: string; }; export const DesktopSetup = () => { - const queryClient = useQueryClient(); const { LL } = useI18nContext(); const toaster = useToaster(); const stepLL = LL.pages.enrollment.steps.deviceSetup; const { - enrollment: { createDevice, activateUser }, + enrollment: { createDevice }, } = useEnrollmentApi(); const deviceName = useEnrollmentStore((state) => state.deviceName); const [userInfo, userPassword] = useEnrollmentStore((state) => [ @@ -47,21 +42,6 @@ export const DesktopSetup = () => { const next = useEnrollmentStore((state) => state.nextStep); const [isLoading, setIsLoading] = useState(false); - const { mutateAsync: mutateUserActivation, isPending: activationPending } = useMutation( - { - mutationFn: activateUser, - onError: (e) => { - toaster.error( - LL.common.messages.errorWithMessage({ - message: String(e), - }), - ); - console.error(e); - error(String(e)); - }, - }, - ); - const { mutateAsync: createDeviceMutation, isPending: createDevicePending } = useMutation({ mutationFn: createDevice, @@ -89,7 +69,7 @@ export const DesktopSetup = () => { const handleValidSubmit: SubmitHandler = async (values) => { if (!userInfo || !userPassword) return; const { publicKey, privateKey } = generateWGKeys(); - const deviceResponse = await createDeviceMutation({ + const deviceResponse = (await createDeviceMutation({ name: values.name, pubkey: publicKey, }).then(async (res) => { @@ -101,58 +81,18 @@ export const DesktopSetup = () => { ); } return res; + })) as CreateDeviceResponse; + toaster.success(stepLL.desktopSetup.messages.deviceConfigured()); + setEnrollmentStore({ + deviceName: values.name, + deviceKeys: { + private: privateKey, + public: publicKey, + }, + deviceResponse, }); - mutateUserActivation({ - password: userPassword, - phone_number: userInfo.phone_number, - }).then(async (res) => { - if (!res.ok) { - error( - `Failed to activate user during the enrollment.Error details: ${JSON.stringify( - await res.json(), - )} Error status code: ${res.status} `, - ); - throw Error('Failed to activate user'); - } - info('User activated'); - setIsLoading(true); - debug('Invoking save_device_config'); - const response = (await deviceResponse.json()) as CreateDeviceResponse; - saveConfig({ - privateKey, - response, - }) - .then(() => { - debug('Config saved'); - setIsLoading(false); - setEnrollmentStore({ deviceName: values.name }); - toaster.success(stepLL.desktopSetup.messages.deviceConfigured()); - const invalidate = [clientQueryKeys.getInstances, clientQueryKeys.getLocations]; - invalidate.forEach((key) => { - queryClient.invalidateQueries({ - queryKey: [key], - }); - }); - next(); - }) - .catch((e) => { - setIsLoading(false); - - if (typeof e === 'string') { - if (e.includes('Network Error')) { - toaster.error(LL.common.messages.networkError()); - return; - } - toaster.error(LL.common.messages.errorWithMessage({ message: String(e) })); - } else { - toaster.error( - LL.common.messages.errorWithMessage({ - message: String(e), - }), - ); - } - }); - }); + setIsLoading(false); + next(); }; return ( @@ -174,7 +114,7 @@ export const DesktopSetup = () => { : stepLL.desktopSetup.controls.create() } disabled={!isUndefined(deviceName)} - loading={isLoading || activationPending || createDevicePending} + loading={isLoading || createDevicePending} /> diff --git a/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx b/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx new file mode 100644 index 00000000..46c7a080 --- /dev/null +++ b/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx @@ -0,0 +1,139 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { debug, error, info } from '@tauri-apps/plugin-log'; +import { useCallback, useEffect } from 'react'; +import { shallow } from 'zustand/shallow'; +import { useI18nContext } from '../../../../i18n/i18n-react'; +import { Card } from '../../../../shared/defguard-ui/components/Layout/Card/Card'; +import { LoaderSpinner } from '../../../../shared/defguard-ui/components/Layout/LoaderSpinner/LoaderSpinner'; +import { useToaster } from '../../../../shared/defguard-ui/hooks/toasts/useToaster'; +import { isPresent } from '../../../../shared/defguard-ui/utils/isPresent'; +import type { + ActivateUserRequest, + CreateDeviceResponse, +} from '../../../../shared/hooks/api/types'; +import { clientApi } from '../../../client/clientAPI/clientApi'; +import { clientQueryKeys } from '../../../client/query'; +import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; +import { useEnrollmentApi } from '../../hooks/useEnrollmentApi'; +import './style.scss'; + +const { saveConfig } = clientApi; + +export const SendFinishStep = () => { + const { LL } = useI18nContext(); + const toaster = useToaster(); + const { + enrollment: { activateUser }, + } = useEnrollmentApi(); + const queryClient = useQueryClient(); + + const finishData = useEnrollmentStore((s) => ({ + phone_number: s.userInfo?.phone_number as string, + password: s.userPassword as string, + })); + + const deviceKeys = useEnrollmentStore((s) => s.deviceKeys); + const deviceResponse = useEnrollmentStore((s) => s.deviceResponse); + + const [setEnrollmentStore, next] = useEnrollmentStore( + (state) => [state.setState, state.nextStep], + shallow, + ); + + const queryFn = useCallback( + async ( + finishData: ActivateUserRequest, + deviceResponse: CreateDeviceResponse, + privateKey: string, + ) => { + await activateUser(finishData); + info('User activated'); + debug('Invoking save_device_config'); + saveConfig({ + privateKey, + response: deviceResponse, + }) + .then(() => { + debug('Config saved'); + setEnrollmentStore({ deviceName: deviceResponse.device.name }); + const invalidate = [clientQueryKeys.getInstances, clientQueryKeys.getLocations]; + invalidate.forEach((key) => { + queryClient.invalidateQueries({ + queryKey: [key], + }); + }); + next(); + }) + .catch((e) => { + if (typeof e === 'string') { + if (e.includes('Network Error')) { + toaster.error(LL.common.messages.networkError()); + return; + } + toaster.error(LL.common.messages.errorWithMessage({ message: String(e) })); + } else { + toaster.error( + LL.common.messages.errorWithMessage({ + message: String(e), + }), + ); + } + }); + }, + [ + LL.common.messages.errorWithMessage, + LL.common.messages.networkError, + activateUser, + next, + queryClient.invalidateQueries, + setEnrollmentStore, + toaster.error, + ], + ); + + const { mutate, isPending, isSuccess } = useMutation({ + mutationFn: () => + queryFn( + finishData, + deviceResponse as CreateDeviceResponse, + deviceKeys?.private as string, + ), + onError: (e) => { + toaster.error( + LL.common.messages.errorWithMessage({ + message: String(e), + }), + ); + console.error(e); + error(String(e)); + }, + onSuccess: () => { + next(); + }, + }); + + // biome-ignore lint/correctness/useExhaustiveDependencies: onMount + useEffect(() => { + console.log({ + isPending, + finishData, + deviceKeys, + deviceResponse, + }); + if ( + !isPending && + !isSuccess && + isPresent(finishData) && + isPresent(deviceKeys) && + isPresent(deviceResponse) + ) { + mutate(); + } + }, [finishData, deviceKeys, deviceResponse]); + + return ( + + + + ); +}; diff --git a/src/pages/enrollment/steps/SendFinishStep/style.scss b/src/pages/enrollment/steps/SendFinishStep/style.scss new file mode 100644 index 00000000..ff74a618 --- /dev/null +++ b/src/pages/enrollment/steps/SendFinishStep/style.scss @@ -0,0 +1,9 @@ +#enrollment-finish-request-step { + width: 100%; + max-width: 650px; + min-height: 25dvh; + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; +} diff --git a/src/pages/enrollment/steps/Totp/TotpEnrollmentStep.tsx b/src/pages/enrollment/steps/Totp/TotpEnrollmentStep.tsx new file mode 100644 index 00000000..3e196953 --- /dev/null +++ b/src/pages/enrollment/steps/Totp/TotpEnrollmentStep.tsx @@ -0,0 +1,140 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { useEffect, useRef } from 'react'; +import { type SubmitHandler, useForm } from 'react-hook-form'; +import QRCode from 'react-qr-code'; +import z from 'zod'; +import { shallow } from 'zustand/shallow'; +import { FormInput } from '../../../../shared/defguard-ui/components/Form/FormInput/FormInput'; +import { Card } from '../../../../shared/defguard-ui/components/Layout/Card/Card'; +import { LoaderSpinner } from '../../../../shared/defguard-ui/components/Layout/LoaderSpinner/LoaderSpinner'; +import { isPresent } from '../../../../shared/defguard-ui/utils/isPresent'; +import { MfaMethod } from '../../../../shared/types'; +import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; +import { useEnrollmentApi } from '../../hooks/useEnrollmentApi'; +import './style.scss'; +import { error } from '@tauri-apps/plugin-log'; +import { Button } from '../../../../shared/defguard-ui/components/Layout/Button/Button'; +import { MessageBox } from '../../../../shared/defguard-ui/components/Layout/MessageBox/MessageBox'; +import SvgIconCopy from '../../../../shared/defguard-ui/components/svg/IconCopy'; +import { useToaster } from '../../../../shared/defguard-ui/hooks/toasts/useToaster'; +import { useClipboard } from '../../../../shared/hooks/useClipboard'; + +const formSchema = z.object({ + code: z.string().min(6, 'Enter valid code').max(6, 'Enter valid code'), +}); + +type FormFields = z.infer; + +export const TotpEnrollmentStep = () => { + const toaster = useToaster(); + const submitRef = useRef(null); + const { writeToClipboard } = useClipboard(); + const userInfo = useEnrollmentStore((s) => s.userInfo); + const [nextSubject, setStoreState, nextStep] = useEnrollmentStore( + (s) => [s.nextSubject, s.setState, s.nextStep], + shallow, + ); + + const { + enrollment: { registerCodeMfaFinish, registerCodeMfaStart }, + } = useEnrollmentApi(); + + const { data: startData, isLoading: startLoading } = useQuery({ + queryFn: () => registerCodeMfaStart(MfaMethod.TOTP), + queryKey: ['enrollment', 'register-mfa', 'start'], + refetchOnWindowFocus: false, + }); + + const { handleSubmit, control, setError } = useForm({ + resolver: zodResolver(formSchema), + }); + + const { mutate, isPending } = useMutation({ + mutationFn: registerCodeMfaFinish, + onSuccess: () => { + toaster.success('MFA configured'); + setStoreState({ loading: false }); + nextStep(); + }, + onError: (err) => { + setError( + 'code', + { + message: 'Enter valid code', + type: 'value', + }, + { + shouldFocus: true, + }, + ); + error(`MFA configuration failed! \nReason: ${err.message}`); + console.error(err); + }, + }); + + const isLoading = startLoading || isPending; + + const submitHandler: SubmitHandler = (data) => { + mutate(data); + }; + + // biome-ignore lint/correctness/useExhaustiveDependencies: sideEffect + useEffect(() => { + setStoreState({ + loading: startLoading || isPending, + }); + }, [startLoading, isPending]); + + useEffect(() => { + const sub = nextSubject.subscribe(() => { + submitRef.current?.click(); + }); + return () => { + sub.unsubscribe(); + }; + }, [nextSubject]); + + return ( + + {isLoading && ( +
+ +
+ )} + {!isLoading && ( + <> +

Configure MFA

+ +
+ {!isLoading && + isPresent(startData) && + isPresent(startData.totp_secret) && + isPresent(userInfo) && ( + <> +
+ +
+
); }; + +const ExampleButton = () => { + return ( +
+ +
+ ); +}; + +const QrIcon = () => { + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/style.scss b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/style.scss index 4ef30d7b..330f1e4d 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/style.scss +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/style.scss @@ -6,4 +6,23 @@ box-sizing: border-box; padding: var(--spacing-m) var(--spacing-s); gap: var(--spacing-m); + + .message-box { + p { + display: inline-flex; + column-gap: 10px; + align-items: center; + } + } + + .example-mobile-button { + border-radius: 15px; + background-color: var(--surface-main-primary); + display: inline-flex; + flex-flow: row nowrap; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + } } From 56701defebbd8735fac37a2db26ce7743c99575f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Mon, 25 Aug 2025 10:22:49 +0200 Subject: [PATCH 4/9] style fix --- .../modals/MFAModal/components/MfaMobileApprove/style.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/style.scss b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/style.scss index 330f1e4d..82ebca66 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/style.scss +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/style.scss @@ -10,6 +10,7 @@ .message-box { p { display: inline-flex; + flex-flow: row wrap; column-gap: 10px; align-items: center; } From 8f61fed3360bdf058a73a6adb4d6ec9e476f7d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Mon, 25 Aug 2025 10:57:52 +0200 Subject: [PATCH 5/9] fix enrollment state --- .../MfaMobileApprove/MfaMobileApprove.tsx | 12 +------- .../EnrollmentSideBar/EnrollmentSideBar.tsx | 4 +-- .../EnrollmentStepIndicator.tsx | 16 ++++++++-- .../enrollment/hooks/useEnrollmentApi.tsx | 1 - .../components/DesktopSetup/DesktopSetup.tsx | 2 +- .../steps/SendFinishStep/SendFinishStep.tsx | 30 +++++++------------ 6 files changed, 29 insertions(+), 36 deletions(-) diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/MfaMobileApprove.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/MfaMobileApprove.tsx index b8c0f200..ac098e12 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/MfaMobileApprove.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/MfaMobileApprove.tsx @@ -47,16 +47,10 @@ export const MfaMobileApprove = ({ [proxyUrl], ); - const { lastMessage, readyState } = useWebSocket(wsUrl, { + const { lastMessage } = useWebSocket(wsUrl, { queryParams: { token, }, - onOpen: () => { - console.log('Websocket connected'); - }, - onClose: () => { - console.log('Websocket closed'); - }, }); const qrString = useMemo(() => { @@ -71,10 +65,6 @@ export const MfaMobileApprove = ({ return fromUint8Array(encoded); }, [token, challenge, instanceUuid]); - useEffect(() => { - console.log(`Last msg: ${lastMessage}\nState ${readyState}`); - }, [lastMessage, readyState]); - // biome-ignore lint/correctness/useExhaustiveDependencies: Side effect useEffect(() => { if (lastMessage != null) { diff --git a/src/pages/enrollment/components/EnrollmentSideBar/EnrollmentSideBar.tsx b/src/pages/enrollment/components/EnrollmentSideBar/EnrollmentSideBar.tsx index 342fcf09..1dd2deda 100644 --- a/src/pages/enrollment/components/EnrollmentSideBar/EnrollmentSideBar.tsx +++ b/src/pages/enrollment/components/EnrollmentSideBar/EnrollmentSideBar.tsx @@ -50,12 +50,12 @@ export const EnrollmentSideBar = () => { { label: `${steps.mfa()}*`, stepDisplayNumber: 5, - stepIndex: 4, + stepIndex: [4, 5], }, { label: steps.finish(), stepDisplayNumber: 6, - stepIndex: 5, + stepIndex: 6, }, ]; }, [LL.pages.enrollment.sideBar.steps, vpnOptional]); diff --git a/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx b/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx index 41b8a895..eb7dc4bc 100644 --- a/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx +++ b/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx @@ -1,16 +1,28 @@ import './style.scss'; +import { useMemo } from 'react'; import { useI18nContext } from '../../../../i18n/i18n-react'; import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; export const EnrollmentStepIndicator = () => { const { LL } = useI18nContext(); - const [step, maxStep] = useEnrollmentStore((state) => [ - state.step + 1, + const [stateStep, maxStep, ignoredSteps] = useEnrollmentStore((state) => [ + state.step, state.stepsMax - state.stepsIgnored.length + 1, + state.stepsIgnored, ]); + const step = useMemo(() => { + let res = stateStep; + ignoredSteps.forEach((ignored) => { + if (ignored <= stateStep) { + res += 1; + } + }); + return res + 1; + }, [ignoredSteps, stateStep]); + return (

diff --git a/src/pages/enrollment/hooks/useEnrollmentApi.tsx b/src/pages/enrollment/hooks/useEnrollmentApi.tsx index 6c76adf0..01198ac5 100644 --- a/src/pages/enrollment/hooks/useEnrollmentApi.tsx +++ b/src/pages/enrollment/hooks/useEnrollmentApi.tsx @@ -13,7 +13,6 @@ export const useEnrollmentApi = (): UseApi => { const registerCodeMfaStart: UseApi['enrollment']['registerCodeMfaStart'] = async ( method, ) => { - console.log(cookie); const response = await fetch(`${proxyUrl}/enrollment/register-mfa/start`, { method: 'POST', headers: { diff --git a/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx b/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx index d3042924..8b714aed 100644 --- a/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx +++ b/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx @@ -80,7 +80,7 @@ export const DesktopSetup = () => { )} Error status code: ${res.status} `, ); } - return res; + return await res.json(); })) as CreateDeviceResponse; toaster.success(stepLL.desktopSetup.messages.deviceConfigured()); setEnrollmentStore({ diff --git a/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx b/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx index 46c7a080..dfd67530 100644 --- a/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx +++ b/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx @@ -1,12 +1,11 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { debug, error, info } from '@tauri-apps/plugin-log'; -import { useCallback, useEffect } from 'react'; +import { useCallback } from 'react'; import { shallow } from 'zustand/shallow'; import { useI18nContext } from '../../../../i18n/i18n-react'; import { Card } from '../../../../shared/defguard-ui/components/Layout/Card/Card'; import { LoaderSpinner } from '../../../../shared/defguard-ui/components/Layout/LoaderSpinner/LoaderSpinner'; import { useToaster } from '../../../../shared/defguard-ui/hooks/toasts/useToaster'; -import { isPresent } from '../../../../shared/defguard-ui/utils/isPresent'; import type { ActivateUserRequest, CreateDeviceResponse, @@ -16,6 +15,7 @@ import { clientQueryKeys } from '../../../client/query'; import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; import { useEnrollmentApi } from '../../hooks/useEnrollmentApi'; import './style.scss'; +import useEffectOnce from '../../../../shared/defguard-ui/utils/useEffectOnce'; const { saveConfig } = clientApi; @@ -91,7 +91,7 @@ export const SendFinishStep = () => { ], ); - const { mutate, isPending, isSuccess } = useMutation({ + const { mutate } = useMutation({ mutationFn: () => queryFn( finishData, @@ -99,6 +99,7 @@ export const SendFinishStep = () => { deviceKeys?.private as string, ), onError: (e) => { + setEnrollmentStore({ loading: false }); toaster.error( LL.common.messages.errorWithMessage({ message: String(e), @@ -108,28 +109,19 @@ export const SendFinishStep = () => { error(String(e)); }, onSuccess: () => { + setEnrollmentStore({ loading: false }); next(); }, }); - // biome-ignore lint/correctness/useExhaustiveDependencies: onMount - useEffect(() => { - console.log({ - isPending, - finishData, - deviceKeys, - deviceResponse, + useEffectOnce(() => { + setEnrollmentStore({ + loading: true, }); - if ( - !isPending && - !isSuccess && - isPresent(finishData) && - isPresent(deviceKeys) && - isPresent(deviceResponse) - ) { + setTimeout(() => { mutate(); - } - }, [finishData, deviceKeys, deviceResponse]); + }, 250); + }); return ( From bc9b913263c20a1794e69e41ac425b0ace699d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Mon, 25 Aug 2025 12:18:59 +0200 Subject: [PATCH 6/9] style fixes --- .../EnrollmentStepIndicator.tsx | 29 ++++++++++++------- .../components/DesktopSetup/DesktopSetup.tsx | 2 ++ .../steps/PasswordStep/PasswordStep.tsx | 27 +++++++++++++---- .../steps/Totp/TotpEnrollmentStep.tsx | 6 +++- src/pages/enrollment/style.scss | 2 +- 5 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx b/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx index eb7dc4bc..1ca4c669 100644 --- a/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx +++ b/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx @@ -4,31 +4,38 @@ import { useMemo } from 'react'; import { useI18nContext } from '../../../../i18n/i18n-react'; import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; +function getDisplayStep( + currentIndex: number, + stepsMax: number, + ignoredSteps: number[], +): number { + const ignored = [...new Set(ignoredSteps)] + .filter((s) => s >= 0 && s < stepsMax) + .sort((a, b) => a - b); + const ignoredBefore = ignored.filter((step) => step <= currentIndex).length; + return currentIndex + 1 - ignoredBefore; +} + export const EnrollmentStepIndicator = () => { const { LL } = useI18nContext(); const [stateStep, maxStep, ignoredSteps] = useEnrollmentStore((state) => [ state.step, - state.stepsMax - state.stepsIgnored.length + 1, + state.stepsMax, state.stepsIgnored, ]); - const step = useMemo(() => { - let res = stateStep; - ignoredSteps.forEach((ignored) => { - if (ignored <= stateStep) { - res += 1; - } - }); - return res + 1; - }, [ignoredSteps, stateStep]); + const step = useMemo( + () => getDisplayStep(stateStep, maxStep, ignoredSteps), + [ignoredSteps, stateStep, maxStep], + ); return (

{LL.pages.enrollment.stepsIndicator.step()} {step}{' '} - {LL.pages.enrollment.stepsIndicator.of()} {maxStep} + {LL.pages.enrollment.stepsIndicator.of()} {maxStep + 1 - ignoredSteps.length}

diff --git a/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx b/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx index 8b714aed..fc0e0636 100644 --- a/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx +++ b/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx @@ -19,6 +19,7 @@ import { Card } from '../../../../../../shared/defguard-ui/components/Layout/Car import { useToaster } from '../../../../../../shared/defguard-ui/hooks/toasts/useToaster'; import type { CreateDeviceResponse } from '../../../../../../shared/hooks/api/types'; import { generateWGKeys } from '../../../../../../shared/utils/generateWGKeys'; +import { EnrollmentStepIndicator } from '../../../../components/EnrollmentStepIndicator/EnrollmentStepIndicator'; import { useEnrollmentStore } from '../../../../hooks/store/useEnrollmentStore'; import { useEnrollmentApi } from '../../../../hooks/useEnrollmentApi'; @@ -97,6 +98,7 @@ export const DesktopSetup = () => { return ( +

{stepLL.desktopSetup.title()}

{ password: passwordValidator(LL), repeat: z.string().min(1, LL.form.errors.required()), }) - .refine((values) => values.password === values.repeat, { - message: pageLL.form.fields.repeat.errors.matching(), - path: ['repeat'], + .superRefine((values, ctx) => { + if (values.password !== values.repeat && values.repeat.length >= 1) { + ctx.addIssue({ + path: ['repeat'], + message: pageLL.form.fields.repeat.errors.matching(), + code: 'custom', + }); + } }), [LL, pageLL.form.fields.repeat.errors], ); @@ -85,7 +90,13 @@ export const PasswordStep = () => { > { /> diff --git a/src/pages/enrollment/steps/Totp/TotpEnrollmentStep.tsx b/src/pages/enrollment/steps/Totp/TotpEnrollmentStep.tsx index 3e196953..21f881ff 100644 --- a/src/pages/enrollment/steps/Totp/TotpEnrollmentStep.tsx +++ b/src/pages/enrollment/steps/Totp/TotpEnrollmentStep.tsx @@ -19,6 +19,7 @@ import { MessageBox } from '../../../../shared/defguard-ui/components/Layout/Mes import SvgIconCopy from '../../../../shared/defguard-ui/components/svg/IconCopy'; import { useToaster } from '../../../../shared/defguard-ui/hooks/toasts/useToaster'; import { useClipboard } from '../../../../shared/hooks/useClipboard'; +import { EnrollmentStepIndicator } from '../../components/EnrollmentStepIndicator/EnrollmentStepIndicator'; const formSchema = z.object({ code: z.string().min(6, 'Enter valid code').max(6, 'Enter valid code'), @@ -104,7 +105,10 @@ export const TotpEnrollmentStep = () => { )} {!isLoading && ( <> -

Configure MFA

+
+ +

Configure MFA

+
h3 { + h3 { @include typography(app-body-1); color: var(--text-body-primary); From 88908e5497785f49e8515f3dc744a59dc6e27cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Tue, 26 Aug 2025 09:55:30 +0200 Subject: [PATCH 7/9] update endpoints --- src/pages/enrollment/hooks/useEnrollmentApi.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/enrollment/hooks/useEnrollmentApi.tsx b/src/pages/enrollment/hooks/useEnrollmentApi.tsx index 01198ac5..913304c6 100644 --- a/src/pages/enrollment/hooks/useEnrollmentApi.tsx +++ b/src/pages/enrollment/hooks/useEnrollmentApi.tsx @@ -13,7 +13,7 @@ export const useEnrollmentApi = (): UseApi => { const registerCodeMfaStart: UseApi['enrollment']['registerCodeMfaStart'] = async ( method, ) => { - const response = await fetch(`${proxyUrl}/enrollment/register-mfa/start`, { + const response = await fetch(`${proxyUrl}/enrollment/register-mfa/code/start`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -29,7 +29,7 @@ export const useEnrollmentApi = (): UseApi => { const registerCodeMfaFinish: UseApi['enrollment']['registerCodeMfaFinish'] = async ( data, ) => { - const response = await fetch(`${proxyUrl}/enrollment/register-mfa/finish`, { + const response = await fetch(`${proxyUrl}/enrollment/register-mfa/code/finish`, { method: 'POST', headers: { 'Content-Type': 'application/json', From 3c0db114987bd42b97bc49454478caa3fe480011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= <102536422+filipslezaklab@users.noreply.github.com> Date: Tue, 26 Aug 2025 10:03:29 +0200 Subject: [PATCH 8/9] Update src/i18n/en/index.ts Co-authored-by: Maciek <19913370+wojcik91@users.noreply.github.com> --- src/i18n/en/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index ccef98e6..82640aa2 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -490,7 +490,7 @@ If this will not change, please contact your administrator.`, password: 'Create password', vpn: 'Configure VPN', finish: 'Finish', - mfa: 'Configure mfa', + mfa: 'Configure MFA', }, appVersion: 'Application version', }, From b31ff70a438608d7b68cfcfd04fc002d1bc47f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Tue, 26 Aug 2025 10:04:34 +0200 Subject: [PATCH 9/9] Update proto --- src-tauri/proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/proto b/src-tauri/proto index dbb08b75..4688d4d5 160000 --- a/src-tauri/proto +++ b/src-tauri/proto @@ -1 +1 @@ -Subproject commit dbb08b75e1b009c48a73bfc07cb25bea1838243a +Subproject commit 4688d4d587246e09d800c03963721b437f3a3eca