diff --git a/src-tauri/proto b/src-tauri/proto index 4688d4d5..fa9c14ef 160000 --- a/src-tauri/proto +++ b/src-tauri/proto @@ -1 +1 @@ -Subproject commit 4688d4d587246e09d800c03963721b437f3a3eca +Subproject commit fa9c14efd121182ec39c8716370e1250c77fa652 diff --git a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceInitForm/AddInstanceInitForm.tsx b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceInitForm/AddInstanceInitForm.tsx index 01ec4d22..65d98146 100644 --- a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceInitForm/AddInstanceInitForm.tsx +++ b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceInitForm/AddInstanceInitForm.tsx @@ -139,10 +139,12 @@ export const AddInstanceInitForm = ({ nextStep }: Props) => { ); } debug('Response received with status OK'); - const r = (await res.json()) as EnrollmentStartResponse; + const startResponse = (await res.json()) as EnrollmentStartResponse; // get client registered instances const clientInstances = await clientApi.getInstances(); - const instance = clientInstances.find((i) => i.uuid === r.instance.id); + const instance = clientInstances.find( + (i) => i.uuid === startResponse.instance.id, + ); let proxy_api_url = values.url; if (proxy_api_url[proxy_api_url.length - 1] === '/') { proxy_api_url = proxy_api_url.slice(0, -1); @@ -191,24 +193,29 @@ export const AddInstanceInitForm = ({ nextStep }: Props) => { } // register new instance // is user in need of full enrollment ? - if (r.user.enrolled) { + if (startResponse.user.enrolled) { //no, only create new device for desktop client debug('User already active, adding device only.'); nextStep({ url: proxy_api_url, cookie: authCookie, - device_names: r.user.device_names, + device_names: startResponse.user.device_names, }); } else { // yes, enroll the user debug('User is not active. Starting enrollment.'); - const sessionEnd = dayjs.unix(r.deadline_timestamp).utc().local().format(); + const sessionEnd = dayjs + .unix(startResponse.deadline_timestamp) + .utc() + .local() + .format(); const sessionStart = dayjs().local().format(); initEnrollment({ - userInfo: r.user, - adminInfo: r.admin, - endContent: r.final_page_content, + userInfo: startResponse.user, + adminInfo: startResponse.admin, + endContent: startResponse.final_page_content, proxy_url: proxy_api_url, + enrollmentSettings: startResponse.settings, sessionEnd, sessionStart, cookie: authCookie, diff --git a/src/pages/enrollment/EnrollmentPage.tsx b/src/pages/enrollment/EnrollmentPage.tsx index d0cc4a5a..64c472cb 100644 --- a/src/pages/enrollment/EnrollmentPage.tsx +++ b/src/pages/enrollment/EnrollmentPage.tsx @@ -75,10 +75,6 @@ export const EnrollmentPage = () => { enrollmentFinished.current = currentStep === EnrollmentStepKey.FINISH; }, [currentStep]); - useEffect(() => { - console.log(currentStepConfig); - }, [currentStepConfig]); - return ( diff --git a/src/pages/enrollment/components/EnrollmentSideBar/EnrollmentSideBar.tsx b/src/pages/enrollment/components/EnrollmentSideBar/EnrollmentSideBar.tsx index d16f9cdc..962551fb 100644 --- a/src/pages/enrollment/components/EnrollmentSideBar/EnrollmentSideBar.tsx +++ b/src/pages/enrollment/components/EnrollmentSideBar/EnrollmentSideBar.tsx @@ -22,6 +22,8 @@ type SideBarItem = { export const EnrollmentSideBar = () => { const { LL } = useI18nContext(); + const enrollmentSettings = useEnrollmentStore((s) => s.enrollmentSettings); + const vpnOptional = useEnrollmentStore((state) => state.vpnOptional); const currentStep = useEnrollmentStore((state) => state.step); @@ -41,7 +43,7 @@ export const EnrollmentSideBar = () => { case EnrollmentStepKey.DEVICE: return `${stepsLL.vpn()}${vpnOptional ? '*' : ''}`; case EnrollmentStepKey.MFA: - return stepsLL.mfa(); + return `${stepsLL.mfa()}${enrollmentSettings.mfa_required ? '' : '*'}`; case EnrollmentStepKey.MFA_CHOICE: return stepsLL.mfaChoice(); case EnrollmentStepKey.MFA_SETUP: @@ -56,7 +58,7 @@ export const EnrollmentSideBar = () => { return ''; } }, - [LL.pages.enrollment.sideBar.steps, vpnOptional], + [LL.pages.enrollment.sideBar.steps, vpnOptional, enrollmentSettings.mfa_required], ); const stepsData = useMemo( diff --git a/src/pages/enrollment/hooks/store/useEnrollmentStore.tsx b/src/pages/enrollment/hooks/store/useEnrollmentStore.tsx index 20eb2a6e..ff404086 100644 --- a/src/pages/enrollment/hooks/store/useEnrollmentStore.tsx +++ b/src/pages/enrollment/hooks/store/useEnrollmentStore.tsx @@ -1,3 +1,4 @@ +import type { Dayjs } from 'dayjs'; import { pick } from 'lodash-es'; import { Subject } from 'rxjs'; import { createJSONStorage, persist } from 'zustand/middleware'; @@ -5,6 +6,7 @@ import { createWithEqualityFn } from 'zustand/traditional'; import type { AdminInfo, CreateDeviceResponse, + EnrollmentSettings, UserInfo, } from '../../../../shared/hooks/api/types'; import { MfaMethod } from '../../../../shared/types'; @@ -15,6 +17,14 @@ const defaultValues: StoreValues = { // assume default dev proxy_url: '/api/v1/', loading: false, + enrollmentSettings: { + admin_device_management: false, + mfa_required: false, + only_client_activation: false, + smtp_configured: false, + vpn_setup_optional: true, + }, + emailResendTimestamp: undefined, step: EnrollmentStepKey.WELCOME, mfaMethod: MfaMethod.TOTP, recoveryCodes: [], @@ -46,6 +56,7 @@ const persistKeys: Array = [ 'deviceKeys', 'deviceResponse', 'cookie', + 'enrollmentSettings', ]; export const useEnrollmentStore = createWithEqualityFn()( @@ -75,10 +86,11 @@ export const useEnrollmentStore = createWithEqualityFn()( type Store = StoreValues & StoreMethods; type StoreValues = { - // next and back are disabled loading: boolean; + enrollmentSettings: EnrollmentSettings; step: EnrollmentStepKey; mfaMethod: MfaMethod; + emailResendTimestamp?: Dayjs; nextSubject: Subject; // Date proxy_url: string; diff --git a/src/pages/enrollment/steps/ChooseMfaStep/ChooseMfaStep.tsx b/src/pages/enrollment/steps/ChooseMfaStep/ChooseMfaStep.tsx index aeccf33b..21bbb261 100644 --- a/src/pages/enrollment/steps/ChooseMfaStep/ChooseMfaStep.tsx +++ b/src/pages/enrollment/steps/ChooseMfaStep/ChooseMfaStep.tsx @@ -18,6 +18,7 @@ import { EnrollmentNavDirection } from '../../hooks/types'; export const ChooseMfaStep = () => { const selectedMethod = useEnrollmentStore((s) => s.mfaMethod); + const enrollmentSettings = useEnrollmentStore((s) => s.enrollmentSettings); const [setStore, navSubject] = useEnrollmentStore( (s) => [s.setState, s.nextSubject], shallow, @@ -55,6 +56,7 @@ export const ChooseMfaStep = () => { }} /> { @@ -63,17 +65,19 @@ export const ChooseMfaStep = () => { />
-
+ ); +}; diff --git a/src/pages/enrollment/steps/MfaSetupStep/MfaSetupStep.tsx b/src/pages/enrollment/steps/MfaSetupStep/MfaSetupStep.tsx index 084d07a6..10f826c8 100644 --- a/src/pages/enrollment/steps/MfaSetupStep/MfaSetupStep.tsx +++ b/src/pages/enrollment/steps/MfaSetupStep/MfaSetupStep.tsx @@ -1,27 +1,25 @@ +import './style.scss'; + import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQuery } from '@tanstack/react-query'; -import { useEffect, useRef } from 'react'; +import { error } from '@tauri-apps/plugin-log'; +import { type Ref, 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 { useToaster } from '../../../../shared/defguard-ui/hooks/toasts/useToaster'; 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'; import { EnrollmentStepKey } from '../../const'; +import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore'; import { EnrollmentNavDirection } from '../../hooks/types'; +import { useEnrollmentApi } from '../../hooks/useEnrollmentApi'; +import { MfaSetupEmail } from './MfaSetupEmail'; +import { MfaSetupTotp } from './MfaSetupTotp'; const formSchema = z.object({ code: z.string().trim().min(6, 'Enter valid code').max(6, 'Enter valid code'), @@ -30,7 +28,6 @@ const formSchema = z.object({ type FormFields = z.infer; export const MfaSetupStep = () => { - const toaster = useToaster(); const submitRef = useRef(null); const [userInfo, mfaMethod] = useEnrollmentStore((s) => [s.userInfo, s.mfaMethod]); const [nextSubject, setStoreState] = useEnrollmentStore( @@ -39,16 +36,78 @@ export const MfaSetupStep = () => { ); const { - enrollment: { registerCodeMfaFinish, registerCodeMfaStart }, + enrollment: { registerCodeMfaStart }, } = useEnrollmentApi(); - const { data: startData, isLoading: startLoading } = useQuery({ + const { + data: startData, + isLoading: startLoading, + refetch, + } = useQuery({ queryFn: () => registerCodeMfaStart(mfaMethod), queryKey: ['register-mfa', mfaMethod], refetchOnWindowFocus: false, enabled: isPresent(mfaMethod), }); + // biome-ignore lint/correctness/useExhaustiveDependencies: rxjs sub + useEffect(() => { + const sub = nextSubject.subscribe((direction) => { + switch (direction) { + case EnrollmentNavDirection.NEXT: + submitRef.current?.click(); + break; + case EnrollmentNavDirection.BACK: + setStoreState({ step: EnrollmentStepKey.MFA_CHOICE }); + break; + } + }); + return () => { + sub.unsubscribe(); + }; + }, [nextSubject]); + + return ( + +
+ +

Configure MFA

+
+ {startLoading && ( +
+ +
+ )} + {!startLoading && isPresent(userInfo) && ( + <> + {mfaMethod === MfaMethod.TOTP && isPresent(startData?.totp_secret) && ( + + + + )} + {mfaMethod === MfaMethod.EMAIL && ( + + + + )} + + )} +
+ ); +}; + +type CodeFormProps = { + inputRef: Ref; +}; + +const CodeForm = ({ inputRef }: CodeFormProps) => { + const toaster = useToaster(); + const mfaMethod = useEnrollmentStore((s) => s.mfaMethod); + const setStoreState = useEnrollmentStore((s) => s.setState); + const { + enrollment: { registerCodeMfaFinish }, + } = useEnrollmentApi(); + const { handleSubmit, control, setError } = useForm({ resolver: zodResolver(formSchema), }); @@ -79,8 +138,6 @@ export const MfaSetupStep = () => { }, }); - const isLoading = startLoading || isPending; - const submitHandler: SubmitHandler = (data) => { const sendData = { code: data.code, @@ -92,93 +149,17 @@ export const MfaSetupStep = () => { // biome-ignore lint/correctness/useExhaustiveDependencies: sideEffect useEffect(() => { setStoreState({ - loading: startLoading || isPending, - }); - }, [startLoading, isPending]); - - // biome-ignore lint/correctness/useExhaustiveDependencies: rxjs sub - useEffect(() => { - const sub = nextSubject.subscribe((direction) => { - switch (direction) { - case EnrollmentNavDirection.NEXT: - submitRef.current?.click(); - break; - case EnrollmentNavDirection.BACK: - setStoreState({ step: EnrollmentStepKey.MFA_CHOICE }); - break; - } + loading: isPending, }); - return () => { - sub.unsubscribe(); - }; - }, [nextSubject]); - - return ( - -
- -

Configure MFA

-
- {isLoading && ( -
- -
- )} - {!isLoading && isPresent(userInfo) && ( - <> - {mfaMethod === MfaMethod.TOTP && ( - <> - - {isPresent(startData?.totp_secret) && ( - - )} - - )} - {mfaMethod === MfaMethod.EMAIL && ( - -

- To setup your MFA, enter the code that was sent to your account email: -
- {userInfo.email} -

-
- )} -
- - - - - )} -
- ); -}; + }, [isPending]); -type TotpProps = { - email: string; - secret: string; -}; - -const TotpQr = ({ email, secret }: TotpProps) => { - const { writeToClipboard } = useClipboard(); return ( -
-
- -
-
+ + ); }; diff --git a/src/pages/enrollment/steps/MfaSetupStep/MfaSetupTotp.tsx b/src/pages/enrollment/steps/MfaSetupStep/MfaSetupTotp.tsx new file mode 100644 index 00000000..2c4f5073 --- /dev/null +++ b/src/pages/enrollment/steps/MfaSetupStep/MfaSetupTotp.tsx @@ -0,0 +1,49 @@ +import type { ReactNode } from 'react'; +import QRCode from 'react-qr-code'; +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 { useClipboard } from '../../../../shared/hooks/useClipboard'; + +type Props = { + children: ReactNode; + email: string; + secret: string; +}; + +export const MfaSetupTotp = ({ children, email, secret }: Props) => { + return ( +
+ + + {children} +
+ ); +}; + +type TotpProps = { + email: string; + secret: string; +}; + +const TotpQr = ({ email, secret }: TotpProps) => { + const { writeToClipboard } = useClipboard(); + return ( +
+
+ +
+
+ ); +}; diff --git a/src/pages/enrollment/steps/MfaSetupStep/style.scss b/src/pages/enrollment/steps/MfaSetupStep/style.scss index 445745fd..ad95f873 100644 --- a/src/pages/enrollment/steps/MfaSetupStep/style.scss +++ b/src/pages/enrollment/steps/MfaSetupStep/style.scss @@ -36,12 +36,30 @@ } } + .btn.resend { + width: 100%; + } + + .totp-setup, + .email-setup { + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + row-gap: var(--spacing-s); + } + + .resend { + width: 100%; + } + form { display: flex; flex-flow: column; align-items: center; justify-content: center; row-gap: var(--spacing-m); + width: 100%; & > .input { width: 100%; diff --git a/src/shared/defguard-ui b/src/shared/defguard-ui index 4fe9a92e..41f09091 160000 --- a/src/shared/defguard-ui +++ b/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 4fe9a92ef6b9cbd00189a92b9a70ab9bb5bfa43f +Subproject commit 41f0909110f8358e5225f3a9c19bd6590d5e905c diff --git a/src/shared/hooks/api/types.ts b/src/shared/hooks/api/types.ts index 8e2d54f4..e000eda6 100644 --- a/src/shared/hooks/api/types.ts +++ b/src/shared/hooks/api/types.ts @@ -24,13 +24,22 @@ export type EnrollmentStartRequest = { token: string; }; +export type EnrollmentSettings = { + admin_device_management: boolean; + mfa_required: boolean; + only_client_activation: boolean; + smtp_configured: boolean; + vpn_setup_optional: boolean; +}; + export type EnrollmentStartResponse = { admin: AdminInfo; user: UserInfo; + instance: EnrollmentInstanceInfo; deadline_timestamp: number; final_page_content: string; vpn_setup_optional: boolean; - instance: EnrollmentInstanceInfo; + settings: EnrollmentSettings; }; export type ActivateUserRequest = {