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:
@@ -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);
|
||||
|
||||
+11
-1
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user