Make token update synchronous on FE (#11486)

1. Removing tokenPair internal variable of ApolloFactory. We will relay
on cookieStorage
2. setting the cookie explicitely instead of only relaying on recoil
cookieEffect which is too late
This commit is contained in:
Charles Bochet
2025-04-10 01:39:25 +02:00
committed by GitHub
parent 7bd68ad176
commit a7e6564017
28 changed files with 160 additions and 159 deletions

View File

@ -16,8 +16,8 @@ import { useUpdateEffect } from '~/hooks/useUpdateEffect';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { AppPath } from '@/types/AppPath';
import { ApolloFactory, Options } from '../services/apollo.factory';
import { isDefined } from 'twenty-shared/utils';
import { ApolloFactory, Options } from '../services/apollo.factory';
export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
// eslint-disable-next-line @nx/workspace-no-state-useref
@ -26,7 +26,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
const navigate = useNavigate();
const { isMatchingLocation } = useIsMatchingLocation();
const [tokenPair, setTokenPair] = useRecoilState(tokenPairState);
const setTokenPair = useSetRecoilState(tokenPairState);
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
);
@ -62,8 +62,6 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
},
},
connectToDevTools: isDebugMode,
// We don't want to re-create the client on token change or it will cause infinite loop
initialTokenPair: tokenPair,
currentWorkspaceMember: currentWorkspaceMember,
onTokenPairChange: (tokenPair) => {
setTokenPair(tokenPair);
@ -104,12 +102,6 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
setPreviousUrl,
]);
useUpdateEffect(() => {
if (isDefined(apolloRef.current)) {
apolloRef.current.updateTokenPair(tokenPair);
}
}, [tokenPair]);
useUpdateEffect(() => {
if (isDefined(apolloRef.current)) {
apolloRef.current.updateWorkspaceMember(currentWorkspaceMember);

View File

@ -33,10 +33,6 @@ const mockWorkspaceMember = {
const createMockOptions = (): Options<any> => ({
uri: 'http://localhost:3000',
initialTokenPair: {
accessToken: { token: 'mockAccessToken', expiresAt: '' },
refreshToken: { token: 'mockRefreshToken', expiresAt: '' },
},
currentWorkspaceMember: mockWorkspaceMember,
cache: new InMemoryCache(),
isDebugMode: true,

View File

@ -18,9 +18,11 @@ import { logDebug } from '~/utils/logDebug';
import { i18n } from '@lingui/core';
import { GraphQLFormattedError } from 'graphql';
import { isDefined } from 'twenty-shared/utils';
import { cookieStorage } from '~/utils/cookie-storage';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { ApolloManager } from '../types/apolloManager.interface';
import { loggerLink } from '../utils/loggerLink';
import { isDefined } from 'twenty-shared/utils';
const logger = loggerLink(() => 'Twenty');
@ -29,7 +31,6 @@ export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
onNetworkError?: (err: Error | ServerParseError | ServerError) => void;
onTokenPairChange?: (tokenPair: AuthTokenPair) => void;
onUnauthenticatedError?: () => void;
initialTokenPair: AuthTokenPair | null;
currentWorkspaceMember: CurrentWorkspaceMember | null;
extraLinks?: ApolloLink[];
isDebugMode?: boolean;
@ -37,7 +38,6 @@ export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
private client: ApolloClient<TCacheShape>;
private tokenPair: AuthTokenPair | null = null;
private currentWorkspaceMember: CurrentWorkspaceMember | null = null;
constructor(opts: Options<TCacheShape>) {
@ -47,28 +47,45 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
onNetworkError,
onTokenPairChange,
onUnauthenticatedError,
initialTokenPair,
currentWorkspaceMember,
extraLinks,
isDebugMode,
...options
} = opts;
this.tokenPair = initialTokenPair;
this.currentWorkspaceMember = currentWorkspaceMember;
const getTokenPair = () => {
const stringTokenPair = cookieStorage.getItem('tokenPair');
const tokenPair = isDefined(stringTokenPair)
? (JSON.parse(stringTokenPair) as AuthTokenPair)
: undefined;
return tokenPair;
};
const buildApolloLink = (): ApolloLink => {
const httpLink = createUploadLink({
uri,
});
const authLink = setContext(async (_, { headers }) => {
const tokenPair = getTokenPair();
if (isUndefinedOrNull(tokenPair)) {
return {
headers: {
...headers,
...options.headers,
},
};
}
return {
headers: {
...headers,
...options.headers,
authorization: this.tokenPair?.accessToken.token
? `Bearer ${this.tokenPair?.accessToken.token}`
authorization: tokenPair.accessToken.token
? `Bearer ${tokenPair.accessToken.token}`
: '',
...(this.currentWorkspaceMember?.locale
? { 'x-locale': this.currentWorkspaceMember.locale }
@ -93,7 +110,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
for (const graphQLError of graphQLErrors) {
if (graphQLError.message === 'Unauthorized') {
return fromPromise(
renewToken(uri, this.tokenPair)
renewToken(uri, getTokenPair())
.then((tokens) => {
if (isDefined(tokens)) {
onTokenPairChange?.(tokens);
@ -108,10 +125,14 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
switch (graphQLError?.extensions?.code) {
case 'UNAUTHENTICATED': {
return fromPromise(
renewToken(uri, this.tokenPair)
renewToken(uri, getTokenPair())
.then((tokens) => {
if (isDefined(tokens)) {
onTokenPairChange?.(tokens);
cookieStorage.setItem(
'tokenPair',
JSON.stringify(tokens),
);
}
})
.catch(() => {
@ -162,10 +183,6 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
});
}
updateTokenPair(tokenPair: AuthTokenPair | null) {
this.tokenPair = tokenPair;
}
updateWorkspaceMember(workspaceMember: CurrentWorkspaceMember | null) {
this.currentWorkspaceMember = workspaceMember;
}

View File

@ -1,10 +1,8 @@
import { ApolloClient } from '@apollo/client';
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
import { AuthTokenPair } from '~/generated/graphql';
export interface ApolloManager<TCacheShape> {
getClient(): ApolloClient<TCacheShape>;
updateTokenPair(tokenPair: AuthTokenPair | null): void;
updateWorkspaceMember(workspaceMember: CurrentWorkspaceMember | null): void;
}