mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
feat(experience): add catpcha for sign in and forgot password (#7209)
* refactor(experience): execute captcha when click * refactor(experience): execute captcha when click * feat(experience): add catpcha for sign in and forgot password
This commit is contained in:
parent
c92202674b
commit
9f488ae160
8 changed files with 82 additions and 30 deletions
|
@ -0,0 +1,15 @@
|
|||
import { CaptchaType } from '@logto/schemas';
|
||||
|
||||
import { type SignInExperienceResponse } from '@/types';
|
||||
|
||||
export const getScript = (config: SignInExperienceResponse['captchaConfig']) => {
|
||||
if (!config) {
|
||||
throw new Error('Captcha config is not found');
|
||||
}
|
||||
|
||||
if (config.type === CaptchaType.Turnstile) {
|
||||
return `https://challenges.cloudflare.com/turnstile/v0/api.js`;
|
||||
}
|
||||
|
||||
return `https://www.google.com/recaptcha/enterprise.js?render=${config.siteKey}`;
|
||||
};
|
|
@ -63,8 +63,11 @@ export const signInWithVerifiedIdentifier = async (verificationId: string) => {
|
|||
};
|
||||
|
||||
// Password APIs
|
||||
export const signInWithPasswordIdentifier = async (payload: PasswordVerificationPayload) => {
|
||||
await initInteraction(InteractionEvent.SignIn);
|
||||
export const signInWithPasswordIdentifier = async (
|
||||
payload: PasswordVerificationPayload,
|
||||
captchaToken?: string
|
||||
) => {
|
||||
await initInteraction(InteractionEvent.SignIn, captchaToken);
|
||||
|
||||
const { verificationId } = await api
|
||||
.post(`${experienceApiRoutes.verification}/password`, {
|
||||
|
|
|
@ -174,16 +174,19 @@ describe('UsernamePasswordSignInForm', () => {
|
|||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(signInWithPasswordIdentifier).toBeCalledWith({
|
||||
identifier: {
|
||||
type,
|
||||
value:
|
||||
type === SignInIdentifier.Phone
|
||||
? `${getDefaultCountryCallingCode()}${identifier}`
|
||||
: identifier,
|
||||
expect(signInWithPasswordIdentifier).toBeCalledWith(
|
||||
{
|
||||
identifier: {
|
||||
type,
|
||||
value:
|
||||
type === SignInIdentifier.Phone
|
||||
? `${getDefaultCountryCallingCode()}${identifier}`
|
||||
: identifier,
|
||||
},
|
||||
password: 'password',
|
||||
},
|
||||
password: 'password',
|
||||
});
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -4,12 +4,14 @@ import { useCallback, useContext, useEffect } from 'react';
|
|||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import CaptchaContext from '@/Providers/CaptchaContextProvider/CaptchaContext';
|
||||
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||
import LockIcon from '@/assets/icons/lock.svg?react';
|
||||
import Button from '@/components/Button';
|
||||
import ErrorMessage from '@/components/ErrorMessage';
|
||||
import { SmartInputField, PasswordInputField } from '@/components/InputFields';
|
||||
import type { IdentifierInputValue } from '@/components/InputFields/SmartInputField';
|
||||
import CaptchaBox from '@/containers/CaptchaBox';
|
||||
import ForgotPasswordLink from '@/containers/ForgotPasswordLink';
|
||||
import TermsAndPrivacyCheckbox from '@/containers/TermsAndPrivacyCheckbox';
|
||||
import usePasswordSignIn from '@/hooks/use-password-sign-in';
|
||||
|
@ -41,6 +43,7 @@ const PasswordSignInForm = ({ className, autoFocus, signInMethods }: Props) => {
|
|||
const { termsValidation, agreeToTermsPolicy } = useTerms();
|
||||
const { setIdentifierInputValue } = useContext(UserInteractionContext);
|
||||
const prefilledIdentifier = usePrefilledIdentifier({ enabledIdentifiers: signInMethods });
|
||||
const { executeCaptcha } = useContext(CaptchaContext);
|
||||
|
||||
const {
|
||||
watch,
|
||||
|
@ -81,10 +84,13 @@ const PasswordSignInForm = ({ className, autoFocus, signInMethods }: Props) => {
|
|||
return;
|
||||
}
|
||||
|
||||
await onSubmit({
|
||||
identifier: { type, value },
|
||||
password,
|
||||
});
|
||||
await onSubmit(
|
||||
{
|
||||
identifier: { type, value },
|
||||
password,
|
||||
},
|
||||
await executeCaptcha()
|
||||
);
|
||||
})(event);
|
||||
},
|
||||
[
|
||||
|
@ -96,6 +102,7 @@ const PasswordSignInForm = ({ className, autoFocus, signInMethods }: Props) => {
|
|||
setIdentifierInputValue,
|
||||
showSingleSignOnForm,
|
||||
termsValidation,
|
||||
executeCaptcha,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -148,6 +155,8 @@ const PasswordSignInForm = ({ className, autoFocus, signInMethods }: Props) => {
|
|||
/>
|
||||
)}
|
||||
|
||||
<CaptchaBox />
|
||||
|
||||
{errorMessage && <ErrorMessage className={styles.formErrors}>{errorMessage}</ErrorMessage>}
|
||||
|
||||
{isForgotPasswordEnabled && !showSingleSignOnForm && (
|
||||
|
|
|
@ -38,7 +38,7 @@ const usePasswordSignIn = () => {
|
|||
);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (payload: PasswordVerificationPayload) => {
|
||||
async (payload: PasswordVerificationPayload, captchaToken?: string) => {
|
||||
const { identifier } = payload;
|
||||
|
||||
// Check if the email is registered with any SSO connectors. If the email is registered with any SSO connectors, we should not proceed to the next step
|
||||
|
@ -50,7 +50,7 @@ const usePasswordSignIn = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const [error, result] = await asyncSignIn(payload);
|
||||
const [error, result] = await asyncSignIn(payload, captchaToken);
|
||||
|
||||
if (error) {
|
||||
await handleError(error, errorHandlers);
|
||||
|
|
|
@ -3,10 +3,12 @@ import { useCallback, useContext, useEffect } from 'react';
|
|||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import CaptchaContext from '@/Providers/CaptchaContextProvider/CaptchaContext';
|
||||
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||
import Button from '@/components/Button';
|
||||
import ErrorMessage from '@/components/ErrorMessage';
|
||||
import { SmartInputField } from '@/components/InputFields';
|
||||
import CaptchaBox from '@/containers/CaptchaBox';
|
||||
import useSendVerificationCode from '@/hooks/use-send-verification-code';
|
||||
import type { VerificationCodeIdentifier } from '@/types';
|
||||
import { UserFlow } from '@/types';
|
||||
|
@ -34,6 +36,7 @@ const ForgotPasswordForm = ({ className, autoFocus, defaultValue = '', enabledTy
|
|||
const { errorMessage, clearErrorMessage, onSubmit } = useSendVerificationCode(
|
||||
UserFlow.ForgotPassword
|
||||
);
|
||||
const { executeCaptcha } = useContext(CaptchaContext);
|
||||
|
||||
const { setForgotPasswordIdentifierInputValue } = useContext(UserInteractionContext);
|
||||
|
||||
|
@ -68,10 +71,16 @@ const ForgotPasswordForm = ({ className, autoFocus, defaultValue = '', enabledTy
|
|||
// Cache or update the forgot password identifier input value
|
||||
setForgotPasswordIdentifierInputValue({ type, value });
|
||||
|
||||
await onSubmit({ identifier: type, value });
|
||||
await onSubmit({ identifier: type, value }, undefined, await executeCaptcha());
|
||||
})(event);
|
||||
},
|
||||
[clearErrorMessage, handleSubmit, onSubmit, setForgotPasswordIdentifierInputValue]
|
||||
[
|
||||
clearErrorMessage,
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
setForgotPasswordIdentifierInputValue,
|
||||
executeCaptcha,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -111,6 +120,8 @@ const ForgotPasswordForm = ({ className, autoFocus, defaultValue = '', enabledTy
|
|||
)}
|
||||
/>
|
||||
|
||||
<CaptchaBox />
|
||||
|
||||
{errorMessage && <ErrorMessage className={styles.formErrors}>{errorMessage}</ErrorMessage>}
|
||||
|
||||
<Button title="action.continue" htmlType="submit" isLoading={isSubmitting} />
|
||||
|
|
|
@ -71,13 +71,16 @@ describe('PasswordSignInForm', () => {
|
|||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(signInWithPasswordIdentifier).toBeCalledWith({
|
||||
identifier: {
|
||||
type: identifier,
|
||||
value,
|
||||
expect(signInWithPasswordIdentifier).toBeCalledWith(
|
||||
{
|
||||
identifier: {
|
||||
type: identifier,
|
||||
value,
|
||||
},
|
||||
password,
|
||||
},
|
||||
password,
|
||||
});
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
if (isVerificationCodeEnabled) {
|
||||
|
|
|
@ -4,11 +4,13 @@ import { useCallback, useContext, useEffect } from 'react';
|
|||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import CaptchaContext from '@/Providers/CaptchaContextProvider/CaptchaContext';
|
||||
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||
import Button from '@/components/Button';
|
||||
import ErrorMessage from '@/components/ErrorMessage';
|
||||
import { PasswordInputField } from '@/components/InputFields';
|
||||
import type { IdentifierInputValue } from '@/components/InputFields/SmartInputField';
|
||||
import CaptchaBox from '@/containers/CaptchaBox';
|
||||
import ForgotPasswordLink from '@/containers/ForgotPasswordLink';
|
||||
import usePasswordSignIn from '@/hooks/use-password-sign-in';
|
||||
import { useForgotPasswordSettings } from '@/hooks/use-sie';
|
||||
|
@ -42,6 +44,7 @@ const PasswordForm = ({
|
|||
const { errorMessage, clearErrorMessage, onSubmit } = usePasswordSignIn();
|
||||
const { setIdentifierInputValue } = useContext(UserInteractionContext);
|
||||
const { isForgotPasswordEnabled } = useForgotPasswordSettings();
|
||||
const { executeCaptcha } = useContext(CaptchaContext);
|
||||
|
||||
const {
|
||||
register,
|
||||
|
@ -76,13 +79,16 @@ const PasswordForm = ({
|
|||
|
||||
setIdentifierInputValue({ type, value });
|
||||
|
||||
await onSubmit({
|
||||
identifier: { type, value },
|
||||
password,
|
||||
});
|
||||
await onSubmit(
|
||||
{
|
||||
identifier: { type, value },
|
||||
password,
|
||||
},
|
||||
await executeCaptcha()
|
||||
);
|
||||
})(event);
|
||||
},
|
||||
[clearErrorMessage, handleSubmit, onSubmit, setIdentifierInputValue]
|
||||
[clearErrorMessage, handleSubmit, onSubmit, setIdentifierInputValue, executeCaptcha]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -108,6 +114,8 @@ const PasswordForm = ({
|
|||
{...register('password', { required: t('error.password_required') })}
|
||||
/>
|
||||
|
||||
<CaptchaBox />
|
||||
|
||||
{errorMessage && <ErrorMessage className={styles.formErrors}>{errorMessage}</ErrorMessage>}
|
||||
|
||||
{isForgotPasswordEnabled && (
|
||||
|
|
Loading…
Add table
Reference in a new issue