Reputation: 41
I am developing a SaaS-based inventory application in Qwik JS with TypeScript. I use Axios to fetch data from a REST API and have implemented Axios interceptors to refresh the access token when it expires using the refresh token.
Since** Qwik JS is SSR**, cookies are not accessible in client mode. To authenticate protected API calls, I pass the cookie in the request header.
My approach:
I have a function axiosProtected, where I pass the cookie object as a parameter. On each call to this function, a singleton createAxiosInstance is invoked.
My concerns:
My implementation (code snippet):
import { type Cookie } from '@builder.io/qwik-city';
import type { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, AxiosInstance } from 'axios';
import type { ApiOptions, ResultResponse } from '../types';
import { APP_CONSTANTS, REFRESH_TOKEN, VIRUKSH_PUBLIC_URL } from '~/constants';
import { getAccessToken, refreshAuthToken, setToken } from './tokenService';
import axios from 'axios';
/**
* Function to create axios instance to serve private requests with token in the header
* Use interceptors.response to refresh the access token, if access token expired at any moment in between
* @param cookieObj - Cookie
* new token from interceptors.response set to cookie
* update on 27-April-2024 - converted GET to POST
*/
export async function createAxiosInstance(cookieObj: Cookie) {
const fetchAxiosClient: AxiosInstance = axios.create({
baseURL: VIRUKSH_PUBLIC_URL,
timeout: 60000
});
interface FailedRequests {
resolve: (value: AxiosResponse) => void;
reject: (value: AxiosError) => void;
config: AxiosRequestConfig;
error: AxiosError;
}
// Add the auth token to every request
fetchAxiosClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = getAccessToken(cookieObj).access_token;
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
config.headers['Content-Type'] = 'application/json';
return config;
},
error => Promise.reject(error)
);
let failedRequests: FailedRequests[] = [];
let isTokenRefreshing = false;
fetchAxiosClient.interceptors.response.use(
function (response: AxiosResponse) {
return response;
},
async (error: AxiosError) => {
const status = error.response?.status;
//const data = error.response?.data as AxiosFecthError;
//if (status !== 401 || data.code === 'ERR10088'|| data.code === 'ERR10086') {
if (status !== 401) {
return Promise.reject(error);
}
const originalRequestConfig = error.config!;
if (isTokenRefreshing) {
return new Promise((resolve, reject) => {
failedRequests.push({resolve, reject, config: originalRequestConfig,
error: error,
});
});
}
isTokenRefreshing = true;
try {
const { access_token, refresh_token } = await refreshAuthToken(cookieObj);
setToken(cookieObj, access_token!);
setToken(cookieObj, refresh_token!);
failedRequests.forEach(({ resolve, reject, config }) => {
fetchAxiosClient(config)
.then((response) => resolve(response))
.catch((error) => reject(error));
});
} catch (_error: unknown) {
console.error(_error);
failedRequests.forEach(({ reject, error }) => reject(error));
cookieObj.delete(APP_CONSTANTS.ACCESS_TOKEN, { path: '/' });
return Promise.reject(error);
} finally {
failedRequests = [];
isTokenRefreshing = false;
}
return fetchAxiosClient(originalRequestConfig);
}
);
return fetchAxiosClient;
}
/**
* axios post, put, get, delete
* to access protected api we need to share AccessToken in the headers
* @param cookie - Cookie object to get Access token
* @returns Promise<T>
*/
export async function axiosProtected<T>(options: ApiOptions, cookieObj: Cookie): Promise<ResultResponse<T>> {
const { endpoint, method, headers, body, signal } = options;
const apiUrl = `${VIRUKSH_PUBLIC_URL}${endpoint}`;
const requestHeaders: Record<string, string> = {
"Content-Type": "application/json",
'Accept': 'application/json',
...(headers || {}),
};
const fetchAxiosClient = await createAxiosInstance(cookieObj);
const response = await fetchAxiosClient!({
signal: signal,
method: method,
url: apiUrl,
headers: requestHeaders,
data: body ? JSON.stringify(body) : undefined,
}).then((response) => {
if (response.data.code === 200) {
const data = response.data as T;
return data
} else {
return {
code: response.data.response.code,
response: undefined,
success: false,
message: response.data.response.message,
error: new Error(response.data.response.error),
title: response.data.response.title,
} as T
}
}).catch((exception: any) => {
switch (exception.response.status) {
case 404:
return {
code: "10404",
response: undefined,
success: false,
message: "Page not found",
error: exception,
title: "Page not found",
} as T
case 400:
return {
code: exception.response.data.code,
response: undefined,
success: exception.response.data.success,
message: exception.response.data.message,
error: exception,
title: exception.response.data.title,
} as T
default:
return {
code: exception.response.data.code ? exception.response.data.code : "10144",
response: exception.response.data.response ? exception.response.data.response : undefined,
success: false,
message: exception.response.data.message ? exception.response.data.message : "Server not ready",
error: exception.response.data.error ? exception.response.data.error : exception,
title: exception.response.data.title ? exception.response.data.title : "Server error",
} as T
}
});
return response;
}
I would appreciate any insights on improving this implementation, especially considering Qwik’s SSR nature.
Let me know, if this approach is correct?
Upvotes: 0
Views: 19