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 01/13] 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 04/13] 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 05/13] 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 06/13] 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 07/13] 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 08/13] 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 09/13] 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 From d0fa7232f2cef3ce96f2edcca2b2366a548ef6a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Thu, 28 Aug 2025 10:01:52 +0200 Subject: [PATCH 10/13] enrollment mfa update --- package.json | 1 + pnpm-lock.yaml | 10 ++ src-tauri/Cargo.lock | 60 +++++++ src-tauri/Cargo.toml | 1 + src-tauri/capabilities/default.json | 8 + src-tauri/src/bin/defguard-client.rs | 1 + src-tauri/src/database/mod.rs | 1 + src/i18n/en/index.ts | 3 + src/i18n/i18n-types.ts | 28 ++- src/pages/enrollment/EnrollmentPage.tsx | 86 ++------- .../EnrollmentSideBar/EnrollmentSideBar.tsx | 169 +++++++++++------- .../components/EnrollmentSideBar/style.scss | 12 +- .../EnrollmentStepIndicator.tsx | 33 +--- src/pages/enrollment/components/types.ts | 5 - src/pages/enrollment/const.tsx | 119 ++++++++++++ .../hooks/store/useEnrollmentStore.tsx | 50 +++--- src/pages/enrollment/hooks/types.ts | 4 + .../enrollment/hooks/useEnrollmentApi.tsx | 4 +- .../steps/ChooseMfaStep/ChooseMfaStep.tsx | 90 ++++++++++ .../enrollment/steps/ChooseMfaStep/style.scss | 30 ++++ .../DataVerificationStep.tsx | 8 +- .../steps/DeviceStep/DeviceStep.tsx | 25 ++- .../components/DesktopSetup/DesktopSetup.tsx | 6 +- .../MfaRecoveryCodesStep.tsx | 65 +++++++ .../steps/MfaRecoveryCodesStep/style.scss | 27 +++ .../MfaSetupStep.tsx} | 115 +++++++----- .../steps/{Totp => MfaSetupStep}/style.scss | 22 ++- .../steps/PasswordStep/PasswordStep.tsx | 22 ++- .../steps/SendFinishStep/SendFinishStep.tsx | 11 +- .../steps/WelcomeStep/WelcomeStep.tsx | 10 +- src/shared/defguard-ui | 2 +- src/shared/hooks/api/types.ts | 1 + 32 files changed, 737 insertions(+), 292 deletions(-) delete mode 100644 src/pages/enrollment/components/types.ts create mode 100644 src/pages/enrollment/const.tsx create mode 100644 src/pages/enrollment/hooks/types.ts create mode 100644 src/pages/enrollment/steps/ChooseMfaStep/ChooseMfaStep.tsx create mode 100644 src/pages/enrollment/steps/ChooseMfaStep/style.scss create mode 100644 src/pages/enrollment/steps/MfaRecoveryCodesStep/MfaRecoveryCodesStep.tsx create mode 100644 src/pages/enrollment/steps/MfaRecoveryCodesStep/style.scss rename src/pages/enrollment/steps/{Totp/TotpEnrollmentStep.tsx => MfaSetupStep/MfaSetupStep.tsx} (60%) rename src/pages/enrollment/steps/{Totp => MfaSetupStep}/style.scss (70%) diff --git a/package.json b/package.json index dd3d1c7f..917b7598 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@tauri-apps/plugin-http": "^2.5.2", "@tauri-apps/plugin-log": "^2.6.0", "@tauri-apps/plugin-notification": "^2.3.0", + "@tauri-apps/plugin-opener": "^2.5.0", "@tauri-apps/plugin-window-state": "^2.4.0", "@types/byte-size": "^8.1.2", "@use-gesture/react": "^10.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d65895e9..37d539bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: '@tauri-apps/plugin-notification': specifier: ^2.3.0 version: 2.3.0 + '@tauri-apps/plugin-opener': + specifier: ^2.5.0 + version: 2.5.0 '@tauri-apps/plugin-window-state': specifier: ^2.4.0 version: 2.4.0 @@ -1104,6 +1107,9 @@ packages: '@tauri-apps/plugin-notification@2.3.0': resolution: {integrity: sha512-QDwXo9VzAlH97c0veuf19TZI6cRBPfJDl2O6hNEDvI66j60lOO9z+PL6MJrj8A6Y+t55r7mGhe3rQWLmOre2HA==} + '@tauri-apps/plugin-opener@2.5.0': + resolution: {integrity: sha512-B0LShOYae4CZjN8leiNDbnfjSrTwoZakqKaWpfoH6nXiJwt6Rgj6RnVIffG3DoJiKsffRhMkjmBV9VeilSb4TA==} + '@tauri-apps/plugin-window-state@2.4.0': resolution: {integrity: sha512-hRSzPNi2NG0lPFthfVY0V5C1MyWN/gGaQtQYw7i9zZhLzrhZveHZ2omHG1rIiIsjfTGbO7fhjydSoeTTK9GqLw==} @@ -3671,6 +3677,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.8.0 + '@tauri-apps/plugin-opener@2.5.0': + dependencies: + '@tauri-apps/api': 2.8.0 + '@tauri-apps/plugin-window-state@2.4.0': dependencies: '@tauri-apps/api': 2.8.0 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index aef4f558..680f6dae 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1265,6 +1265,7 @@ dependencies = [ "tauri-plugin-http", "tauri-plugin-log", "tauri-plugin-notification", + "tauri-plugin-opener", "tauri-plugin-single-instance", "tauri-plugin-window-state", "thiserror 2.0.16", @@ -2787,6 +2788,25 @@ dependencies = [ "serde", ] +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -3766,6 +3786,18 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "open" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openssl" version = "0.10.73" @@ -3922,6 +3954,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -6214,6 +6252,28 @@ dependencies = [ "url", ] +[[package]] +name = "tauri-plugin-opener" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786156aa8e89e03d271fbd3fe642207da8e65f3c961baa9e2930f332bf80a1f5" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation 0.3.1", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.16", + "url", + "windows", + "zbus", +] + [[package]] name = "tauri-plugin-single-instance" version = "2.3.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 844fc46e..7a134367 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -94,6 +94,7 @@ x25519-dalek = { version = "2", features = [ "serde", "static_secrets", ] } +tauri-plugin-opener = "2.5.0" [target.'cfg(unix)'.dependencies] tokio-stream = "0.1" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index f3da2f08..7f69045c 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -59,6 +59,14 @@ "url": "http://*:*" } ] + }, + { + "identifier": "opener:allow-open-url", + "allow": [ + { + "url": "https://*" + } + ] } ] } diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 3818c400..560e8a14 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -193,6 +193,7 @@ fn main() { .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_window_state::Builder::new().build()) + .plugin(tauri_plugin_opener::init()) .setup(|app| { // Handle deep-links. let app_handle = app.app_handle().clone(); diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index e2ba9868..e2a03b6a 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -19,6 +19,7 @@ pub static DB_POOL: LazyLock = LazyLock::new(|| { let db_url = prepare_db_url().expect("Wrong database URL."); let opts = SqliteConnectOptions::from_str(&db_url) .expect("Failed to set database connenction options.") + .create_if_missing(true) .auto_vacuum(SqliteAutoVacuum::Incremental) .journal_mode(SqliteJournalMode::Wal); debug!("Connecting to database: {db_url} with options: {opts:?}"); diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index 82640aa2..3f8e0e5e 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -491,6 +491,9 @@ If this will not change, please contact your administrator.`, vpn: 'Configure VPN', finish: 'Finish', mfa: 'Configure MFA', + mfaChoice: 'Choose method', + mfaSetup: 'Complete method', + mfaRecovery: 'Recovery codes', }, appVersion: 'Application version', }, diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index ec907a19..78a251c1 100644 --- a/src/i18n/i18n-types.ts +++ b/src/i18n/i18n-types.ts @@ -1127,9 +1127,21 @@ type RootTranslation = { */ finish: string /** - * C​o​n​f​i​g​u​r​e​ ​m​f​a + * C​o​n​f​i​g​u​r​e​ ​M​F​A */ mfa: string + /** + * C​h​o​o​s​e​ ​m​e​t​h​o​d + */ + mfaChoice: string + /** + * C​o​m​p​l​e​t​e​ ​m​e​t​h​o​d + */ + mfaSetup: string + /** + * R​e​c​o​v​e​r​y​ ​c​o​d​e​s + */ + mfaRecovery: string } /** * A​p​p​l​i​c​a​t​i​o​n​ ​v​e​r​s​i​o​n @@ -2795,9 +2807,21 @@ export type TranslationFunctions = { */ finish: () => LocalizedString /** - * Configure mfa + * Configure MFA */ mfa: () => LocalizedString + /** + * Choose method + */ + mfaChoice: () => LocalizedString + /** + * Complete method + */ + mfaSetup: () => LocalizedString + /** + * Recovery codes + */ + mfaRecovery: () => LocalizedString } /** * Application version diff --git a/src/pages/enrollment/EnrollmentPage.tsx b/src/pages/enrollment/EnrollmentPage.tsx index a4a94d8c..b2c6256c 100644 --- a/src/pages/enrollment/EnrollmentPage.tsx +++ b/src/pages/enrollment/EnrollmentPage.tsx @@ -2,7 +2,7 @@ import './style.scss'; import { debug, error } from '@tauri-apps/plugin-log'; import dayjs from 'dayjs'; -import { type ReactNode, useEffect, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { useBreakpoint } from 'use-breakpoint'; import { shallow } from 'zustand/shallow'; @@ -24,14 +24,8 @@ import { import { routes } from '../../shared/routes'; import { EnrollmentSideBar } from './components/EnrollmentSideBar/EnrollmentSideBar'; import { EnrollmentStepControls } from './components/EnrollmentStepControls/EnrollmentStepControls'; +import { EnrollmentStepKey, enrollmentSteps, flattenEnrollConf } from './const'; import { useEnrollmentStore } from './hooks/store/useEnrollmentStore'; -import { DataVerificationStep } from './steps/DataVerificationStep/DataVerificationStep'; -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 = () => { const enrollmentFinished = useRef(false); @@ -40,30 +34,16 @@ export const EnrollmentPage = () => { const { breakpoint } = useBreakpoint(deviceBreakpoints); const sessionEnd = useEnrollmentStore((state) => state.sessionEnd); const currentStep = useEnrollmentStore((state) => state.step); - const stepsMax = useEnrollmentStore((state) => state.stepsMax); const loading = useEnrollmentStore((state) => state.loading); - const [setEnrollmentState, back, _reset, nextSubject] = useEnrollmentStore( - (state) => [state.setState, state.perviousStep, state.reset, state.nextSubject], - shallow, - ); + const [back, next] = useEnrollmentStore((state) => [state.back, state.next], shallow); const controlsSize: ButtonSize = breakpoint !== 'desktop' ? ButtonSize.SMALL : ButtonSize.LARGE; - // ensure number of steps is correct - useEffect(() => { - const stepsIgnored: number[] = []; - steps.forEach((step, index) => { - if (step.ignoreCount) { - stepsIgnored.push(index); - } - }); - setEnrollmentState({ - stepsIgnored, - stepsMax: steps.length - 1, - }); - }, [setEnrollmentState]); + const flatConf = useMemo(() => flattenEnrollConf(), []); + + const currentStepConfig = flatConf[currentStep]; useEffect(() => { if (!enrollmentFinished.current) { @@ -92,8 +72,8 @@ export const EnrollmentPage = () => { }, [sessionEnd, navigate]); useEffect(() => { - enrollmentFinished.current = stepsMax === currentStep; - }, [currentStep, stepsMax]); + enrollmentFinished.current = currentStep === EnrollmentStepKey.FINISH; + }, [currentStep]); return ( @@ -105,7 +85,7 @@ export const EnrollmentPage = () => { size={controlsSize} styleVariant={ButtonStyleVariant.STANDARD} onClick={() => back()} - disabled={(steps[currentStep].backDisabled ?? false) || loading} + disabled={(currentStepConfig.backEnabled ?? false) || loading} icon={ { />
- {steps.map((data) => ( - + {stepsData.map((step) => ( + + + {isPresent(step.children) && + step.children.map((child) => ( + + ))} + ))}
- {currentStep !== stepsMax && ( + {currentStep !== EnrollmentStepKey.FINISH && ( <> )} - {currentStep === stepsMax && } + {currentStep === EnrollmentStepKey.FINISH && }

- Copyright © {`${dayjs().year}`} - + Copyright © {`${dayjs().year()} `} + { + void openPath('https://defguard.net'); + }} + > defguard

@@ -110,27 +148,24 @@ export const EnrollmentSideBar = () => { }; type StepProps = { - data: EnrollmentSideBarData; + data: SideBarItem; + child?: boolean; }; -const Step = ({ data: { label, stepIndex, stepDisplayNumber } }: StepProps) => { +const Step = ({ data, child = false }: StepProps) => { const currentStep = useEnrollmentStore((state) => state.step); - const active = Array.isArray(stepIndex) - ? stepIndex.includes(currentStep) - : stepIndex === currentStep; - - const cn = classNames('step', { - active, - }); + const active = data.activeKeys.includes(currentStep); return ( -
-

- {stepDisplayNumber}.{' '} - {label} -

- {active &&
} +
+

{data.label}

+ {active && !isPresent(data.children) &&
}
); }; diff --git a/src/pages/enrollment/components/EnrollmentSideBar/style.scss b/src/pages/enrollment/components/EnrollmentSideBar/style.scss index fa1e5176..16e6d77f 100644 --- a/src/pages/enrollment/components/EnrollmentSideBar/style.scss +++ b/src/pages/enrollment/components/EnrollmentSideBar/style.scss @@ -81,9 +81,11 @@ align-items: center; justify-content: flex-start; box-sizing: border-box; - padding: 62px 0 0 0; position: relative; user-select: none; + flex-grow: 1; + padding: var(--spacing-s) 0; + justify-content: center; & > .step { position: relative; @@ -94,14 +96,17 @@ justify-content: flex-start; color: var(--text-body-tertiary); box-sizing: border-box; - padding: 15px 10px 10px 32px; - min-height: 58px; user-select: none; + padding: var(--spacing-xs) var(--spacing-s); transition-property: color; transition-timing-function: ease-in-out; transition-duration: 250ms; + &.child { + padding-left: calc(var(--spacing-s) * 2); + } + &.active { color: var(--text-body-primary); } @@ -127,7 +132,6 @@ } & > .time-left { - margin-top: auto; box-sizing: border-box; margin-bottom: 20px; padding: 0 40px; diff --git a/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx b/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx index 1ca4c669..62398e6f 100644 --- a/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx +++ b/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx @@ -2,41 +2,22 @@ import './style.scss'; import { useMemo } from 'react'; import { useI18nContext } from '../../../../i18n/i18n-react'; +import { flattenEnrollConf } from '../../const'; 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, - ]); - - const step = useMemo( - () => getDisplayStep(stateStep, maxStep, ignoredSteps), - [ignoredSteps, stateStep, maxStep], - ); + const currentStepKey = useEnrollmentStore((state) => state.step); + const flatConf = useMemo(() => flattenEnrollConf(), []); + const currentStep = flatConf[currentStepKey]; return (

- {LL.pages.enrollment.stepsIndicator.step()} {step}{' '} - - {LL.pages.enrollment.stepsIndicator.of()} {maxStep + 1 - ignoredSteps.length} - + {LL.pages.enrollment.stepsIndicator.step()}{' '} + {currentStep.indicatorPrefix ?? currentStep.sideBarPrefix ?? ''}{' '} + {LL.pages.enrollment.stepsIndicator.of()} 6

); diff --git a/src/pages/enrollment/components/types.ts b/src/pages/enrollment/components/types.ts deleted file mode 100644 index 3ee16633..00000000 --- a/src/pages/enrollment/components/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type EnrollmentSideBarData = { - stepIndex: number | number[]; - label: string; - stepDisplayNumber: number; -}; diff --git a/src/pages/enrollment/const.tsx b/src/pages/enrollment/const.tsx new file mode 100644 index 00000000..73425de4 --- /dev/null +++ b/src/pages/enrollment/const.tsx @@ -0,0 +1,119 @@ +import { cloneDeep } from 'lodash-es'; +import type { ReactNode } from 'react'; +import { ChooseMfaStep } from './steps/ChooseMfaStep/ChooseMfaStep'; +import { DataVerificationStep } from './steps/DataVerificationStep/DataVerificationStep'; +import { DeviceStep } from './steps/DeviceStep/DeviceStep'; +import { FinishStep } from './steps/FinishStep/FinishStep'; +import { MfaRecoveryCodesStep } from './steps/MfaRecoveryCodesStep/MfaRecoveryCodesStep'; +import { MfaSetupStep } from './steps/MfaSetupStep/MfaSetupStep'; +import { PasswordStep } from './steps/PasswordStep/PasswordStep'; +import { SendFinishStep } from './steps/SendFinishStep/SendFinishStep'; +import { WelcomeStep } from './steps/WelcomeStep/WelcomeStep'; + +export enum EnrollmentStepKey { + WELCOME = 'welcome', + DATA_VERIFICATION = 'data-verification', + PASSWORD = 'password', + DEVICE = 'device', + MFA = 'mfa', + MFA_CHOICE = 'mfa-choice', + MFA_SETUP = 'mfa-setup', + MFA_RECOVERY = 'mfa-recovery', + ACTIVATE_USER = 'activate', + FINISH = 'finish', +} + +export type EnrollmentStep = { + key: EnrollmentStepKey; + sideBarPrefix?: string; + indicatorPrefix?: string; + // enable back in navigation + backEnabled?: boolean; + nextDisabled?: boolean; + children?: EnrollmentStep[]; + // this means it's only rendered and it doesn't count as a step in UI + // meant for loading in-between steps like send finish + hidden?: boolean; +}; + +// this servers as configuration for side bar and steps indicator +// some steps are like in between loaders and mfa has sub steps that's why this needs to exist +// in side bar this serves as base for final config with translated labels +export const enrollmentStepsConfig: Record = { + [EnrollmentStepKey.WELCOME]: { + key: EnrollmentStepKey.WELCOME, + sideBarPrefix: '1', + }, + [EnrollmentStepKey.DATA_VERIFICATION]: { + key: EnrollmentStepKey.DATA_VERIFICATION, + sideBarPrefix: '2', + }, + [EnrollmentStepKey.PASSWORD]: { + key: EnrollmentStepKey.PASSWORD, + sideBarPrefix: '3', + backEnabled: true, + }, + [EnrollmentStepKey.DEVICE]: { + key: EnrollmentStepKey.DEVICE, + sideBarPrefix: '4', + backEnabled: true, + }, + [EnrollmentStepKey.MFA]: { + key: EnrollmentStepKey.MFA, + sideBarPrefix: '5', + children: [ + { + key: EnrollmentStepKey.MFA_CHOICE, + sideBarPrefix: 'a', + indicatorPrefix: '5a', + nextDisabled: true, + }, + { + key: EnrollmentStepKey.MFA_SETUP, + sideBarPrefix: 'b', + indicatorPrefix: '5b', + backEnabled: true, + }, + { + key: EnrollmentStepKey.MFA_RECOVERY, + sideBarPrefix: 'c', + indicatorPrefix: '5c', + }, + ], + }, + [EnrollmentStepKey.ACTIVATE_USER]: { + key: EnrollmentStepKey.ACTIVATE_USER, + hidden: true, + nextDisabled: true, + }, + [EnrollmentStepKey.FINISH]: { + sideBarPrefix: '6', + key: EnrollmentStepKey.FINISH, + }, +}; + +export const enrollmentSteps: Record = { + [EnrollmentStepKey.WELCOME]: , + [EnrollmentStepKey.DATA_VERIFICATION]: , + [EnrollmentStepKey.PASSWORD]: , + [EnrollmentStepKey.DEVICE]: , + // this will be skipped and is here only for TS + [EnrollmentStepKey.MFA]: null, + [EnrollmentStepKey.MFA_RECOVERY]: , + [EnrollmentStepKey.MFA_CHOICE]: , + [EnrollmentStepKey.MFA_SETUP]: , + [EnrollmentStepKey.ACTIVATE_USER]: , + [EnrollmentStepKey.FINISH]: , +}; + +export const flattenEnrollConf = () => { + const steps = cloneDeep(enrollmentStepsConfig); + Object.values(steps).forEach((step) => { + if (step.children) { + step.children.forEach((child) => { + steps[child.key] = child; + }); + } + }); + return steps; +}; diff --git a/src/pages/enrollment/hooks/store/useEnrollmentStore.tsx b/src/pages/enrollment/hooks/store/useEnrollmentStore.tsx index 05f0b4eb..20eb2a6e 100644 --- a/src/pages/enrollment/hooks/store/useEnrollmentStore.tsx +++ b/src/pages/enrollment/hooks/store/useEnrollmentStore.tsx @@ -7,14 +7,17 @@ import type { CreateDeviceResponse, UserInfo, } from '../../../../shared/hooks/api/types'; +import { MfaMethod } from '../../../../shared/types'; +import { EnrollmentStepKey } from '../../const'; +import { EnrollmentNavDirection } from '../types'; const defaultValues: StoreValues = { // assume default dev proxy_url: '/api/v1/', loading: false, - step: 0, - stepsMax: 6, - stepsIgnored: [], + step: EnrollmentStepKey.WELCOME, + mfaMethod: MfaMethod.TOTP, + recoveryCodes: [], sessionStart: undefined, sessionEnd: undefined, userInfo: undefined, @@ -22,17 +25,18 @@ const defaultValues: StoreValues = { vpnOptional: undefined, userPassword: undefined, cookie: undefined, - nextSubject: new Subject(), + nextSubject: new Subject(), deviceKeys: undefined, deviceResponse: undefined, }; const persistKeys: Array = [ 'step', - 'stepsMax', - 'stepsIgnored', + 'proxy_url', 'userInfo', 'userPassword', + 'recoveryCodes', + 'mfaMethod', 'sessionEnd', 'sessionStart', 'adminInfo', @@ -41,6 +45,7 @@ const persistKeys: Array = [ 'vpnOptional', 'deviceKeys', 'deviceResponse', + 'cookie', ]; export const useEnrollmentStore = createWithEqualityFn()( @@ -50,25 +55,16 @@ export const useEnrollmentStore = createWithEqualityFn()( init: (values) => set({ ...defaultValues, ...values }), setState: (newValues) => set((old) => ({ ...old, ...newValues })), reset: () => set(defaultValues), - nextStep: () => { - const current = get().step; - const max = get().stepsMax; - - if (current < max) { - return set({ step: current + 1 }); - } + next: () => { + get().nextSubject.next(EnrollmentNavDirection.NEXT); }, - perviousStep: () => { - const current = get().step; - - if (current > 0) { - return set({ step: current - 1 }); - } + back: () => { + get().nextSubject.next(EnrollmentNavDirection.BACK); }, }), { name: 'enrollment-storage', - version: 0.1, + version: 2, storage: createJSONStorage(() => sessionStorage), partialize: (state) => pick(state, persistKeys), }, @@ -81,12 +77,12 @@ type Store = StoreValues & StoreMethods; type StoreValues = { // next and back are disabled loading: boolean; - step: number; - stepsMax: number; - stepsIgnored: number[]; - nextSubject: Subject; + step: EnrollmentStepKey; + mfaMethod: MfaMethod; + nextSubject: Subject; // Date proxy_url: string; + recoveryCodes: string[]; sessionStart?: string; sessionEnd?: string; userInfo?: UserInfo; @@ -106,8 +102,8 @@ type StoreValues = { type StoreMethods = { setState: (values: Partial) => void; - reset: () => void; - nextStep: () => void; - perviousStep: () => void; init: (initValues: Partial) => void; + next: () => void; + back: () => void; + reset: () => void; }; diff --git a/src/pages/enrollment/hooks/types.ts b/src/pages/enrollment/hooks/types.ts new file mode 100644 index 00000000..8639ec39 --- /dev/null +++ b/src/pages/enrollment/hooks/types.ts @@ -0,0 +1,4 @@ +export enum EnrollmentNavDirection { + NEXT, + BACK, +} diff --git a/src/pages/enrollment/hooks/useEnrollmentApi.tsx b/src/pages/enrollment/hooks/useEnrollmentApi.tsx index 913304c6..ebf9885f 100644 --- a/src/pages/enrollment/hooks/useEnrollmentApi.tsx +++ b/src/pages/enrollment/hooks/useEnrollmentApi.tsx @@ -2,7 +2,6 @@ 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) => [ @@ -35,8 +34,9 @@ export const useEnrollmentApi = (): UseApi => { 'Content-Type': 'application/json', Cookie: cookie, } as Record, - body: JSON.stringify({ ...data, method: MfaMethod.TOTP.valueOf() }), + body: JSON.stringify(data), }); + if (!response.ok) throw Error('Register finish request failed'); return await response.json(); }; diff --git a/src/pages/enrollment/steps/ChooseMfaStep/ChooseMfaStep.tsx b/src/pages/enrollment/steps/ChooseMfaStep/ChooseMfaStep.tsx new file mode 100644 index 00000000..aeccf33b --- /dev/null +++ b/src/pages/enrollment/steps/ChooseMfaStep/ChooseMfaStep.tsx @@ -0,0 +1,90 @@ +import './style.scss'; + +import { useEffect } from 'react'; +import { shallow } from 'zustand/shallow'; +import { Button } from '../../../../shared/defguard-ui/components/Layout/Button/Button'; +import { + ButtonSize, + ButtonStyleVariant, +} from '../../../../shared/defguard-ui/components/Layout/Button/types'; +import { Card } from '../../../../shared/defguard-ui/components/Layout/Card/Card'; +import { LabeledRadio } from '../../../../shared/defguard-ui/components/Layout/LabeledRadio/LabeledRadio'; +import { MessageBox } from '../../../../shared/defguard-ui/components/Layout/MessageBox/MessageBox'; +import { MfaMethod } from '../../../../shared/types'; +import { EnrollmentStepIndicator } from '../../components/EnrollmentStepIndicator/EnrollmentStepIndicator'; +import { EnrollmentStepKey } from '../../const'; +import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; +import { EnrollmentNavDirection } from '../../hooks/types'; + +export const ChooseMfaStep = () => { + const selectedMethod = useEnrollmentStore((s) => s.mfaMethod); + const [setStore, navSubject] = useEnrollmentStore( + (s) => [s.setState, s.nextSubject], + shallow, + ); + + // biome-ignore lint/correctness/useExhaustiveDependencies: rxjs + useEffect(() => { + const sub = navSubject.subscribe((dir) => { + if (dir === EnrollmentNavDirection.NEXT) { + setStore({ step: EnrollmentStepKey.MFA_SETUP }); + } + }); + return () => { + sub.unsubscribe(); + }; + }, [navSubject]); + + return ( + +
+ +

Enable Multi-Factor Authentication

+
+ +
+ { + setStore({ mfaMethod: MfaMethod.TOTP }); + }} + /> + { + setStore({ mfaMethod: MfaMethod.EMAIL }); + }} + /> +
+
+
+
+ ); +}; diff --git a/src/pages/enrollment/steps/ChooseMfaStep/style.scss b/src/pages/enrollment/steps/ChooseMfaStep/style.scss new file mode 100644 index 00000000..17c87f34 --- /dev/null +++ b/src/pages/enrollment/steps/ChooseMfaStep/style.scss @@ -0,0 +1,30 @@ +#enrollment-choose-mfa-step { + width: 100%; + max-width: 650px; + padding: 50px 40px; + display: flex; + flex-flow: column; + row-gap: var(--spacing-m); + + .choices { + display: flex; + flex-flow: row wrap; + width: 100%; + align-items: start; + justify-content: space-between; + column-gap: var(--spacing-s); + row-gap: var(--spacing-s); + + & > div { + width: calc(50% - (var(--spacing-s) / 2)); + } + } + + .controls { + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: center; + gap: var(--spacing-s); + } +} diff --git a/src/pages/enrollment/steps/DataVerificationStep/DataVerificationStep.tsx b/src/pages/enrollment/steps/DataVerificationStep/DataVerificationStep.tsx index 44491ff4..7139a90a 100644 --- a/src/pages/enrollment/steps/DataVerificationStep/DataVerificationStep.tsx +++ b/src/pages/enrollment/steps/DataVerificationStep/DataVerificationStep.tsx @@ -12,6 +12,7 @@ import { Card } from '../../../../shared/defguard-ui/components/Layout/Card/Card import { MessageBox } from '../../../../shared/defguard-ui/components/Layout/MessageBox/MessageBox'; import { MessageBoxType } from '../../../../shared/defguard-ui/components/Layout/MessageBox/types'; import { EnrollmentStepIndicator } from '../../components/EnrollmentStepIndicator/EnrollmentStepIndicator'; +import { EnrollmentStepKey } from '../../const'; import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; const phonePattern = /^\+?[0-9]+( [0-9]+)?$/; @@ -28,10 +29,7 @@ export const DataVerificationStep = () => { const userInfo = useEnrollmentStore((state) => state.userInfo); - const [setEnrollment, next] = useEnrollmentStore( - (state) => [state.setState, state.nextStep], - shallow, - ); + const [setEnrollment] = useEnrollmentStore((state) => [state.setState], shallow); const pageLL = LL.pages.enrollment.steps.dataVerification; @@ -61,8 +59,8 @@ export const DataVerificationStep = () => { if (userInfo) { setEnrollment({ userInfo: { ...userInfo, phone_number: values.phone }, + step: EnrollmentStepKey.PASSWORD, }); - next(); } }; diff --git a/src/pages/enrollment/steps/DeviceStep/DeviceStep.tsx b/src/pages/enrollment/steps/DeviceStep/DeviceStep.tsx index ac3e9853..3d67e1ff 100644 --- a/src/pages/enrollment/steps/DeviceStep/DeviceStep.tsx +++ b/src/pages/enrollment/steps/DeviceStep/DeviceStep.tsx @@ -3,15 +3,17 @@ import './style.scss'; import classNames from 'classnames'; import { useEffect } from 'react'; import { shallow } from 'zustand/shallow'; - +import { EnrollmentStepKey } from '../../const'; import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; +import { EnrollmentNavDirection } from '../../hooks/types'; import { DesktopSetup } from './components/DesktopSetup/DesktopSetup'; export const DeviceStep = () => { const deviceName = useEnrollmentStore((state) => state.deviceName); const vpnOptional = useEnrollmentStore((state) => state.vpnOptional); - const [nextSubject, next] = useEnrollmentStore( - (state) => [state.nextSubject, state.nextStep], + + const [nextSubject, setStore] = useEnrollmentStore( + (state) => [state.nextSubject, state.setState], shallow, ); @@ -20,17 +22,24 @@ export const DeviceStep = () => { optional: vpnOptional, }); + // biome-ignore lint/correctness/useExhaustiveDependencies: jsx useEffect(() => { - const sub = nextSubject.subscribe(() => { - if (deviceName) { - next(); + const sub = nextSubject.subscribe((direction) => { + switch (direction) { + case EnrollmentNavDirection.BACK: + setStore({ step: EnrollmentStepKey.PASSWORD }); + break; + case EnrollmentNavDirection.NEXT: + if (deviceName) { + setStore({ step: EnrollmentStepKey.MFA_CHOICE }); + } + break; } }); - return () => { sub.unsubscribe(); }; - }, [nextSubject, next, deviceName]); + }, [nextSubject, deviceName]); return (
diff --git a/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx b/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx index fc0e0636..af01aef9 100644 --- a/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx +++ b/src/pages/enrollment/steps/DeviceStep/components/DesktopSetup/DesktopSetup.tsx @@ -20,6 +20,7 @@ import { useToaster } from '../../../../../../shared/defguard-ui/hooks/toasts/us import type { CreateDeviceResponse } from '../../../../../../shared/hooks/api/types'; import { generateWGKeys } from '../../../../../../shared/utils/generateWGKeys'; import { EnrollmentStepIndicator } from '../../../../components/EnrollmentStepIndicator/EnrollmentStepIndicator'; +import { EnrollmentStepKey } from '../../../../const'; import { useEnrollmentStore } from '../../../../hooks/store/useEnrollmentStore'; import { useEnrollmentApi } from '../../../../hooks/useEnrollmentApi'; @@ -40,7 +41,6 @@ export const DesktopSetup = () => { state.userPassword, ]); const setEnrollmentStore = useEnrollmentStore((state) => state.setState); - const next = useEnrollmentStore((state) => state.nextStep); const [isLoading, setIsLoading] = useState(false); const { mutateAsync: createDeviceMutation, isPending: createDevicePending } = @@ -85,15 +85,15 @@ export const DesktopSetup = () => { })) as CreateDeviceResponse; toaster.success(stepLL.desktopSetup.messages.deviceConfigured()); setEnrollmentStore({ + deviceResponse, + step: EnrollmentStepKey.MFA_CHOICE, deviceName: values.name, deviceKeys: { private: privateKey, public: publicKey, }, - deviceResponse, }); setIsLoading(false); - next(); }; return ( diff --git a/src/pages/enrollment/steps/MfaRecoveryCodesStep/MfaRecoveryCodesStep.tsx b/src/pages/enrollment/steps/MfaRecoveryCodesStep/MfaRecoveryCodesStep.tsx new file mode 100644 index 00000000..ad332097 --- /dev/null +++ b/src/pages/enrollment/steps/MfaRecoveryCodesStep/MfaRecoveryCodesStep.tsx @@ -0,0 +1,65 @@ +import './style.scss'; +import { useEffect } from 'react'; +import { shallow } from 'zustand/shallow'; +import { Button } from '../../../../shared/defguard-ui/components/Layout/Button/Button'; +import { Card } from '../../../../shared/defguard-ui/components/Layout/Card/Card'; +import { MessageBox } from '../../../../shared/defguard-ui/components/Layout/MessageBox/MessageBox'; +import { + MessageBoxStyleVariant, + MessageBoxType, +} from '../../../../shared/defguard-ui/components/Layout/MessageBox/types'; +import SvgIconCopy from '../../../../shared/defguard-ui/components/svg/IconCopy'; +import { useClipboard } from '../../../../shared/hooks/useClipboard'; +import { EnrollmentStepIndicator } from '../../components/EnrollmentStepIndicator/EnrollmentStepIndicator'; +import { EnrollmentStepKey } from '../../const'; +import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; +import { EnrollmentNavDirection } from '../../hooks/types'; + +export const MfaRecoveryCodesStep = () => { + const { writeToClipboard } = useClipboard(); + const [setStore, navSubject] = useEnrollmentStore( + (s) => [s.setState, s.nextSubject], + shallow, + ); + const codes = useEnrollmentStore((s) => s.recoveryCodes); + + // biome-ignore lint/correctness/useExhaustiveDependencies: rxjs + useEffect(() => { + const sub = navSubject.subscribe((dir) => { + if (dir === EnrollmentNavDirection.NEXT) { + setStore({ step: EnrollmentStepKey.ACTIVATE_USER }); + } + }); + return () => { + sub.unsubscribe(); + }; + }, [navSubject]); + + return ( + +
+ +

Recovery codes

+
+ +
+
    + {codes.map((code) => ( +
  • {code}
  • + ))} +
+
+
+ ); +}; diff --git a/src/pages/enrollment/steps/MfaRecoveryCodesStep/style.scss b/src/pages/enrollment/steps/MfaRecoveryCodesStep/style.scss new file mode 100644 index 00000000..78d373c5 --- /dev/null +++ b/src/pages/enrollment/steps/MfaRecoveryCodesStep/style.scss @@ -0,0 +1,27 @@ +#enrollment-mfa-recovery-codes-step { + width: 100%; + max-width: 650px; + padding: 50px 40px; + display: flex; + flex-flow: column; + gap: var(--spacing-m); + + & > .codes { + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + gap: var(--spacing-s); + + ul { + li { + &:not(:last-child) { + margin-bottom: var(--spacing-xs); + } + + @include typography(app-code); + font-size: 14px; + } + } + } +} diff --git a/src/pages/enrollment/steps/Totp/TotpEnrollmentStep.tsx b/src/pages/enrollment/steps/MfaSetupStep/MfaSetupStep.tsx similarity index 60% rename from src/pages/enrollment/steps/Totp/TotpEnrollmentStep.tsx rename to src/pages/enrollment/steps/MfaSetupStep/MfaSetupStep.tsx index 21f881ff..9c6579f4 100644 --- a/src/pages/enrollment/steps/Totp/TotpEnrollmentStep.tsx +++ b/src/pages/enrollment/steps/MfaSetupStep/MfaSetupStep.tsx @@ -20,6 +20,7 @@ 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'; +import { EnrollmentStepKey } from '../../const'; const formSchema = z.object({ code: z.string().min(6, 'Enter valid code').max(6, 'Enter valid code'), @@ -27,13 +28,12 @@ const formSchema = z.object({ type FormFields = z.infer; -export const TotpEnrollmentStep = () => { +export const MfaSetupStep = () => { 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], + const [userInfo, mfaMethod] = useEnrollmentStore((s) => [s.userInfo, s.mfaMethod]); + const [nextSubject, setStoreState] = useEnrollmentStore( + (s) => [s.nextSubject, s.setState], shallow, ); @@ -42,9 +42,10 @@ export const TotpEnrollmentStep = () => { } = useEnrollmentApi(); const { data: startData, isLoading: startLoading } = useQuery({ - queryFn: () => registerCodeMfaStart(MfaMethod.TOTP), - queryKey: ['enrollment', 'register-mfa', 'start'], + queryFn: () => registerCodeMfaStart(mfaMethod), + queryKey: ['register-mfa', mfaMethod], refetchOnWindowFocus: false, + enabled: isPresent(mfaMethod), }); const { handleSubmit, control, setError } = useForm({ @@ -53,10 +54,13 @@ export const TotpEnrollmentStep = () => { const { mutate, isPending } = useMutation({ mutationFn: registerCodeMfaFinish, - onSuccess: () => { + onSuccess: (response) => { toaster.success('MFA configured'); - setStoreState({ loading: false }); - nextStep(); + setStoreState({ + loading: false, + step: EnrollmentStepKey.MFA_RECOVERY, + recoveryCodes: response.recovery_codes, + }); }, onError: (err) => { setError( @@ -77,7 +81,11 @@ export const TotpEnrollmentStep = () => { const isLoading = startLoading || isPending; const submitHandler: SubmitHandler = (data) => { - mutate(data); + const sendData = { + code: data.code, + method: mfaMethod, + }; + mutate(sendData); }; // biome-ignore lint/correctness/useExhaustiveDependencies: sideEffect @@ -98,47 +106,70 @@ export const TotpEnrollmentStep = () => { return ( +
+ +

Configure MFA

+
{isLoading && (
)} - {!isLoading && ( + {!isLoading && isPresent(userInfo) && ( <> -
- -

Configure MFA

-
- - - {!isLoading && - isPresent(startData) && - isPresent(startData.totp_secret) && - isPresent(userInfo) && ( - <> -
- -
-
+ ); +}; diff --git a/src/pages/enrollment/steps/Totp/style.scss b/src/pages/enrollment/steps/MfaSetupStep/style.scss similarity index 70% rename from src/pages/enrollment/steps/Totp/style.scss rename to src/pages/enrollment/steps/MfaSetupStep/style.scss index 2c4e5563..445745fd 100644 --- a/src/pages/enrollment/steps/Totp/style.scss +++ b/src/pages/enrollment/steps/MfaSetupStep/style.scss @@ -15,17 +15,25 @@ min-height: 25dvh; } - .qr { + .totp-info { display: flex; - flex-flow: row; + flex-flow: column; align-items: center; justify-content: center; - } + row-gap: var(--spacing-s); - .btn { - width: unset; - flex-grow: 0; - width: min-content; + .qr { + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; + } + + .btn { + width: unset; + flex-grow: 0; + width: min-content; + } } form { diff --git a/src/pages/enrollment/steps/PasswordStep/PasswordStep.tsx b/src/pages/enrollment/steps/PasswordStep/PasswordStep.tsx index 1fb5b751..569dd69d 100644 --- a/src/pages/enrollment/steps/PasswordStep/PasswordStep.tsx +++ b/src/pages/enrollment/steps/PasswordStep/PasswordStep.tsx @@ -11,7 +11,9 @@ import { FormInput } from '../../../../shared/defguard-ui/components/Form/FormIn import { Card } from '../../../../shared/defguard-ui/components/Layout/Card/Card'; import { passwordValidator } from '../../../../shared/validators/password'; import { EnrollmentStepIndicator } from '../../components/EnrollmentStepIndicator/EnrollmentStepIndicator'; +import { EnrollmentStepKey } from '../../const'; import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; +import { EnrollmentNavDirection } from '../../hooks/types'; type FormFields = { password: string; @@ -22,10 +24,9 @@ export const PasswordStep = () => { const submitRef = useRef(null); const { LL } = useI18nContext(); - const setStore = useEnrollmentStore((state) => state.setState); const userPassword = useEnrollmentStore((state) => state.userPassword); - const [nextSubject, next] = useEnrollmentStore( - (state) => [state.nextSubject, state.nextStep], + const [nextSubject, setStore] = useEnrollmentStore( + (state) => [state.nextSubject, state.setState], shallow, ); @@ -61,13 +62,20 @@ export const PasswordStep = () => { }); const handleValidSubmit: SubmitHandler = (values) => { - setStore({ userPassword: values.password }); - next(); + setStore({ userPassword: values.password, step: EnrollmentStepKey.DEVICE }); }; + // biome-ignore lint/correctness/useExhaustiveDependencies: rxjs useEffect(() => { - const sub = nextSubject.subscribe(() => { - submitRef.current?.click(); + const sub = nextSubject.subscribe((direction) => { + switch (direction) { + case EnrollmentNavDirection.BACK: + setStore({ step: EnrollmentStepKey.DATA_VERIFICATION }); + break; + case EnrollmentNavDirection.NEXT: + submitRef.current?.click(); + break; + } }); return () => { diff --git a/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx b/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx index dfd67530..64a108f6 100644 --- a/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx +++ b/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx @@ -16,6 +16,7 @@ import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; import { useEnrollmentApi } from '../../hooks/useEnrollmentApi'; import './style.scss'; import useEffectOnce from '../../../../shared/defguard-ui/utils/useEffectOnce'; +import { EnrollmentStepKey } from '../../const'; const { saveConfig } = clientApi; @@ -35,10 +36,7 @@ export const SendFinishStep = () => { const deviceKeys = useEnrollmentStore((s) => s.deviceKeys); const deviceResponse = useEnrollmentStore((s) => s.deviceResponse); - const [setEnrollmentStore, next] = useEnrollmentStore( - (state) => [state.setState, state.nextStep], - shallow, - ); + const [setEnrollmentStore] = useEnrollmentStore((state) => [state.setState], shallow); const queryFn = useCallback( async ( @@ -62,7 +60,6 @@ export const SendFinishStep = () => { queryKey: [key], }); }); - next(); }) .catch((e) => { if (typeof e === 'string') { @@ -84,7 +81,6 @@ export const SendFinishStep = () => { LL.common.messages.errorWithMessage, LL.common.messages.networkError, activateUser, - next, queryClient.invalidateQueries, setEnrollmentStore, toaster.error, @@ -109,8 +105,7 @@ export const SendFinishStep = () => { error(String(e)); }, onSuccess: () => { - setEnrollmentStore({ loading: false }); - next(); + setEnrollmentStore({ loading: false, step: EnrollmentStepKey.FINISH }); }, }); diff --git a/src/pages/enrollment/steps/WelcomeStep/WelcomeStep.tsx b/src/pages/enrollment/steps/WelcomeStep/WelcomeStep.tsx index e2b5305a..694b9acc 100644 --- a/src/pages/enrollment/steps/WelcomeStep/WelcomeStep.tsx +++ b/src/pages/enrollment/steps/WelcomeStep/WelcomeStep.tsx @@ -10,6 +10,7 @@ import { useI18nContext } from '../../../../i18n/i18n-react'; import { Card } from '../../../../shared/defguard-ui/components/Layout/Card/Card'; import { AdminInfo } from '../../components/AdminInfo/AdminInfo'; import { EnrollmentStepIndicator } from '../../components/EnrollmentStepIndicator/EnrollmentStepIndicator'; +import { EnrollmentStepKey } from '../../const'; import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; export const WelcomeStep = () => { @@ -20,8 +21,8 @@ export const WelcomeStep = () => { ]); const userInfo = useEnrollmentStore((state) => state.userInfo); - const [nextSubject, next] = useEnrollmentStore( - (state) => [state.nextSubject, state.nextStep], + const [nextSubject, setStore] = useEnrollmentStore( + (state) => [state.nextSubject, state.setState], shallow, ); @@ -36,14 +37,15 @@ export const WelcomeStep = () => { }); }, [LL.pages.enrollment.steps.welcome, sessionEnd, sessionStart]); + // biome-ignore lint/correctness/useExhaustiveDependencies: rxjs sub useEffect(() => { const sub = nextSubject.subscribe(() => { - next(); + setStore({ step: EnrollmentStepKey.DATA_VERIFICATION }); }); return () => { sub.unsubscribe(); }; - }, [next, nextSubject]); + }, [nextSubject]); return ( diff --git a/src/shared/defguard-ui b/src/shared/defguard-ui index 2d940bbe..4fe9a92e 160000 --- a/src/shared/defguard-ui +++ b/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 2d940bbe208268a327200e95618d7c9a7783894a +Subproject commit 4fe9a92ef6b9cbd00189a92b9a70ab9bb5bfa43f diff --git a/src/shared/hooks/api/types.ts b/src/shared/hooks/api/types.ts index 960a30d6..8e2d54f4 100644 --- a/src/shared/hooks/api/types.ts +++ b/src/shared/hooks/api/types.ts @@ -105,6 +105,7 @@ export type NewApplicationVersionInfo = { export type RegisterCodeMfaFinishRequest = { code: string; + method: MfaMethod; }; export type RegisterCodeMfaStartResponse = { From 679ba12cd13e95c0aa1bdd3852957e801add6c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Thu, 28 Aug 2025 10:11:54 +0200 Subject: [PATCH 11/13] upgrade packages ui --- package.json | 6 +- pnpm-lock.yaml | 285 +++++++++++++++++++++++++------------------------ 2 files changed, 148 insertions(+), 143 deletions(-) diff --git a/package.json b/package.json index 8155e091..12ae2208 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "classnames": "^2.5.1", "clsx": "^2.1.1", "compare-versions": "^6.1.1", - "dayjs": "^1.11.14", + "dayjs": "^1.11.15", "deepmerge-ts": "^7.1.5", "detect-browser": "^5.3.0", "fast-deep-equal": "^3.1.3", @@ -113,9 +113,9 @@ "@types/file-saver": "^2.0.7", "@types/lodash-es": "^4.17.12", "@types/node": "^24.3.0", - "@types/react": "^19.1.11", + "@types/react": "^19.1.12", "@types/react-dom": "^19.1.8", - "@vitejs/plugin-react": "^5.0.1", + "@vitejs/plugin-react": "^5.0.2", "@vitejs/plugin-react-swc": "^4.0.1", "autoprefixer": "^10.4.21", "npm-run-all": "^4.1.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 038bd533..c2ae4156 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,8 +75,8 @@ importers: specifier: ^6.1.1 version: 6.1.1 dayjs: - specifier: ^1.11.14 - version: 1.11.14 + specifier: ^1.11.15 + version: 1.11.15 deepmerge-ts: specifier: ^7.1.5 version: 7.1.5 @@ -94,7 +94,7 @@ importers: version: 1.0.3 html-react-parser: specifier: ^5.2.6 - version: 5.2.6(@types/react@19.1.11)(react@19.1.1) + version: 5.2.6(@types/react@19.1.12)(react@19.1.1) itertools: specifier: ^2.4.1 version: 2.4.1 @@ -106,7 +106,7 @@ importers: version: 4.17.21 merge-refs: specifier: ^2.0.0 - version: 2.0.0(@types/react@19.1.11) + version: 2.0.0(@types/react@19.1.12) millify: specifier: ^6.1.0 version: 6.1.0 @@ -142,7 +142,7 @@ importers: version: 3.5.0(react@19.1.1) react-markdown: specifier: ^10.1.0 - version: 10.1.0(@types/react@19.1.11)(react@19.1.1) + version: 10.1.0(@types/react@19.1.12)(react@19.1.1) react-qr-code: specifier: ^2.0.18 version: 2.0.18(react@19.1.1) @@ -157,7 +157,7 @@ importers: version: 1.0.26(react-dom@19.1.1(react@19.1.1))(react@19.1.1) recharts: specifier: ^3.1.2 - version: 3.1.2(@types/react@19.1.11)(react-dom@19.1.1(react@19.1.1))(react-is@18.3.1)(react@19.1.1)(redux@5.0.1) + version: 3.1.2(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react-is@18.3.1)(react@19.1.1)(redux@5.0.1) rehype-sanitize: specifier: ^6.0.0 version: 6.0.0 @@ -172,14 +172,14 @@ importers: version: 3.25.76 zustand: specifier: ^5.0.8 - version: 5.0.8(@types/react@19.1.11)(immer@10.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) + version: 5.0.8(@types/react@19.1.12)(immer@10.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) devDependencies: '@biomejs/biome': specifier: ^2.2.2 version: 2.2.2 '@hookform/devtools': specifier: ^4.4.0 - version: 4.4.0(@types/react@19.1.11)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 4.4.0(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@svgr/cli': specifier: ^8.1.0 version: 8.1.0(typescript@5.9.2) @@ -202,14 +202,14 @@ importers: specifier: ^24.3.0 version: 24.3.0 '@types/react': - specifier: ^19.1.11 - version: 19.1.11 + specifier: ^19.1.12 + version: 19.1.12 '@types/react-dom': specifier: ^19.1.8 - version: 19.1.8(@types/react@19.1.11) + version: 19.1.8(@types/react@19.1.12) '@vitejs/plugin-react': - specifier: ^5.0.1 - version: 5.0.1(vite@7.1.3(@types/node@24.3.0)(sass@1.70.0)(yaml@2.8.1)) + specifier: ^5.0.2 + version: 5.0.2(vite@7.1.3(@types/node@24.3.0)(sass@1.70.0)(yaml@2.8.1)) '@vitejs/plugin-react-swc': specifier: ^4.0.1 version: 4.0.1(vite@7.1.3(@types/node@24.3.0)(sass@1.70.0)(yaml@2.8.1)) @@ -618,8 +618,8 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@gerrit0/mini-shiki@3.11.0': - resolution: {integrity: sha512-ooCDMAOKv71O7MszbXjSQGcI6K5T6NKlemQZOBHLq7Sv/oXCRfYbZ7UgbzFdl20lSXju6Juds4I3y30R6rHA4Q==} + '@gerrit0/mini-shiki@3.12.0': + resolution: {integrity: sha512-CF1vkfe2ViPtmoFEvtUWilEc4dOCiFzV8+J7/vEISSsslKQ97FjeTPNMCqUhZEiKySmKRgK3UO/CxtkyOp7DvA==} '@hookform/devtools@4.4.0': resolution: {integrity: sha512-Mtlic+uigoYBPXlfvPBfiYYUZuyMrD3pTjDpVIhL6eCZTvQkHsKBSKeZCvXWUZr8fqrkzDg27N+ZuazLKq6Vmg==} @@ -678,103 +678,106 @@ packages: '@rolldown/pluginutils@1.0.0-beta.32': resolution: {integrity: sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g==} - '@rollup/rollup-android-arm-eabi@4.48.1': - resolution: {integrity: sha512-rGmb8qoG/zdmKoYELCBwu7vt+9HxZ7Koos3pD0+sH5fR3u3Wb/jGcpnqxcnWsPEKDUyzeLSqksN8LJtgXjqBYw==} + '@rolldown/pluginutils@1.0.0-beta.34': + resolution: {integrity: sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA==} + + '@rollup/rollup-android-arm-eabi@4.49.0': + resolution: {integrity: sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.48.1': - resolution: {integrity: sha512-4e9WtTxrk3gu1DFE+imNJr4WsL13nWbD/Y6wQcyku5qadlKHY3OQ3LJ/INrrjngv2BJIHnIzbqMk1GTAC2P8yQ==} + '@rollup/rollup-android-arm64@4.49.0': + resolution: {integrity: sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.48.1': - resolution: {integrity: sha512-+XjmyChHfc4TSs6WUQGmVf7Hkg8ferMAE2aNYYWjiLzAS/T62uOsdfnqv+GHRjq7rKRnYh4mwWb4Hz7h/alp8A==} + '@rollup/rollup-darwin-arm64@4.49.0': + resolution: {integrity: sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.48.1': - resolution: {integrity: sha512-upGEY7Ftw8M6BAJyGwnwMw91rSqXTcOKZnnveKrVWsMTF8/k5mleKSuh7D4v4IV1pLxKAk3Tbs0Lo9qYmii5mQ==} + '@rollup/rollup-darwin-x64@4.49.0': + resolution: {integrity: sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.48.1': - resolution: {integrity: sha512-P9ViWakdoynYFUOZhqq97vBrhuvRLAbN/p2tAVJvhLb8SvN7rbBnJQcBu8e/rQts42pXGLVhfsAP0k9KXWa3nQ==} + '@rollup/rollup-freebsd-arm64@4.49.0': + resolution: {integrity: sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.48.1': - resolution: {integrity: sha512-VLKIwIpnBya5/saccM8JshpbxfyJt0Dsli0PjXozHwbSVaHTvWXJH1bbCwPXxnMzU4zVEfgD1HpW3VQHomi2AQ==} + '@rollup/rollup-freebsd-x64@4.49.0': + resolution: {integrity: sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.48.1': - resolution: {integrity: sha512-3zEuZsXfKaw8n/yF7t8N6NNdhyFw3s8xJTqjbTDXlipwrEHo4GtIKcMJr5Ed29leLpB9AugtAQpAHW0jvtKKaQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.49.0': + resolution: {integrity: sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.48.1': - resolution: {integrity: sha512-leo9tOIlKrcBmmEypzunV/2w946JeLbTdDlwEZ7OnnsUyelZ72NMnT4B2vsikSgwQifjnJUbdXzuW4ToN1wV+Q==} + '@rollup/rollup-linux-arm-musleabihf@4.49.0': + resolution: {integrity: sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.48.1': - resolution: {integrity: sha512-Vy/WS4z4jEyvnJm+CnPfExIv5sSKqZrUr98h03hpAMbE2aI0aD2wvK6GiSe8Gx2wGp3eD81cYDpLLBqNb2ydwQ==} + '@rollup/rollup-linux-arm64-gnu@4.49.0': + resolution: {integrity: sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.48.1': - resolution: {integrity: sha512-x5Kzn7XTwIssU9UYqWDB9VpLpfHYuXw5c6bJr4Mzv9kIv242vmJHbI5PJJEnmBYitUIfoMCODDhR7KoZLot2VQ==} + '@rollup/rollup-linux-arm64-musl@4.49.0': + resolution: {integrity: sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.48.1': - resolution: {integrity: sha512-yzCaBbwkkWt/EcgJOKDUdUpMHjhiZT/eDktOPWvSRpqrVE04p0Nd6EGV4/g7MARXXeOqstflqsKuXVM3H9wOIQ==} + '@rollup/rollup-linux-loongarch64-gnu@4.49.0': + resolution: {integrity: sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.48.1': - resolution: {integrity: sha512-UK0WzWUjMAJccHIeOpPhPcKBqax7QFg47hwZTp6kiMhQHeOYJeaMwzeRZe1q5IiTKsaLnHu9s6toSYVUlZ2QtQ==} + '@rollup/rollup-linux-ppc64-gnu@4.49.0': + resolution: {integrity: sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.48.1': - resolution: {integrity: sha512-3NADEIlt+aCdCbWVZ7D3tBjBX1lHpXxcvrLt/kdXTiBrOds8APTdtk2yRL2GgmnSVeX4YS1JIf0imFujg78vpw==} + '@rollup/rollup-linux-riscv64-gnu@4.49.0': + resolution: {integrity: sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.48.1': - resolution: {integrity: sha512-euuwm/QTXAMOcyiFCcrx0/S2jGvFlKJ2Iro8rsmYL53dlblp3LkUQVFzEidHhvIPPvcIsxDhl2wkBE+I6YVGzA==} + '@rollup/rollup-linux-riscv64-musl@4.49.0': + resolution: {integrity: sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.48.1': - resolution: {integrity: sha512-w8mULUjmPdWLJgmTYJx/W6Qhln1a+yqvgwmGXcQl2vFBkWsKGUBRbtLRuKJUln8Uaimf07zgJNxOhHOvjSQmBQ==} + '@rollup/rollup-linux-s390x-gnu@4.49.0': + resolution: {integrity: sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.48.1': - resolution: {integrity: sha512-90taWXCWxTbClWuMZD0DKYohY1EovA+W5iytpE89oUPmT5O1HFdf8cuuVIylE6vCbrGdIGv85lVRzTcpTRZ+kA==} + '@rollup/rollup-linux-x64-gnu@4.49.0': + resolution: {integrity: sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.48.1': - resolution: {integrity: sha512-2Gu29SkFh1FfTRuN1GR1afMuND2GKzlORQUP3mNMJbqdndOg7gNsa81JnORctazHRokiDzQ5+MLE5XYmZW5VWg==} + '@rollup/rollup-linux-x64-musl@4.49.0': + resolution: {integrity: sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.48.1': - resolution: {integrity: sha512-6kQFR1WuAO50bxkIlAVeIYsz3RUx+xymwhTo9j94dJ+kmHe9ly7muH23sdfWduD0BA8pD9/yhonUvAjxGh34jQ==} + '@rollup/rollup-win32-arm64-msvc@4.49.0': + resolution: {integrity: sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.48.1': - resolution: {integrity: sha512-RUyZZ/mga88lMI3RlXFs4WQ7n3VyU07sPXmMG7/C1NOi8qisUg57Y7LRarqoGoAiopmGmChUhSwfpvQ3H5iGSQ==} + '@rollup/rollup-win32-ia32-msvc@4.49.0': + resolution: {integrity: sha512-gq5aW/SyNpjp71AAzroH37DtINDcX1Qw2iv9Chyz49ZgdOP3NV8QCyKZUrGsYX9Yyggj5soFiRCgsL3HwD8TdA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.48.1': - resolution: {integrity: sha512-8a/caCUN4vkTChxkaIJcMtwIVcBhi4X2PQRoT+yCK3qRYaZ7cURrmJFL5Ux9H9RaMIXj9RuihckdmkBX3zZsgg==} + '@rollup/rollup-win32-x64-msvc@4.49.0': + resolution: {integrity: sha512-gEtqFbzmZLFk2xKh7g0Rlo8xzho8KrEFEkzvHbfUGkrgXOpZ4XagQ6n+wIZFNh1nTb8UD16J4nFSFKXYgnbdBg==} cpu: [x64] os: [win32] @@ -1191,8 +1194,8 @@ packages: peerDependencies: '@types/react': ^19.0.0 - '@types/react@19.1.11': - resolution: {integrity: sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==} + '@types/react@19.1.12': + resolution: {integrity: sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==} '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1220,8 +1223,8 @@ packages: peerDependencies: vite: ^4 || ^5 || ^6 || ^7 - '@vitejs/plugin-react@5.0.1': - resolution: {integrity: sha512-DE4UNaBXwtVoDJ0ccBdLVjFTWL70NRuWNCxEieTI3lrq9ORB9aOCQEKstwDXBl87NvFdbqh/p7eINGyj0BthJA==} + '@vitejs/plugin-react@5.0.2': + resolution: {integrity: sha512-tmyFgixPZCx2+e6VO9TNITWcCQl8+Nl/E8YbAyPVv85QCc7/A3JrdfG2A8gIzvVhWuzMOVrFW1aReaNxrI6tbw==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -1505,8 +1508,8 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - dayjs@1.11.14: - resolution: {integrity: sha512-E8fIdSxUlyqSA8XYGnNa3IkIzxtEmFjI+JU/6ic0P1zmSqyL6HyG5jHnpPjRguDNiaHLpfvHKWFiohNsJLqcJQ==} + dayjs@1.11.15: + resolution: {integrity: sha512-MC+DfnSWiM9APs7fpiurHGCoeIx0Gdl6QZBy+5lu8MbYKN5FZEXqOgrundfibdfhGZ15o9hzmZ2xJjZnbvgKXQ==} debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} @@ -1569,8 +1572,8 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.209: - resolution: {integrity: sha512-Xoz0uMrim9ZETCQt8UgM5FxQF9+imA7PBpokoGcZloA1uw2LeHzTlip5cb5KOAsXZLjh/moN2vReN3ZjJmjI9A==} + electron-to-chromium@1.5.211: + resolution: {integrity: sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2488,8 +2491,8 @@ packages: engines: {node: '>= 0.4'} hasBin: true - rollup@4.48.1: - resolution: {integrity: sha512-jVG20NvbhTYDkGAty2/Yh7HK6/q3DGSRH4o8ALKGArmMuaauM9kLfoMZ+WliPwA5+JHr2lTn3g557FxBV87ifg==} + rollup@4.49.0: + resolution: {integrity: sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -3081,7 +3084,7 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.1.11)(react@19.1.1)': + '@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1)': dependencies: '@babel/runtime': 7.28.3 '@emotion/babel-plugin': 11.13.5 @@ -3093,7 +3096,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 19.1.1 optionalDependencies: - '@types/react': 19.1.11 + '@types/react': 19.1.12 transitivePeerDependencies: - supports-color @@ -3107,18 +3110,18 @@ snapshots: '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.11)(react@19.1.1))(@types/react@19.1.11)(react@19.1.1)': + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1)': dependencies: '@babel/runtime': 7.28.3 '@emotion/babel-plugin': 11.13.5 '@emotion/is-prop-valid': 1.3.1 - '@emotion/react': 11.14.0(@types/react@19.1.11)(react@19.1.1) + '@emotion/react': 11.14.0(@types/react@19.1.12)(react@19.1.1) '@emotion/serialize': 1.3.3 '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.1) '@emotion/utils': 1.4.2 react: 19.1.1 optionalDependencies: - '@types/react': 19.1.11 + '@types/react': 19.1.12 transitivePeerDependencies: - supports-color @@ -3235,7 +3238,7 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@gerrit0/mini-shiki@3.11.0': + '@gerrit0/mini-shiki@3.12.0': dependencies: '@shikijs/engine-oniguruma': 3.12.0 '@shikijs/langs': 3.12.0 @@ -3243,10 +3246,10 @@ snapshots: '@shikijs/types': 3.12.0 '@shikijs/vscode-textmate': 10.0.2 - '@hookform/devtools@4.4.0(@types/react@19.1.11)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@hookform/devtools@4.4.0(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@emotion/react': 11.14.0(@types/react@19.1.11)(react@19.1.1) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.11)(react@19.1.1))(@types/react@19.1.11)(react@19.1.1) + '@emotion/react': 11.14.0(@types/react@19.1.12)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) '@types/lodash': 4.17.20 little-state-machine: 4.8.1(react@19.1.1) lodash: 4.17.21 @@ -3291,7 +3294,7 @@ snapshots: '@react-hook/passive-layout-effect': 1.2.1(react@19.1.1) react: 19.1.1 - '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@19.1.11)(react@19.1.1)(redux@5.0.1))(react@19.1.1)': + '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@19.1.12)(react@19.1.1)(redux@5.0.1))(react@19.1.1)': dependencies: '@standard-schema/spec': 1.0.0 '@standard-schema/utils': 0.3.0 @@ -3301,70 +3304,72 @@ snapshots: reselect: 5.1.1 optionalDependencies: react: 19.1.1 - react-redux: 9.2.0(@types/react@19.1.11)(react@19.1.1)(redux@5.0.1) + react-redux: 9.2.0(@types/react@19.1.12)(react@19.1.1)(redux@5.0.1) '@remix-run/router@1.23.0': {} '@rolldown/pluginutils@1.0.0-beta.32': {} - '@rollup/rollup-android-arm-eabi@4.48.1': + '@rolldown/pluginutils@1.0.0-beta.34': {} + + '@rollup/rollup-android-arm-eabi@4.49.0': optional: true - '@rollup/rollup-android-arm64@4.48.1': + '@rollup/rollup-android-arm64@4.49.0': optional: true - '@rollup/rollup-darwin-arm64@4.48.1': + '@rollup/rollup-darwin-arm64@4.49.0': optional: true - '@rollup/rollup-darwin-x64@4.48.1': + '@rollup/rollup-darwin-x64@4.49.0': optional: true - '@rollup/rollup-freebsd-arm64@4.48.1': + '@rollup/rollup-freebsd-arm64@4.49.0': optional: true - '@rollup/rollup-freebsd-x64@4.48.1': + '@rollup/rollup-freebsd-x64@4.49.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.48.1': + '@rollup/rollup-linux-arm-gnueabihf@4.49.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.48.1': + '@rollup/rollup-linux-arm-musleabihf@4.49.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.48.1': + '@rollup/rollup-linux-arm64-gnu@4.49.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.48.1': + '@rollup/rollup-linux-arm64-musl@4.49.0': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.48.1': + '@rollup/rollup-linux-loongarch64-gnu@4.49.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.48.1': + '@rollup/rollup-linux-ppc64-gnu@4.49.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.48.1': + '@rollup/rollup-linux-riscv64-gnu@4.49.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.48.1': + '@rollup/rollup-linux-riscv64-musl@4.49.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.48.1': + '@rollup/rollup-linux-s390x-gnu@4.49.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.48.1': + '@rollup/rollup-linux-x64-gnu@4.49.0': optional: true - '@rollup/rollup-linux-x64-musl@4.48.1': + '@rollup/rollup-linux-x64-musl@4.49.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.48.1': + '@rollup/rollup-win32-arm64-msvc@4.49.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.48.1': + '@rollup/rollup-win32-ia32-msvc@4.49.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.48.1': + '@rollup/rollup-win32-x64-msvc@4.49.0': optional: true '@shikijs/engine-oniguruma@3.12.0': @@ -3758,11 +3763,11 @@ snapshots: '@types/parse-json@4.0.2': {} - '@types/react-dom@19.1.8(@types/react@19.1.11)': + '@types/react-dom@19.1.8(@types/react@19.1.12)': dependencies: - '@types/react': 19.1.11 + '@types/react': 19.1.12 - '@types/react@19.1.11': + '@types/react@19.1.12': dependencies: csstype: 3.1.3 @@ -3789,12 +3794,12 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@5.0.1(vite@7.1.3(@types/node@24.3.0)(sass@1.70.0)(yaml@2.8.1))': + '@vitejs/plugin-react@5.0.2(vite@7.1.3(@types/node@24.3.0)(sass@1.70.0)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.3 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.3) '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.3) - '@rolldown/pluginutils': 1.0.0-beta.32 + '@rolldown/pluginutils': 1.0.0-beta.34 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 vite: 7.1.3(@types/node@24.3.0)(sass@1.70.0)(yaml@2.8.1) @@ -3879,7 +3884,7 @@ snapshots: browserslist@4.25.3: dependencies: caniuse-lite: 1.0.30001737 - electron-to-chromium: 1.5.209 + electron-to-chromium: 1.5.211 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.3) @@ -4086,7 +4091,7 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - dayjs@1.11.14: {} + dayjs@1.11.15: {} debug@4.4.1: dependencies: @@ -4151,7 +4156,7 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.209: {} + electron-to-chromium@1.5.211: {} emoji-regex@8.0.0: {} @@ -4448,7 +4453,7 @@ snapshots: domhandler: 5.0.3 htmlparser2: 10.0.0 - html-react-parser@5.2.6(@types/react@19.1.11)(react@19.1.1): + html-react-parser@5.2.6(@types/react@19.1.12)(react@19.1.1): dependencies: domhandler: 5.0.3 html-dom-parser: 5.1.1 @@ -4456,7 +4461,7 @@ snapshots: react-property: 2.0.2 style-to-js: 1.1.17 optionalDependencies: - '@types/react': 19.1.11 + '@types/react': 19.1.12 html-url-attributes@3.0.1: {} @@ -4788,9 +4793,9 @@ snapshots: memorystream@0.3.1: {} - merge-refs@2.0.0(@types/react@19.1.11): + merge-refs@2.0.0(@types/react@19.1.12): optionalDependencies: - '@types/react': 19.1.11 + '@types/react': 19.1.12 micromark-core-commonmark@2.0.3: dependencies: @@ -5123,11 +5128,11 @@ snapshots: dependencies: react: 19.1.1 - react-markdown@10.1.0(@types/react@19.1.11)(react@19.1.1): + react-markdown@10.1.0(@types/react@19.1.12)(react@19.1.1): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 19.1.11 + '@types/react': 19.1.12 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 @@ -5149,13 +5154,13 @@ snapshots: qr.js: 0.0.0 react: 19.1.1 - react-redux@9.2.0(@types/react@19.1.11)(react@19.1.1)(redux@5.0.1): + react-redux@9.2.0(@types/react@19.1.12)(react@19.1.1)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 react: 19.1.1 use-sync-external-store: 1.5.0(react@19.1.1) optionalDependencies: - '@types/react': 19.1.11 + '@types/react': 19.1.12 redux: 5.0.1 react-refresh@0.17.0: {} @@ -5195,9 +5200,9 @@ snapshots: dependencies: picomatch: 2.3.1 - recharts@3.1.2(@types/react@19.1.11)(react-dom@19.1.1(react@19.1.1))(react-is@18.3.1)(react@19.1.1)(redux@5.0.1): + recharts@3.1.2(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react-is@18.3.1)(react@19.1.1)(redux@5.0.1): dependencies: - '@reduxjs/toolkit': 2.8.2(react-redux@9.2.0(@types/react@19.1.11)(react@19.1.1)(redux@5.0.1))(react@19.1.1) + '@reduxjs/toolkit': 2.8.2(react-redux@9.2.0(@types/react@19.1.12)(react@19.1.1)(redux@5.0.1))(react@19.1.1) clsx: 2.1.1 decimal.js-light: 2.5.1 es-toolkit: 1.39.10 @@ -5206,7 +5211,7 @@ snapshots: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) react-is: 18.3.1 - react-redux: 9.2.0(@types/react@19.1.11)(react@19.1.1)(redux@5.0.1) + react-redux: 9.2.0(@types/react@19.1.12)(react@19.1.1)(redux@5.0.1) reselect: 5.1.1 tiny-invariant: 1.3.3 use-sync-external-store: 1.5.0(react@19.1.1) @@ -5275,30 +5280,30 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rollup@4.48.1: + rollup@4.49.0: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.48.1 - '@rollup/rollup-android-arm64': 4.48.1 - '@rollup/rollup-darwin-arm64': 4.48.1 - '@rollup/rollup-darwin-x64': 4.48.1 - '@rollup/rollup-freebsd-arm64': 4.48.1 - '@rollup/rollup-freebsd-x64': 4.48.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.48.1 - '@rollup/rollup-linux-arm-musleabihf': 4.48.1 - '@rollup/rollup-linux-arm64-gnu': 4.48.1 - '@rollup/rollup-linux-arm64-musl': 4.48.1 - '@rollup/rollup-linux-loongarch64-gnu': 4.48.1 - '@rollup/rollup-linux-ppc64-gnu': 4.48.1 - '@rollup/rollup-linux-riscv64-gnu': 4.48.1 - '@rollup/rollup-linux-riscv64-musl': 4.48.1 - '@rollup/rollup-linux-s390x-gnu': 4.48.1 - '@rollup/rollup-linux-x64-gnu': 4.48.1 - '@rollup/rollup-linux-x64-musl': 4.48.1 - '@rollup/rollup-win32-arm64-msvc': 4.48.1 - '@rollup/rollup-win32-ia32-msvc': 4.48.1 - '@rollup/rollup-win32-x64-msvc': 4.48.1 + '@rollup/rollup-android-arm-eabi': 4.49.0 + '@rollup/rollup-android-arm64': 4.49.0 + '@rollup/rollup-darwin-arm64': 4.49.0 + '@rollup/rollup-darwin-x64': 4.49.0 + '@rollup/rollup-freebsd-arm64': 4.49.0 + '@rollup/rollup-freebsd-x64': 4.49.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.49.0 + '@rollup/rollup-linux-arm-musleabihf': 4.49.0 + '@rollup/rollup-linux-arm64-gnu': 4.49.0 + '@rollup/rollup-linux-arm64-musl': 4.49.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.49.0 + '@rollup/rollup-linux-ppc64-gnu': 4.49.0 + '@rollup/rollup-linux-riscv64-gnu': 4.49.0 + '@rollup/rollup-linux-riscv64-musl': 4.49.0 + '@rollup/rollup-linux-s390x-gnu': 4.49.0 + '@rollup/rollup-linux-x64-gnu': 4.49.0 + '@rollup/rollup-linux-x64-musl': 4.49.0 + '@rollup/rollup-win32-arm64-msvc': 4.49.0 + '@rollup/rollup-win32-ia32-msvc': 4.49.0 + '@rollup/rollup-win32-x64-msvc': 4.49.0 fsevents: 2.3.3 rxjs@7.8.2: @@ -5557,7 +5562,7 @@ snapshots: typedoc@0.28.11(typescript@5.9.2): dependencies: - '@gerrit0/mini-shiki': 3.11.0 + '@gerrit0/mini-shiki': 3.12.0 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 @@ -5675,7 +5680,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.48.1 + rollup: 4.49.0 tinyglobby: 0.2.14 optionalDependencies: '@types/node': 24.3.0 @@ -5758,9 +5763,9 @@ snapshots: zod@3.25.76: {} - zustand@5.0.8(@types/react@19.1.11)(immer@10.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)): + zustand@5.0.8(@types/react@19.1.12)(immer@10.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)): optionalDependencies: - '@types/react': 19.1.11 + '@types/react': 19.1.12 immer: 10.1.1 react: 19.1.1 use-sync-external-store: 1.5.0(react@19.1.1) From b6c21837b57c3436d69f7845e14d19c0d719e952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Thu, 28 Aug 2025 10:16:51 +0200 Subject: [PATCH 12/13] fix after merge --- package.json | 1 + pnpm-lock.yaml | 10 ++ .../steps/Totp/TotpEnrollmentStep.tsx | 144 ------------------ src/pages/enrollment/steps/Totp/style.scss | 46 ------ 4 files changed, 11 insertions(+), 190 deletions(-) delete mode 100644 src/pages/enrollment/steps/Totp/TotpEnrollmentStep.tsx delete mode 100644 src/pages/enrollment/steps/Totp/style.scss diff --git a/package.json b/package.json index 12ae2208..09d80de6 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@tauri-apps/plugin-http": "^2.5.2", "@tauri-apps/plugin-log": "^2.6.0", "@tauri-apps/plugin-notification": "^2.3.1", + "@tauri-apps/plugin-opener": "^2.5.0", "@tauri-apps/plugin-window-state": "^2.4.0", "@types/byte-size": "^8.1.2", "@use-gesture/react": "^10.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2ae4156..d295a28f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: '@tauri-apps/plugin-notification': specifier: ^2.3.1 version: 2.3.1 + '@tauri-apps/plugin-opener': + specifier: ^2.5.0 + version: 2.5.0 '@tauri-apps/plugin-window-state': specifier: ^2.4.0 version: 2.4.0 @@ -1107,6 +1110,9 @@ packages: '@tauri-apps/plugin-notification@2.3.1': resolution: {integrity: sha512-7gqgfANSREKhh35fY1L4j3TUjUdePmU735FYDqRGeIf8nMXWpcx6j4FhN9/4nYz+m0mv79DCTPLqIPTySggGgg==} + '@tauri-apps/plugin-opener@2.5.0': + resolution: {integrity: sha512-B0LShOYae4CZjN8leiNDbnfjSrTwoZakqKaWpfoH6nXiJwt6Rgj6RnVIffG3DoJiKsffRhMkjmBV9VeilSb4TA==} + '@tauri-apps/plugin-window-state@2.4.0': resolution: {integrity: sha512-hRSzPNi2NG0lPFthfVY0V5C1MyWN/gGaQtQYw7i9zZhLzrhZveHZ2omHG1rIiIsjfTGbO7fhjydSoeTTK9GqLw==} @@ -3676,6 +3682,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.8.0 + '@tauri-apps/plugin-opener@2.5.0': + dependencies: + '@tauri-apps/api': 2.8.0 + '@tauri-apps/plugin-window-state@2.4.0': dependencies: '@tauri-apps/api': 2.8.0 diff --git a/src/pages/enrollment/steps/Totp/TotpEnrollmentStep.tsx b/src/pages/enrollment/steps/Totp/TotpEnrollmentStep.tsx deleted file mode 100644 index 21f881ff..00000000 --- a/src/pages/enrollment/steps/Totp/TotpEnrollmentStep.tsx +++ /dev/null @@ -1,144 +0,0 @@ -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'; -import { EnrollmentStepIndicator } from '../../components/EnrollmentStepIndicator/EnrollmentStepIndicator'; - -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) && ( - <> -
- -
-