- 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..1ca4c669 100644
--- a/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx
+++ b/src/pages/enrollment/components/EnrollmentStepIndicator/EnrollmentStepIndicator.tsx
@@ -1,24 +1,41 @@
import './style.scss';
-import { shallow } from 'zustand/shallow';
-
+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 [step, maxStep] = useEnrollmentStore(
- (state) => [state.step, state.stepsMax],
- shallow,
+ const [stateStep, maxStep, ignoredSteps] = useEnrollmentStore((state) => [
+ state.step,
+ state.stepsMax,
+ state.stepsIgnored,
+ ]);
+
+ const step = useMemo(
+ () => getDisplayStep(stateStep, maxStep, ignoredSteps),
+ [ignoredSteps, stateStep, maxStep],
);
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 + 1 - ignoredSteps.length}
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..913304c6 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,36 @@ export const useEnrollmentApi = (): UseApi => {
state.cookie,
]);
+ const registerCodeMfaStart: UseApi['enrollment']['registerCodeMfaStart'] = async (
+ method,
+ ) => {
+ const response = await fetch(`${proxyUrl}/enrollment/register-mfa/code/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/code/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 +95,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..fc0e0636 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,20 @@ 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 { EnrollmentStepIndicator } from '../../../../components/EnrollmentStepIndicator/EnrollmentStepIndicator';
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 +43,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 +70,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) => {
@@ -100,63 +81,24 @@ export const DesktopSetup = () => {
)} Error status code: ${res.status} `,
);
}
- return res;
- });
- 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),
- }),
- );
- }
- });
+ return await res.json();
+ })) as CreateDeviceResponse;
+ toaster.success(stepLL.desktopSetup.messages.deviceConfigured());
+ setEnrollmentStore({
+ deviceName: values.name,
+ deviceKeys: {
+ private: privateKey,
+ public: publicKey,
+ },
+ deviceResponse,
});
+ setIsLoading(false);
+ next();
};
return (
+
{stepLL.desktopSetup.title()}
diff --git a/src/pages/enrollment/steps/PasswordStep/PasswordStep.tsx b/src/pages/enrollment/steps/PasswordStep/PasswordStep.tsx
index 2c330d10..1fb5b751 100644
--- a/src/pages/enrollment/steps/PasswordStep/PasswordStep.tsx
+++ b/src/pages/enrollment/steps/PasswordStep/PasswordStep.tsx
@@ -38,9 +38,14 @@ export const PasswordStep = () => {
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/SendFinishStep/SendFinishStep.tsx b/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx
new file mode 100644
index 00000000..dfd67530
--- /dev/null
+++ b/src/pages/enrollment/steps/SendFinishStep/SendFinishStep.tsx
@@ -0,0 +1,131 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { debug, error, info } from '@tauri-apps/plugin-log';
+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 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';
+import useEffectOnce from '../../../../shared/defguard-ui/utils/useEffectOnce';
+
+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 } = useMutation({
+ mutationFn: () =>
+ queryFn(
+ finishData,
+ deviceResponse as CreateDeviceResponse,
+ deviceKeys?.private as string,
+ ),
+ onError: (e) => {
+ setEnrollmentStore({ loading: false });
+ toaster.error(
+ LL.common.messages.errorWithMessage({
+ message: String(e),
+ }),
+ );
+ console.error(e);
+ error(String(e));
+ },
+ onSuccess: () => {
+ setEnrollmentStore({ loading: false });
+ next();
+ },
+ });
+
+ useEffectOnce(() => {
+ setEnrollmentStore({
+ loading: true,
+ });
+ setTimeout(() => {
+ mutate();
+ }, 250);
+ });
+
+ 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..21f881ff
--- /dev/null
+++ b/src/pages/enrollment/steps/Totp/TotpEnrollmentStep.tsx
@@ -0,0 +1,144 @@
+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
+
+
+
+ >
+ )}
+
+ );
+};
diff --git a/src/pages/enrollment/steps/Totp/style.scss b/src/pages/enrollment/steps/Totp/style.scss
new file mode 100644
index 00000000..2c4e5563
--- /dev/null
+++ b/src/pages/enrollment/steps/Totp/style.scss
@@ -0,0 +1,46 @@
+#enrollment-totp-card {
+ width: 100%;
+ max-width: 650px;
+ padding: 50px 40px;
+ display: flex;
+ flex-flow: column;
+ row-gap: var(--spacing-m);
+
+ .loading {
+ width: 100%;
+ display: flex;
+ flex-flow: row;
+ align-items: center;
+ justify-content: center;
+ min-height: 25dvh;
+ }
+
+ .qr {
+ display: flex;
+ flex-flow: row;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .btn {
+ width: unset;
+ flex-grow: 0;
+ width: min-content;
+ }
+
+ form {
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+ justify-content: center;
+ row-gap: var(--spacing-m);
+
+ & > .input {
+ width: 100%;
+ }
+
+ input[type='submit'] {
+ @include visually-hidden();
+ }
+ }
+}
diff --git a/src/pages/enrollment/style.scss b/src/pages/enrollment/style.scss
index ad2fa9f8..5b391c08 100644
--- a/src/pages/enrollment/style.scss
+++ b/src/pages/enrollment/style.scss
@@ -16,7 +16,7 @@
text-align: left;
}
- & > h3 {
+ h3 {
@include typography(app-body-1);
color: var(--text-body-primary);
diff --git a/src/shared/hooks/api/types.ts b/src/shared/hooks/api/types.ts
index 7f6e56f4..960a30d6 100644
--- a/src/shared/hooks/api/types.ts
+++ b/src/shared/hooks/api/types.ts
@@ -1,4 +1,5 @@
import type { DefguardInstance } from '../../../pages/client/types';
+import type { MfaMethod } from '../../types';
export type EmptyApiResponse = Record;
@@ -102,12 +103,28 @@ export type NewApplicationVersionInfo = {
update_url: string;
};
+export type RegisterCodeMfaFinishRequest = {
+ code: string;
+};
+
+export type RegisterCodeMfaStartResponse = {
+ totp_secret?: string;
+};
+
+export type RegisterCodeMfaFinishResponse = {
+ recovery_codes: string[];
+};
+
// FIXME: strong types
export type UseApi = {
enrollment: {
start: (data: EnrollmentStartRequest) => Promise;
activateUser: (data: ActivateUserRequest) => Promise;
createDevice: (data: CreateDeviceRequest) => Promise;
+ registerCodeMfaStart: (method: MfaMethod) => Promise;
+ registerCodeMfaFinish: (
+ data: RegisterCodeMfaFinishRequest,
+ ) => Promise;
};
getAppInfo: () => Promise;
};
diff --git a/src/shared/types.ts b/src/shared/types.ts
new file mode 100644
index 00000000..bee7e50f
--- /dev/null
+++ b/src/shared/types.ts
@@ -0,0 +1,7 @@
+export enum MfaMethod {
+ TOTP = 'Totp',
+ EMAIL = 'Email',
+ OIDC = 'Oidc',
+ BIOMETRIC = 'Biometric',
+ MOBILE_APPROVE = 'MobileApprove',
+}