fix(auth): add captcha auto-refresh via ApolloLink (#12758)

- Introduced `createCaptchaRefreshLink` to trigger captcha token refresh
automatically.
- Removed redundant manual captcha refresh calls and integrated it into
Apollo Provider.
This commit is contained in:
Antoine Moreaux
2025-06-20 11:38:01 +02:00
committed by GitHub
parent fe5574fdf6
commit 2469c509a6
4 changed files with 76 additions and 63 deletions

View File

@ -1,10 +1,17 @@
import { ApolloProvider as ApolloProviderBase } from '@apollo/client'; import { ApolloProvider as ApolloProviderBase } from '@apollo/client';
import { useApolloFactory } from '@/apollo/hooks/useApolloFactory'; import { useApolloFactory } from '@/apollo/hooks/useApolloFactory';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { createCaptchaRefreshLink } from '@/apollo/utils/captchaRefreshLink';
export const ApolloProvider = ({ children }: React.PropsWithChildren) => { export const ApolloProvider = ({ children }: React.PropsWithChildren) => {
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
const captchaRefreshLink = createCaptchaRefreshLink(requestFreshCaptchaToken);
const apolloClient = useApolloFactory({ const apolloClient = useApolloFactory({
connectToDevTools: true, connectToDevTools: true,
extraLinks: [captchaRefreshLink],
}); });
// This will attach the right apollo client to Apollo Dev Tools // This will attach the right apollo client to Apollo Dev Tools

View File

@ -0,0 +1,19 @@
import { ApolloLink } from '@apollo/client';
export const createCaptchaRefreshLink = (
requestFreshCaptchaToken: () => void,
) => {
return new ApolloLink((operation, forward) => {
const { variables } = operation;
const hasCaptchaToken = variables && 'captchaToken' in variables;
return forward(operation).map((response) => {
if (hasCaptchaToken) {
requestFreshCaptchaToken();
}
return response;
});
});
};

View File

@ -501,7 +501,8 @@ export const useAuth = () => {
const handleSignOut = useCallback(async () => { const handleSignOut = useCallback(async () => {
await clearSession(); await clearSession();
}, [clearSession]); await requestFreshCaptchaToken();
}, [clearSession, requestFreshCaptchaToken]);
const handleCredentialsSignUpInWorkspace = useCallback( const handleCredentialsSignUpInWorkspace = useCallback(
async ({ async ({
@ -519,7 +520,6 @@ export const useAuth = () => {
captchaToken?: string; captchaToken?: string;
verifyEmailNextPath?: string; verifyEmailNextPath?: string;
}) => { }) => {
try {
const signUpInWorkspaceResult = await signUpInWorkspace({ const signUpInWorkspaceResult = await signUpInWorkspace({
variables: { variables: {
email, email,
@ -559,8 +559,7 @@ export const useAuth = () => {
{ {
...(!isEmailVerificationRequired && { ...(!isEmailVerificationRequired && {
loginToken: loginToken:
signUpInWorkspaceResult.data.signUpInWorkspace.loginToken signUpInWorkspaceResult.data.signUpInWorkspace.loginToken.token,
.token,
}), }),
email, email,
}, },
@ -570,9 +569,6 @@ export const useAuth = () => {
await handleGetAuthTokensFromLoginToken( await handleGetAuthTokensFromLoginToken(
signUpInWorkspaceResult.data?.signUpInWorkspace.loginToken.token, signUpInWorkspaceResult.data?.signUpInWorkspace.loginToken.token,
); );
} finally {
requestFreshCaptchaToken();
}
}, },
[ [
signUpInWorkspace, signUpInWorkspace,
@ -583,7 +579,6 @@ export const useAuth = () => {
setSearchParams, setSearchParams,
isEmailVerificationRequired, isEmailVerificationRequired,
redirectToWorkspaceDomain, redirectToWorkspaceDomain,
requestFreshCaptchaToken,
], ],
); );

View File

@ -10,7 +10,6 @@ import {
} from '@/auth/states/signInUpStepState'; } from '@/auth/states/signInUpStepState';
import { SignInUpMode } from '@/auth/types/signInUpMode'; import { SignInUpMode } from '@/auth/types/signInUpMode';
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken'; import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { useBuildSearchParamsFromUrlSyncedStates } from '@/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates'; import { useBuildSearchParamsFromUrlSyncedStates } from '@/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
@ -47,16 +46,14 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
checkUserExists: { checkUserExistsQuery }, checkUserExists: { checkUserExistsQuery },
} = useAuth(); } = useAuth();
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
const { readCaptchaToken } = useReadCaptchaToken(); const { readCaptchaToken } = useReadCaptchaToken();
const { buildSearchParamsFromUrlSyncedStates } = const { buildSearchParamsFromUrlSyncedStates } =
useBuildSearchParamsFromUrlSyncedStates(); useBuildSearchParamsFromUrlSyncedStates();
const continueWithEmail = useCallback(() => { const continueWithEmail = useCallback(() => {
requestFreshCaptchaToken();
setSignInUpStep(SignInUpStep.Email); setSignInUpStep(SignInUpStep.Email);
}, [requestFreshCaptchaToken, setSignInUpStep]); }, [setSignInUpStep]);
const continueWithCredentials = useCallback(async () => { const continueWithCredentials = useCallback(async () => {
const token = await readCaptchaToken(); const token = await readCaptchaToken();
@ -74,7 +71,6 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
}); });
}, },
onCompleted: (data) => { onCompleted: (data) => {
requestFreshCaptchaToken();
setSignInUpMode( setSignInUpMode(
data?.checkUserExists.exists data?.checkUserExists.exists
? SignInUpMode.SignIn ? SignInUpMode.SignIn
@ -88,7 +84,6 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
form, form,
checkUserExistsQuery, checkUserExistsQuery,
enqueueSnackBar, enqueueSnackBar,
requestFreshCaptchaToken,
setSignInUpStep, setSignInUpStep,
setSignInUpMode, setSignInUpMode,
]); ]);
@ -154,8 +149,6 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
enqueueSnackBar(err?.message, { enqueueSnackBar(err?.message, {
variant: SnackBarVariant.Error, variant: SnackBarVariant.Error,
}); });
} finally {
requestFreshCaptchaToken();
} }
}, },
[ [
@ -169,7 +162,6 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
workspaceInviteHash, workspaceInviteHash,
workspacePersonalInviteToken, workspacePersonalInviteToken,
enqueueSnackBar, enqueueSnackBar,
requestFreshCaptchaToken,
buildSearchParamsFromUrlSyncedStates, buildSearchParamsFromUrlSyncedStates,
isOnAWorkspace, isOnAWorkspace,
], ],