0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-20 21:32:31 -05:00
logto/packages/experience-legacy/src/hooks/use-mfa-error-handler.ts
simeng-li baa8577c45
refactor(experience): experience api migration (#6407)
* refactor(experience): migrate the password register and sign-in

migrate the password register and sign-in flow

* fix(experience): update some namings

update some namings

* refactor(experience): refactor the verification code flow (migration-2) (#6408)

* refactor(experience): refactor the verificaiton code flow

refactor the verification code flow

* refactor(experience): migrate the social and sso flow (migration-3) (#6406)

* refactor(experience): migrate the social and sso flow

migrate the social and sso flow

* refactor(experience): migrate profile fulfillment flow  (migration-4) (#6414)

* refactor(experience): migrate profile fulfillment flow

migrate the profile fulfillment flow

* refactor(experience): remove unused hook

remove unused hook

* fix(experience): fix password policy checker

fix password policy checker error display

* fix(experience): fix the api name

fix the api name

* refactor(experience): migrate mfa flow (migration-5) (#6417)

* refactor(experience): migrate mfa binding flow

migrate mfa binding flow

* test(experience): update unit tests (migration-6) (#6420)

* test(experience): update unit tests

update unit tests

* chore(experience): remove legacy APIs

remove legacy APIs

* refactor(experience): revert api prefix

revert api prefix

* fix(experience): update the sso connectors endpoint

update the sso connectors endpoint

* chore: add changeset

add changeset

* fix(experience): comments fix

comments fix

* refactor(experience): refactor the code verificatin api

refactor the code verification api

* refactor(experience): code refactor

refactor some implementation logic

* feat(experience, core): add experience legacy package (#6527)

add experience legacy package
2024-09-09 10:08:52 +08:00

150 lines
5.1 KiB
TypeScript

import { MfaFactor, type RequestErrorBody } from '@logto/schemas';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { validate } from 'superstruct';
import { UserMfaFlow } from '@/types';
import {
type MfaFlowState,
mfaErrorDataGuard,
backupCodeErrorDataGuard,
type BackupCodeBindingState,
} from '@/types/guard';
import { isNativeWebview } from '@/utils/native-sdk';
import type { ErrorHandlers } from './use-error-handler';
import useStartTotpBinding from './use-start-totp-binding';
import useStartWebAuthnProcessing from './use-start-webauthn-processing';
import useToast from './use-toast';
export type Options = {
replace?: boolean;
};
const useMfaErrorHandler = ({ replace }: Options = {}) => {
const navigate = useNavigate();
const { t } = useTranslation();
const { setToast } = useToast();
const startTotpBinding = useStartTotpBinding({ replace });
const startWebAuthnProcessing = useStartWebAuthnProcessing({ replace });
/**
* Redirect the user to the corresponding MFA page.
*
* Binding pages are hosted on following routes:
* - /{@link UserMfaFlow.MfaBinding} List of available MFA factors for binding.
* - /{@link UserMfaFlow.MfaBinding}/{@link MfaFactor} Binding page for the specific factor.
*
* Verification pages are hosted on following routes:
* - /{@link UserMfaFlow.MfaVerification} List of available MFA factors for verification.
* - /{@link UserMfaFlow.MfaVerification}/{@link MfaFactor} Verification page for the specific factor.
*
* Redirection rules:
* - If there is only one available factor, redirect to the specific MFA factor page.
* - If there are multiple available factors:
* - Binding: redirect to the available factors list page.
* - Verification: redirect to the last used specific factor page.
*/
const handleMfaRedirect = useCallback(
async (flow: UserMfaFlow, state: MfaFlowState) => {
const { availableFactors } = state;
if (availableFactors.length > 1 && flow === UserMfaFlow.MfaBinding) {
/**
* Redirect to the MFA binding page if there are multiple available factors.
*/
navigate({ pathname: `/${flow}` }, { replace, state });
return;
}
/**
* For verification: the first available factor is the last used factor which is guaranteed by the backend.
* For binding: the first available factor is the only available factor since we handle the multiple factors case above.
*/
const factor = availableFactors[0];
if (!factor) {
/**
* This should never happen since we check the available factors' length before handling the redirection.
*/
setToast(t('error.unknown'));
return;
}
if (factor === MfaFactor.TOTP && flow === UserMfaFlow.MfaBinding) {
/**
* Start TOTP binding process if only TOTP is available.
*/
return startTotpBinding(state);
}
if (factor === MfaFactor.WebAuthn) {
/**
* Start WebAuthn processing if only TOTP is available.
*/
return startWebAuthnProcessing(flow, state);
}
/**
* Redirect to the specific MFA factor page.
*/
navigate({ pathname: `/${flow}/${factor}` }, { replace, state });
},
[navigate, replace, setToast, startTotpBinding, startWebAuthnProcessing, t]
);
const handleMfaError = useCallback(
(flow: UserMfaFlow) => {
return async (error: RequestErrorBody) => {
const [_, data] = validate(error.data, mfaErrorDataGuard);
const factors = data?.availableFactors ?? [];
const skippable = data?.skippable;
if (factors.length === 0) {
setToast(error.message);
return;
}
const availableFactors =
// Hide the webauthn factor on native webview if the user has other options, since it's not supported.
isNativeWebview() && factors.length > 1
? factors.filter((factor) => factor !== MfaFactor.WebAuthn)
: factors;
await handleMfaRedirect(flow, { availableFactors, skippable });
};
},
[handleMfaRedirect, setToast]
);
const handleBackupCodeError = useCallback(
(error: RequestErrorBody) => {
const [_, data] = validate(error.data, backupCodeErrorDataGuard);
if (!data) {
setToast(error.message);
return;
}
navigate(
{ pathname: `/${UserMfaFlow.MfaBinding}/${MfaFactor.BackupCode}` },
{ replace, state: data satisfies BackupCodeBindingState }
);
},
[navigate, replace, setToast]
);
const mfaVerificationErrorHandler = useMemo<ErrorHandlers>(
() => ({
'user.missing_mfa': handleMfaError(UserMfaFlow.MfaBinding),
'session.mfa.require_mfa_verification': handleMfaError(UserMfaFlow.MfaVerification),
'session.mfa.backup_code_required': handleBackupCodeError,
}),
[handleBackupCodeError, handleMfaError]
);
return mfaVerificationErrorHandler;
};
export default useMfaErrorHandler;