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
@@ -1,20 +1,22 @@
import { useEffect } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useLocation } from 'react-router-dom';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { getCaptchaUrlByProvider } from '@/captcha/utils/getCaptchaUrlByProvider';
import { captchaState } from '@/client-config/states/captchaState';
import { CaptchaDriverType } from '~/generated/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
import { useCaptcha } from '@/client-config/hooks/useCaptcha';
import { captchaState } from '@/client-config/states/captchaState';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { CaptchaDriverType } from '~/generated/graphql';
export const CaptchaProviderScriptLoaderEffect = () => {
const captcha = useRecoilValue(captchaState);
const [isCaptchaScriptLoaded, setIsCaptchaScriptLoaded] = useRecoilState(
const setIsCaptchaScriptLoaded = useSetRecoilState(
isCaptchaScriptLoadedState,
);
const { isCaptchaScriptLoaded, isCaptchaConfigured } = useCaptcha();
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
const location = useLocation();
@@ -60,10 +62,12 @@ export const CaptchaProviderScriptLoaderEffect = () => {
]);
useEffect(() => {
if (isUndefinedOrNull(captcha?.provider) || !isCaptchaScriptLoaded) {
if (!isCaptchaConfigured || !isCaptchaScriptLoaded) {
return;
}
assertIsDefinedOrThrow(captcha);
let refreshInterval: NodeJS.Timeout;
switch (captcha.provider) {
@@ -81,7 +85,13 @@ export const CaptchaProviderScriptLoaderEffect = () => {
}
return () => clearInterval(refreshInterval);
}, [captcha?.provider, requestFreshCaptchaToken, isCaptchaScriptLoaded]);
}, [
captcha,
captcha?.provider,
isCaptchaConfigured,
isCaptchaScriptLoaded,
requestFreshCaptchaToken,
]);
return <></>;
};
@@ -6,7 +6,7 @@ import { isDefined } from 'twenty-shared/utils';
export const useReadCaptchaToken = () => {
const readCaptchaToken = useRecoilCallback(
({ snapshot }) =>
async () => {
() => {
const existingCaptchaToken = snapshot
.getLoadable(captchaTokenState)
.getValue();
@@ -4,8 +4,8 @@ import { captchaTokenState } from '@/captcha/states/captchaTokenState';
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
import { captchaState } from '@/client-config/states/captchaState';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { CaptchaDriverType } from '~/generated-metadata/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useRequestFreshCaptchaToken = () => {
const setCaptchaToken = useSetRecoilState(captchaTokenState);
@@ -22,14 +22,15 @@ export const useRequestFreshCaptchaToken = () => {
const captcha = snapshot.getLoadable(captchaState).getValue();
if (isUndefinedOrNull(captcha?.provider)) {
if (!isDefined(captcha)) {
return;
}
assertIsDefinedOrThrow(captcha);
setIsRequestingCaptchaToken(true);
let captchaWidget: any;
switch (captcha.provider) {
case CaptchaDriverType.GOOGLE_RECAPTCHA:
window.grecaptcha