feat(auth): integrate captcha validation and component for sign-in/up… (#15054)

… process

Added captcha token validation to the sign-in/up flow. Introduced
`StyledLoaderContainer` for better UI feedback during loading. Enhanced
error handling within `useSignInUp` to handle GraphQL errors
effectively.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Antoine Moreaux
2025-10-24 10:13:52 +02:00
committed by GitHub
parent 06a195af41
commit 4220bba69b
11 changed files with 185 additions and 38 deletions
@@ -42,6 +42,7 @@ import {
getFirstAvailableWorkspaces,
} from '@/auth/utils/availableWorkspacesUtils';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { apiConfigState } from '@/client-config/states/apiConfigState';
import { captchaState } from '@/client-config/states/captchaState';
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
@@ -65,7 +66,6 @@ import { type AuthToken } from '~/generated/graphql';
import { cookieStorage } from '~/utils/cookie-storage';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { loginTokenState } from '../states/loginTokenState';
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
export const useAuth = () => {
const setTokenPair = useSetRecoilState(tokenPairState);
@@ -12,6 +12,7 @@ import {
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
import { useCaptcha } from '@/client-config/hooks/useCaptcha';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Trans, useLingui } from '@lingui/react/macro';
import { OTPInput, type SlotProps } from 'input-otp';
@@ -184,6 +185,7 @@ export const SignInUpTOTPVerification = () => {
const navigate = useNavigateApp();
const { readCaptchaToken } = useReadCaptchaToken();
const { isCaptchaReady } = useCaptcha();
const loginToken = useRecoilValue(loginTokenState);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const { t } = useLingui();
@@ -193,7 +195,15 @@ export const SignInUpTOTPVerification = () => {
const submitOTP = async (values: OTPFormValues) => {
setIsLoading(true);
try {
const captchaToken = await readCaptchaToken();
if (!isCaptchaReady) {
enqueueErrorSnackBar({
message: t`Captcha (anti-bot check) is still loading, try again`,
});
setIsLoading(false);
return;
}
const captchaToken = readCaptchaToken();
if (!loginToken) {
return navigate(AppPath.SignInUp);
@@ -10,16 +10,18 @@ import {
} from '@/auth/states/signInUpStepState';
import { SignInUpMode } from '@/auth/types/signInUpMode';
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
import { useCaptcha } from '@/client-config/hooks/useCaptcha';
import { useBuildSearchParamsFromUrlSyncedStates } from '@/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { useLingui } from '@lingui/react/macro';
import { useRecoilState } from 'recoil';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { buildAppPathWithQueryParams } from '~/utils/buildAppPathWithQueryParams';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
import { useAuth } from '../../hooks/useAuth';
import { useLingui } from '@lingui/react/macro';
export const useSignInUp = (form: UseFormReturn<Form>) => {
const { enqueueErrorSnackBar } = useSnackBar();
@@ -28,6 +30,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
const { isCaptchaReady } = useCaptcha();
const location = useLocation();
@@ -59,49 +62,65 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
const errorMsgUserAlreadyExist = t`An error occurred while checking user existence`;
const continueWithCredentials = useCallback(async () => {
const token = await readCaptchaToken();
if (!form.getValues('email')) {
return;
return enqueueErrorSnackBar({
message: t`Email is required`,
});
}
if (!isCaptchaReady) {
return enqueueErrorSnackBar({
message: t`Captcha (anti-bot check) is still loading, try again`,
});
}
try {
const { data } = await checkUserExistsQuery({
const token = readCaptchaToken();
const { data, error } = await checkUserExistsQuery({
variables: {
email: form.getValues('email').toLowerCase().trim(),
captchaToken: token,
},
});
if (isDefined(error)) {
return enqueueErrorSnackBar({ apolloError: error });
}
setSignInUpMode(
data?.checkUserExists.exists
? SignInUpMode.SignIn
: SignInUpMode.SignUp,
);
setSignInUpStep(SignInUpStep.Password);
} catch (error) {
enqueueErrorSnackBar({
...(error instanceof ApolloError
? { apolloError: error }
: { message: errorMsgUserAlreadyExist }),
});
} catch {
enqueueErrorSnackBar({ message: errorMsgUserAlreadyExist });
}
}, [
readCaptchaToken,
form,
checkUserExistsQuery,
isCaptchaReady,
enqueueErrorSnackBar,
setSignInUpStep,
t,
checkUserExistsQuery,
setSignInUpMode,
setSignInUpStep,
errorMsgUserAlreadyExist,
]);
const submitCredentials: SubmitHandler<Form> = useCallback(
async (data) => {
const token = await readCaptchaToken();
try {
if (!data.email || !data.password) {
throw new Error('Email and password are required');
}
if (!data.email || !data.password) {
throw new Error('Email and password are required');
}
if (!isCaptchaReady) {
return enqueueErrorSnackBar({
message: t`Captcha (anti-bot check) is still loading, try again`,
});
}
const token = readCaptchaToken();
try {
if (
!isInviteMode &&
signInUpMode === SignInUpMode.SignIn &&
@@ -158,6 +177,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
}
},
[
isCaptchaReady,
readCaptchaToken,
signInUpMode,
isInviteMode,
@@ -170,6 +190,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
enqueueErrorSnackBar,
buildSearchParamsFromUrlSyncedStates,
isOnAWorkspace,
t,
],
);