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 { useApolloFactory } from '@/apollo/hooks/useApolloFactory';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { createCaptchaRefreshLink } from '@/apollo/utils/captchaRefreshLink';
export const ApolloProvider = ({ children }: React.PropsWithChildren) => {
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
const captchaRefreshLink = createCaptchaRefreshLink(requestFreshCaptchaToken);
const apolloClient = useApolloFactory({
connectToDevTools: true,
extraLinks: [captchaRefreshLink],
});
// 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 () => {
await clearSession();
}, [clearSession]);
await requestFreshCaptchaToken();
}, [clearSession, requestFreshCaptchaToken]);
const handleCredentialsSignUpInWorkspace = useCallback(
async ({
@ -519,60 +520,55 @@ export const useAuth = () => {
captchaToken?: string;
verifyEmailNextPath?: string;
}) => {
try {
const signUpInWorkspaceResult = await signUpInWorkspace({
variables: {
email,
password,
workspaceInviteHash,
workspacePersonalInviteToken,
captchaToken,
locale: i18n.locale ?? 'en',
...(workspacePublicData?.id
? { workspaceId: workspacePublicData.id }
: {}),
verifyEmailNextPath,
},
});
const signUpInWorkspaceResult = await signUpInWorkspace({
variables: {
email,
password,
workspaceInviteHash,
workspacePersonalInviteToken,
captchaToken,
locale: i18n.locale ?? 'en',
...(workspacePublicData?.id
? { workspaceId: workspacePublicData.id }
: {}),
verifyEmailNextPath,
},
});
if (isDefined(signUpInWorkspaceResult.errors)) {
throw signUpInWorkspaceResult.errors;
}
if (!signUpInWorkspaceResult.data?.signUpInWorkspace) {
throw new Error('No login token');
}
if (isEmailVerificationRequired) {
setSearchParams({ email });
setSignInUpStep(SignInUpStep.EmailVerification);
return null;
}
if (isMultiWorkspaceEnabled) {
return await redirectToWorkspaceDomain(
getWorkspaceUrl(
signUpInWorkspaceResult.data.signUpInWorkspace.workspace
.workspaceUrls,
),
isEmailVerificationRequired ? AppPath.SignInUp : AppPath.Verify,
{
...(!isEmailVerificationRequired && {
loginToken:
signUpInWorkspaceResult.data.signUpInWorkspace.loginToken
.token,
}),
email,
},
);
}
await handleGetAuthTokensFromLoginToken(
signUpInWorkspaceResult.data?.signUpInWorkspace.loginToken.token,
);
} finally {
requestFreshCaptchaToken();
if (isDefined(signUpInWorkspaceResult.errors)) {
throw signUpInWorkspaceResult.errors;
}
if (!signUpInWorkspaceResult.data?.signUpInWorkspace) {
throw new Error('No login token');
}
if (isEmailVerificationRequired) {
setSearchParams({ email });
setSignInUpStep(SignInUpStep.EmailVerification);
return null;
}
if (isMultiWorkspaceEnabled) {
return await redirectToWorkspaceDomain(
getWorkspaceUrl(
signUpInWorkspaceResult.data.signUpInWorkspace.workspace
.workspaceUrls,
),
isEmailVerificationRequired ? AppPath.SignInUp : AppPath.Verify,
{
...(!isEmailVerificationRequired && {
loginToken:
signUpInWorkspaceResult.data.signUpInWorkspace.loginToken.token,
}),
email,
},
);
}
await handleGetAuthTokensFromLoginToken(
signUpInWorkspaceResult.data?.signUpInWorkspace.loginToken.token,
);
},
[
signUpInWorkspace,
@ -583,7 +579,6 @@ export const useAuth = () => {
setSearchParams,
isEmailVerificationRequired,
redirectToWorkspaceDomain,
requestFreshCaptchaToken,
],
);

View File

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