Host-remote refresh token implementation (#18044)
This commit is contained in:
+5
@@ -9,6 +9,7 @@ import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { useRequestApplicationTokenRefresh } from '@/front-components/hooks/useRequestApplicationTokenRefresh';
|
||||
import { useUnmountHeadlessFrontComponent } from '@/front-components/hooks/useUnmountHeadlessFrontComponent';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
@@ -26,6 +27,9 @@ export const useFrontComponentExecutionContext = ({
|
||||
} => {
|
||||
const currentUser = useRecoilValueV2(currentUserState);
|
||||
const navigateApp = useNavigateApp();
|
||||
const { requestAccessTokenRefresh } = useRequestApplicationTokenRefresh({
|
||||
frontComponentId,
|
||||
});
|
||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||
const setCommandMenuSearchState = useSetRecoilStateV2(commandMenuSearchState);
|
||||
const { getIcon } = useIcons();
|
||||
@@ -115,6 +119,7 @@ export const useFrontComponentExecutionContext = ({
|
||||
const frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi =
|
||||
{
|
||||
navigate,
|
||||
requestAccessTokenRefresh,
|
||||
openSidePanelPage,
|
||||
enqueueSnackbar,
|
||||
unmountFrontComponent,
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import { ApolloError, useApolloClient } from '@apollo/client';
|
||||
import { type GraphQLFormattedError } from 'graphql';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
|
||||
|
||||
import { frontComponentApplicationTokenPairComponentState } from '@/front-components/states/frontComponentApplicationTokenPairComponentState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import {
|
||||
FindOneFrontComponentDocument,
|
||||
type FindOneFrontComponentQuery,
|
||||
type FindOneFrontComponentQueryVariables,
|
||||
RenewApplicationTokenDocument,
|
||||
type RenewApplicationTokenMutation,
|
||||
type RenewApplicationTokenMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED_SUB_CODE =
|
||||
'APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED';
|
||||
|
||||
const hasApplicationRefreshTokenInvalidOrExpiredSubCode = (
|
||||
errors: ReadonlyArray<GraphQLFormattedError>,
|
||||
): boolean =>
|
||||
errors.some(
|
||||
(error) =>
|
||||
error.extensions?.subCode ===
|
||||
APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED_SUB_CODE,
|
||||
);
|
||||
|
||||
type UseRequestApplicationTokenRefreshArgs = {
|
||||
frontComponentId: string;
|
||||
};
|
||||
|
||||
export const useRequestApplicationTokenRefresh = ({
|
||||
frontComponentId,
|
||||
}: UseRequestApplicationTokenRefreshArgs) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const applicationTokenPairCallbackState = useRecoilComponentCallbackState(
|
||||
frontComponentApplicationTokenPairComponentState,
|
||||
frontComponentId,
|
||||
);
|
||||
|
||||
const requestAccessTokenRefresh = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
async (): Promise<string> => {
|
||||
const refetchFrontComponentForNewTokenPair =
|
||||
async (): Promise<string> => {
|
||||
const result = await apolloClient.query<
|
||||
FindOneFrontComponentQuery,
|
||||
FindOneFrontComponentQueryVariables
|
||||
>({
|
||||
query: FindOneFrontComponentDocument,
|
||||
variables: { id: frontComponentId },
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
const newTokenPair =
|
||||
result.data?.frontComponent?.applicationTokenPair;
|
||||
|
||||
if (!isDefined(newTokenPair)) {
|
||||
throw new Error('Failed to refetch application token pair');
|
||||
}
|
||||
|
||||
set(applicationTokenPairCallbackState, newTokenPair);
|
||||
|
||||
return newTokenPair.applicationAccessToken.token;
|
||||
};
|
||||
|
||||
const applicationTokenPair = getSnapshotValue(
|
||||
snapshot,
|
||||
applicationTokenPairCallbackState,
|
||||
);
|
||||
|
||||
if (!isDefined(applicationTokenPair)) {
|
||||
throw new Error(
|
||||
'Application token pair must be initialized before requesting a refresh. Ensure the front component has loaded its token pair before invoking refresh.',
|
||||
);
|
||||
}
|
||||
|
||||
// First try renewing via the refresh token (fast path).
|
||||
// If the refresh token itself is expired, fall back to refetching
|
||||
// the front component which issues a fresh token pair server-side.
|
||||
try {
|
||||
const renewResult = await apolloClient.mutate<
|
||||
RenewApplicationTokenMutation,
|
||||
RenewApplicationTokenMutationVariables
|
||||
>({
|
||||
mutation: RenewApplicationTokenDocument,
|
||||
variables: {
|
||||
applicationRefreshToken:
|
||||
applicationTokenPair.applicationRefreshToken.token,
|
||||
},
|
||||
});
|
||||
|
||||
if (isNonEmptyArray(renewResult.errors)) {
|
||||
if (
|
||||
hasApplicationRefreshTokenInvalidOrExpiredSubCode(
|
||||
renewResult.errors,
|
||||
)
|
||||
) {
|
||||
return await refetchFrontComponentForNewTokenPair();
|
||||
}
|
||||
|
||||
const errorMessage = renewResult.errors
|
||||
.map((error) => error.message)
|
||||
.join(', ');
|
||||
|
||||
throw new Error(`Token renewal failed: ${errorMessage}`);
|
||||
}
|
||||
|
||||
const renewedTokenPair = renewResult.data?.renewApplicationToken;
|
||||
|
||||
if (!isDefined(renewedTokenPair)) {
|
||||
throw new Error('Failed to renew application token');
|
||||
}
|
||||
|
||||
set(applicationTokenPairCallbackState, renewedTokenPair);
|
||||
|
||||
return renewedTokenPair.applicationAccessToken.token;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof ApolloError &&
|
||||
hasApplicationRefreshTokenInvalidOrExpiredSubCode(
|
||||
error.graphQLErrors,
|
||||
)
|
||||
) {
|
||||
return await refetchFrontComponentForNewTokenPair();
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[apolloClient, applicationTokenPairCallbackState, frontComponentId],
|
||||
);
|
||||
|
||||
return { requestAccessTokenRefresh };
|
||||
};
|
||||
Reference in New Issue
Block a user