From 985a3637fb22a2d2646da39b7b11db8fa79a9382 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 10 Oct 2023 14:44:40 +0800 Subject: [PATCH] refactor(experience): reorg mfa factor switching logic (#4628) --- .../components/SwitchMfaFactorsLink/index.tsx | 29 +++++++++++++++++++ .../src/containers/MfaFactorList/index.tsx | 10 +++---- .../use-mfa-verification-error-handler.ts | 15 +++++----- ...ding-totp.ts => use-start-totp-binding.ts} | 4 +-- .../MfaBinding/TotpBinding/index.module.scss | 4 +++ .../pages/MfaBinding/TotpBinding/index.tsx | 15 +++++----- .../experience/src/pages/MfaBinding/index.tsx | 6 ++-- .../TotpVerification/index.tsx | 20 ++++++------- .../src/pages/MfaVerification/index.tsx | 6 ++-- packages/experience/src/types/guard.ts | 12 ++------ 10 files changed, 73 insertions(+), 48 deletions(-) create mode 100644 packages/experience/src/components/SwitchMfaFactorsLink/index.tsx rename packages/experience/src/hooks/{use-start-binding-totp.ts => use-start-totp-binding.ts} (94%) diff --git a/packages/experience/src/components/SwitchMfaFactorsLink/index.tsx b/packages/experience/src/components/SwitchMfaFactorsLink/index.tsx new file mode 100644 index 000000000..ed4fb229b --- /dev/null +++ b/packages/experience/src/components/SwitchMfaFactorsLink/index.tsx @@ -0,0 +1,29 @@ +import { type MfaFactor } from '@logto/schemas'; + +import SwitchIcon from '@/assets/icons/switch-icon.svg'; +import { UserMfaFlow } from '@/types'; +import { type MfaFactorsState } from '@/types/guard'; + +import TextLink from '../TextLink'; + +type Props = { + flow: UserMfaFlow; + factors: MfaFactor[]; + className?: string; +}; + +const SwitchMfaFactorsLink = ({ flow, factors, className }: Props) => ( + } + state={{ availableFactors: factors } satisfies MfaFactorsState} + /> +); + +export default SwitchMfaFactorsLink; diff --git a/packages/experience/src/containers/MfaFactorList/index.tsx b/packages/experience/src/containers/MfaFactorList/index.tsx index b4a6fe48e..c48caed03 100644 --- a/packages/experience/src/containers/MfaFactorList/index.tsx +++ b/packages/experience/src/containers/MfaFactorList/index.tsx @@ -3,9 +3,9 @@ import { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import MfaFactorButton from '@/components/Button/MfaFactorButton'; -import useStartTotpBinding from '@/hooks/use-start-binding-totp'; +import useStartTotpBinding from '@/hooks/use-start-totp-binding'; import { UserMfaFlow } from '@/types'; -import { type TotpVerificationState } from '@/types/guard'; +import { type MfaFactorsState } from '@/types/guard'; import * as styles from './index.module.scss'; @@ -22,17 +22,17 @@ const MfaFactorList = ({ flow, factors }: Props) => { async (factor: MfaFactor) => { if (factor === MfaFactor.TOTP) { if (flow === UserMfaFlow.MfaBinding) { - await startTotpBinding(factors.length > 1); + await startTotpBinding(factors); } if (flow === UserMfaFlow.MfaVerification) { - const state: TotpVerificationState = { allowOtherFactors: true }; + const state: MfaFactorsState = { availableFactors: factors }; navigate(`/${UserMfaFlow.MfaVerification}/${factor}`, { state }); } } // Todo @xiaoyijun implement other factors }, - [factors.length, flow, navigate, startTotpBinding] + [factors, flow, navigate, startTotpBinding] ); return ( diff --git a/packages/experience/src/hooks/use-mfa-verification-error-handler.ts b/packages/experience/src/hooks/use-mfa-verification-error-handler.ts index d0ddddcbe..459a4af7e 100644 --- a/packages/experience/src/hooks/use-mfa-verification-error-handler.ts +++ b/packages/experience/src/hooks/use-mfa-verification-error-handler.ts @@ -8,11 +8,10 @@ import { type MfaFactorsState, missingMfaFactorsErrorDataGuard, requireMfaFactorsErrorDataGuard, - type TotpVerificationState, } from '@/types/guard'; import type { ErrorHandlers } from './use-error-handler'; -import useStartTotpBinding from './use-start-binding-totp'; +import useStartTotpBinding from './use-start-totp-binding'; import useToast from './use-toast'; export type Options = { @@ -22,7 +21,7 @@ export type Options = { const useMfaVerificationErrorHandler = ({ replace }: Options = {}) => { const navigate = useNavigate(); const { setToast } = useToast(); - const startBindingTotp = useStartTotpBinding({ replace }); + const startTotpBinding = useStartTotpBinding({ replace }); const mfaVerificationErrorHandler = useMemo( () => ({ @@ -36,7 +35,7 @@ const useMfaVerificationErrorHandler = ({ replace }: Options = {}) => { } if (missingFactors.length > 1) { - const state: MfaFactorsState = { factors: missingFactors }; + const state: MfaFactorsState = { availableFactors: missingFactors }; navigate({ pathname: `/${UserMfaFlow.MfaBinding}` }, { replace, state }); return; } @@ -44,7 +43,7 @@ const useMfaVerificationErrorHandler = ({ replace }: Options = {}) => { const factor = missingFactors[0]; if (factor === MfaFactor.TOTP) { - void startBindingTotp(); + void startTotpBinding(missingFactors); } // Todo: @xiaoyijun handle other factors }, @@ -57,7 +56,7 @@ const useMfaVerificationErrorHandler = ({ replace }: Options = {}) => { } if (availableFactors.length > 1) { - const state: MfaFactorsState = { factors: availableFactors }; + const state: MfaFactorsState = { availableFactors }; navigate({ pathname: `/${UserMfaFlow.MfaVerification}` }, { replace, state }); return; } @@ -69,13 +68,13 @@ const useMfaVerificationErrorHandler = ({ replace }: Options = {}) => { } if (factor === MfaFactor.TOTP) { - const state: TotpVerificationState = { allowOtherFactors: false }; + const state: MfaFactorsState = { availableFactors }; navigate({ pathname: `/${UserMfaFlow.MfaVerification}/${factor}` }, { replace, state }); } // Todo: @xiaoyijun handle other factors }, }), - [navigate, replace, setToast, startBindingTotp] + [navigate, replace, setToast, startTotpBinding] ); return mfaVerificationErrorHandler; diff --git a/packages/experience/src/hooks/use-start-binding-totp.ts b/packages/experience/src/hooks/use-start-totp-binding.ts similarity index 94% rename from packages/experience/src/hooks/use-start-binding-totp.ts rename to packages/experience/src/hooks/use-start-totp-binding.ts index 5b268bf49..46fbd6386 100644 --- a/packages/experience/src/hooks/use-start-binding-totp.ts +++ b/packages/experience/src/hooks/use-start-totp-binding.ts @@ -20,7 +20,7 @@ const useStartTotpBinding = ({ replace }: Options = {}) => { const handleError = useErrorHandler(); return useCallback( - async (allowOtherFactors = false) => { + async (availableFactors: MfaFactor[]) => { const [error, result] = await asyncCreateTotpSecret(); if (error) { @@ -35,7 +35,7 @@ const useStartTotpBinding = ({ replace }: Options = {}) => { secret, // Todo @wangsijie generate QR code on the server side secretQrCode: await qrcode.toDataURL(`otpauth://totp/?secret=${secret}`), - allowOtherFactors, + availableFactors, }; navigate({ pathname: `/${UserMfaFlow.MfaBinding}/${MfaFactor.TOTP}` }, { replace, state }); } diff --git a/packages/experience/src/pages/MfaBinding/TotpBinding/index.module.scss b/packages/experience/src/pages/MfaBinding/TotpBinding/index.module.scss index 760ca4d19..f6ec61521 100644 --- a/packages/experience/src/pages/MfaBinding/TotpBinding/index.module.scss +++ b/packages/experience/src/pages/MfaBinding/TotpBinding/index.module.scss @@ -6,3 +6,7 @@ margin-bottom: _.unit(6); align-items: stretch; } + +.switchLink { + align-self: start; +} diff --git a/packages/experience/src/pages/MfaBinding/TotpBinding/index.tsx b/packages/experience/src/pages/MfaBinding/TotpBinding/index.tsx index 68e7c0e14..6326f03e9 100644 --- a/packages/experience/src/pages/MfaBinding/TotpBinding/index.tsx +++ b/packages/experience/src/pages/MfaBinding/TotpBinding/index.tsx @@ -2,9 +2,8 @@ import { useLocation } from 'react-router-dom'; import { validate } from 'superstruct'; import SecondaryPageLayout from '@/Layout/SecondaryPageLayout'; -import SwitchIcon from '@/assets/icons/switch-icon.svg'; import Divider from '@/components/Divider'; -import TextLink from '@/components/TextLink'; +import SwitchMfaFactorsLink from '@/components/SwitchMfaFactorsLink'; import ErrorPage from '@/pages/ErrorPage'; import { UserMfaFlow } from '@/types'; import { totpBindingStateGuard } from '@/types/guard'; @@ -21,19 +20,21 @@ const TotpBinding = () => { return ; } + const { availableFactors } = totpBindingState; + return (
- {totpBindingState.allowOtherFactors && ( + {availableFactors.length > 1 && ( <> - } + )} diff --git a/packages/experience/src/pages/MfaBinding/index.tsx b/packages/experience/src/pages/MfaBinding/index.tsx index cd7559100..eeaebcb1f 100644 --- a/packages/experience/src/pages/MfaBinding/index.tsx +++ b/packages/experience/src/pages/MfaBinding/index.tsx @@ -11,15 +11,15 @@ import ErrorPage from '../ErrorPage'; const MfaBinding = () => { const { state } = useLocation(); const [, mfaFactorsState] = validate(state, mfaFactorsStateGuard); - const { factors } = mfaFactorsState ?? {}; + const { availableFactors } = mfaFactorsState ?? {}; - if (!factors || factors.length === 0) { + if (!availableFactors || availableFactors.length === 0) { return ; } return ( - + ); }; diff --git a/packages/experience/src/pages/MfaVerification/TotpVerification/index.tsx b/packages/experience/src/pages/MfaVerification/TotpVerification/index.tsx index dbd7bb36d..d18e701da 100644 --- a/packages/experience/src/pages/MfaVerification/TotpVerification/index.tsx +++ b/packages/experience/src/pages/MfaVerification/TotpVerification/index.tsx @@ -3,23 +3,24 @@ import { validate } from 'superstruct'; import SecondaryPageLayout from '@/Layout/SecondaryPageLayout'; import SectionLayout from '@/Layout/SectionLayout'; -import SwitchIcon from '@/assets/icons/switch-icon.svg'; -import TextLink from '@/components/TextLink'; +import SwitchMfaFactorsLink from '@/components/SwitchMfaFactorsLink'; import TotpCodeVerification from '@/containers/TotpCodeVerification'; import ErrorPage from '@/pages/ErrorPage'; import { UserMfaFlow } from '@/types'; -import { totpVerificationStateGuard } from '@/types/guard'; +import { mfaFactorsStateGuard } from '@/types/guard'; import * as styles from './index.module.scss'; const TotpVerification = () => { const { state } = useLocation(); - const [, totpVerificationState] = validate(state, totpVerificationStateGuard); + const [, mfaFactorsState] = validate(state, mfaFactorsStateGuard); - if (!totpVerificationState) { + if (!mfaFactorsState) { return ; } + const { availableFactors } = mfaFactorsState; + return ( { > - {totpVerificationState.allowOtherFactors && ( - } + {availableFactors.length > 1 && ( + )} diff --git a/packages/experience/src/pages/MfaVerification/index.tsx b/packages/experience/src/pages/MfaVerification/index.tsx index 73f1cf04c..1fbe9929c 100644 --- a/packages/experience/src/pages/MfaVerification/index.tsx +++ b/packages/experience/src/pages/MfaVerification/index.tsx @@ -11,15 +11,15 @@ import ErrorPage from '../ErrorPage'; const MfaVerification = () => { const { state } = useLocation(); const [, mfaFactorsState] = validate(state, mfaFactorsStateGuard); - const { factors } = mfaFactorsState ?? {}; + const { availableFactors } = mfaFactorsState ?? {}; - if (!factors || factors.length === 0) { + if (!availableFactors || availableFactors.length === 0) { return ; } return ( - + ); }; diff --git a/packages/experience/src/types/guard.ts b/packages/experience/src/types/guard.ts index 256ac06aa..007fbc3ef 100644 --- a/packages/experience/src/types/guard.ts +++ b/packages/experience/src/types/guard.ts @@ -80,25 +80,17 @@ export const requireMfaFactorsErrorDataGuard = s.object({ }); export const mfaFactorsStateGuard = s.object({ - factors: mfaFactorsGuard, + availableFactors: mfaFactorsGuard, }); export type MfaFactorsState = s.Infer; -const mfaFlowStateGuard = s.object({ - allowOtherFactors: s.boolean(), -}); - export const totpBindingStateGuard = s.assign( s.object({ secret: s.string(), secretQrCode: s.string(), }), - mfaFlowStateGuard + mfaFactorsStateGuard ); export type TotpBindingState = s.Infer; - -export const totpVerificationStateGuard = mfaFlowStateGuard; - -export type TotpVerificationState = s.Infer;