@@ -6,37 +6,17 @@ import {
66 ApolloLink ,
77 from ,
88 split ,
9- Operation ,
9+ Observable ,
10+ FetchResult ,
1011} from '@apollo/client' ;
1112import { onError } from '@apollo/client/link/error' ;
1213import { GraphQLWsLink } from '@apollo/client/link/subscriptions' ;
1314import { createClient } from 'graphql-ws' ;
14- import { getMainDefinition , Observable } from '@apollo/client/utilities' ;
15+ import { getMainDefinition } from '@apollo/client/utilities' ;
1516import createUploadLink from 'apollo-upload-client/createUploadLink.mjs' ;
1617import { LocalStore } from '@/lib/storage' ;
1718import { 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
4222const 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
228211const httpLinkWithMiddleware = from ( [
212+ tokenRefreshLink , // Add token refresh link first
229213 errorLink ,
230214 requestLoggingMiddleware ,
231215 authMiddleware ,
0 commit comments