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,60 +520,55 @@ export const useAuth = () => {
captchaToken?: string; captchaToken?: string;
verifyEmailNextPath?: string; verifyEmailNextPath?: string;
}) => { }) => {
try { const signUpInWorkspaceResult = await signUpInWorkspace({
const signUpInWorkspaceResult = await signUpInWorkspace({ variables: {
variables: { email,
email, password,
password, workspaceInviteHash,
workspaceInviteHash, workspacePersonalInviteToken,
workspacePersonalInviteToken, captchaToken,
captchaToken, locale: i18n.locale ?? 'en',
locale: i18n.locale ?? 'en', ...(workspacePublicData?.id
...(workspacePublicData?.id ? { workspaceId: workspacePublicData.id }
? { workspaceId: workspacePublicData.id } : {}),
: {}), verifyEmailNextPath,
verifyEmailNextPath, },
}, });
});
if (isDefined(signUpInWorkspaceResult.errors)) { if (isDefined(signUpInWorkspaceResult.errors)) {
throw 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 (!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, 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,
], ],