Skip to content

Commit c42e535

Browse files
committed
make client do auto refresh token
1 parent 8d9135a commit c42e535

File tree

1 file changed

+112
-128
lines changed

1 file changed

+112
-128
lines changed

frontend/src/lib/client.ts

Lines changed: 112 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,17 @@ import {
66
ApolloLink,
77
from,
88
split,
9-
Operation,
9+
Observable,
10+
FetchResult,
1011
} from '@apollo/client';
1112
import { onError } from '@apollo/client/link/error';
1213
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
1314
import { createClient } from 'graphql-ws';
14-
import { getMainDefinition, Observable } from '@apollo/client/utilities';
15+
import { getMainDefinition } from '@apollo/client/utilities';
1516
import createUploadLink from 'apollo-upload-client/createUploadLink.mjs';
1617
import { LocalStore } from '@/lib/storage';
1718
import { logger } from '@/app/log/logger';
18-
19-
// Token refresh state management
20-
let isRefreshing = false;
21-
let pendingRequests: Array<{
22-
operation: Operation;
23-
forward: any;
24-
observer: any;
25-
}> = [];
26-
27-
// Function to refresh token - will be set by AuthProvider
28-
let refreshTokenFunction: () => Promise<string | boolean | void>;
29-
let logoutFunction: () => void;
30-
31-
// Function to register the token refresh function
32-
export const registerRefreshTokenFunction = (
33-
refreshFn: () => Promise<string | boolean | void>,
34-
logout: () => void
35-
) => {
36-
refreshTokenFunction = refreshFn;
37-
logoutFunction = logout;
38-
};
39-
19+
import { REFRESH_TOKEN_MUTATION } from '@/graphql/mutations/auth';
4020

4121
// Create the upload link as the terminating link
4222
const uploadLink = createUploadLink({
@@ -95,123 +75,126 @@ const authMiddleware = new ApolloLink((operation, forward) => {
9575
return forward(operation);
9676
});
9777

98-
// Function to retry failed operations with new token
99-
const retryFailedOperations = () => {
100-
const requests = [...pendingRequests];
101-
pendingRequests = [];
102-
103-
requests.forEach(({ operation, forward, observer }) => {
104-
// Update the authorization header with the new token
105-
const token = localStorage.getItem(LocalStore.accessToken);
106-
if (token) {
107-
operation.setContext(({ headers = {} }) => ({
108-
headers: {
109-
...headers,
110-
Authorization: `Bearer ${token}`,
111-
},
112-
}));
78+
// Refresh Token Handling
79+
const refreshToken = async (): Promise<string | null> => {
80+
try {
81+
const refreshToken = localStorage.getItem(LocalStore.refreshToken);
82+
if (!refreshToken) {
83+
return null;
11384
}
114-
115-
// Retry the operation
116-
forward(operation).subscribe({
117-
next: observer.next.bind(observer),
118-
error: observer.error.bind(observer),
119-
complete: observer.complete.bind(observer),
85+
86+
console.debug('start refreshToken mutate');
87+
88+
// Use the main client for the refresh token request
89+
// The tokenRefreshLink will skip refresh attempts for this operation
90+
const result = await client.mutate({
91+
mutation: REFRESH_TOKEN_MUTATION,
92+
variables: { refreshToken },
12093
});
121-
});
122-
};
12394

124-
// Error Link
125-
// Error Link with Token Refresh Logic
126-
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
127-
const isAuthError = graphQLErrors?.some(error =>
128-
error.extensions?.code === 'UNAUTHENTICATED' ||
129-
error.message.includes('not authenticated') ||
130-
error.message.includes('jwt expired')
131-
) || networkError?.name === 'ServerError' && (networkError as any).statusCode === 401;
95+
if (result.data?.refreshToken?.accessToken) {
96+
const newAccessToken = result.data.refreshToken.accessToken;
97+
const newRefreshToken =
98+
result.data.refreshToken.refreshToken || refreshToken;
13299

133-
if (isAuthError) {
134-
// Check if we have a refresh token
135-
const hasRefreshToken = !!localStorage.getItem(LocalStore.refreshToken);
136-
137-
if (!hasRefreshToken || !refreshTokenFunction) {
138-
// No refresh token or refresh function - logout
139-
if (logoutFunction) {
140-
logoutFunction();
141-
}
142-
if (typeof window !== 'undefined') {
143-
window.location.href = '/';
144-
}
145-
return;
100+
localStorage.setItem(LocalStore.accessToken, newAccessToken);
101+
localStorage.setItem(LocalStore.refreshToken, newRefreshToken);
102+
103+
logger.info('Token refreshed successfully');
104+
return newAccessToken;
146105
}
147106

148-
// Return a new observable to handle the retry logic
149-
return new Observable(observer => {
150-
// If we're already refreshing, queue this request
151-
if (isRefreshing) {
152-
pendingRequests.push({ operation, forward, observer });
153-
} else {
154-
isRefreshing = true;
155-
156-
// Try to refresh the token
157-
refreshTokenFunction()
158-
.then(success => {
159-
isRefreshing = false;
160-
161-
if (success) {
162-
// Retry this operation
163-
const token = localStorage.getItem(LocalStore.accessToken);
164-
if (token) {
165-
operation.setContext(({ headers = {} }) => ({
107+
return null;
108+
} catch (error) {
109+
logger.error('Error refreshing token:', error);
110+
return null;
111+
}
112+
};
113+
114+
// Handle token expiration and refresh
115+
const tokenRefreshLink = onError(
116+
({ graphQLErrors, networkError, operation, forward }) => {
117+
if (graphQLErrors) {
118+
for (const err of graphQLErrors) {
119+
// Check for auth errors (adjust this check based on your API's error structure)
120+
const isAuthError =
121+
err.extensions?.code === 'UNAUTHENTICATED' ||
122+
err.message.includes('Unauthorized') ||
123+
err.message.includes('token expired');
124+
125+
// Don't try to refresh if this operation is the refresh token mutation
126+
// This prevents infinite refresh loops
127+
const operationName = operation.operationName;
128+
const path = err.path;
129+
const isRefreshTokenOperation =
130+
operationName === 'RefreshToken' ||
131+
(path && path.includes('refreshToken'));
132+
133+
if (isAuthError && !isRefreshTokenOperation) {
134+
logger.info('Auth error detected, attempting token refresh');
135+
136+
// Return a new observable to handle the token refresh
137+
return new Observable<FetchResult>((observer) => {
138+
// Attempt to refresh the token
139+
(async () => {
140+
try {
141+
const newToken = await refreshToken();
142+
143+
if (!newToken) {
144+
// If refresh fails, clear tokens and redirect
145+
localStorage.removeItem(LocalStore.accessToken);
146+
localStorage.removeItem(LocalStore.refreshToken);
147+
148+
// Redirect to home/login page when running in browser
149+
if (typeof window !== 'undefined') {
150+
logger.warn(
151+
'Token refresh failed, redirecting to home page'
152+
);
153+
window.location.href = '/';
154+
}
155+
156+
// Complete the observer with the original error
157+
observer.error(err);
158+
observer.complete();
159+
return;
160+
}
161+
162+
// Retry the operation with the new token
163+
// Clone the operation with the new token
164+
const oldHeaders = operation.getContext().headers;
165+
operation.setContext({
166166
headers: {
167-
...headers,
168-
Authorization: `Bearer ${token}`,
167+
...oldHeaders,
168+
Authorization: `Bearer ${newToken}`,
169169
},
170-
}));
171-
}
172-
173-
// Retry all pending operations
174-
retryFailedOperations();
175-
176-
// Retry the current operation
177-
forward(operation).subscribe({
178-
next: observer.next.bind(observer),
179-
error: observer.error.bind(observer),
180-
complete: observer.complete.bind(observer),
181-
});
182-
} else {
183-
// Refresh failed - redirect to homepage
184-
if (logoutFunction) {
185-
logoutFunction();
186-
}
187-
if (typeof window !== 'undefined') {
188-
window.location.href = '/';
170+
});
171+
172+
// Retry the request
173+
forward(operation).subscribe({
174+
next: observer.next.bind(observer),
175+
error: observer.error.bind(observer),
176+
complete: observer.complete.bind(observer),
177+
});
178+
} catch (error) {
179+
logger.error('Error in token refresh flow:', error);
180+
observer.error(error);
181+
observer.complete();
189182
}
190-
191-
// Complete the operation
192-
observer.error(new Error('Session expired. Please log in again.'));
193-
}
194-
})
195-
.catch(error => {
196-
isRefreshing = false;
197-
logger.error('Token refresh failed:', error);
198-
199-
// Refresh failed - redirect to homepage
200-
if (logoutFunction) {
201-
logoutFunction();
202-
}
203-
if (typeof window !== 'undefined') {
204-
window.location.href = '/';
205-
}
206-
207-
// Complete the operation
208-
observer.error(new Error('Session expired. Please log in again.'));
183+
})();
209184
});
185+
}
210186
}
211-
});
187+
}
188+
189+
if (networkError) {
190+
logger.error(`[Network error]: ${networkError}`);
191+
// Handle network errors if needed
192+
}
212193
}
194+
);
213195

214-
// Handle other errors
196+
// Error Link
197+
const errorLink = onError(({ graphQLErrors, networkError }) => {
215198
if (graphQLErrors) {
216199
graphQLErrors.forEach(({ message, locations, path }) => {
217200
logger.error(
@@ -226,6 +209,7 @@ const errorLink = onError(({ graphQLErrors, networkError, operation, forward })
226209

227210
// Build the HTTP link chain
228211
const httpLinkWithMiddleware = from([
212+
tokenRefreshLink, // Add token refresh link first
229213
errorLink,
230214
requestLoggingMiddleware,
231215
authMiddleware,

0 commit comments

Comments
 (0)