import { HTTPSTATUS } from '@/constants'; import { API_PATH_REFRESH_TOKEN } from '@/constants/api'; import { ROUTE_LOGIN } from '@/constants/routes'; import { apiRefreshToken } from '@/services/master/AuthController'; import { checkRefreshTokenExpired } from '@/utils/jwt'; import { clearAllData, getAccessToken, getRefreshToken, setAccessToken, } from '@/utils/storage'; import { history, request, RequestConfig } from '@umijs/max'; import { message } from 'antd'; // Trạng thái dùng chung cho cơ chế refresh token + hàng đợi let refreshingTokenPromise: Promise | null = null; async function getValidAccessToken(): Promise { const refreshToken = getRefreshToken(); if (!refreshToken || checkRefreshTokenExpired(refreshToken)) { return null; } if (!refreshingTokenPromise) { refreshingTokenPromise = apiRefreshToken({ refresh_token: refreshToken }) .then((resp) => { if (resp?.access_token) { setAccessToken(resp.access_token); return resp.access_token; } return null; }) .catch((err) => { // eslint-disable-next-line no-console console.error('Refresh token failed:', err); return null; }) .finally(() => { refreshingTokenPromise = null; }); } return refreshingTokenPromise; } // Runtime configuration export const handleRequestConfig: RequestConfig = { // Unified request settings timeout: 20000, validateStatus: (status) => { return ( (status >= 200 && status < 300) || status === HTTPSTATUS.HTTP_NOTFOUND || status === HTTPSTATUS.HTTP_UNAUTHORIZED ); }, headers: { 'X-Requested-With': 'XMLHttpRequest' }, // Error handling: umi@3's error handling scheme. errorConfig: { // Error throwing errorThrower: (res: any) => { // console.log('Response from backend:', res); const { success, data, errorCode, errorMessage, showType } = res; if (!success) { const error: any = new Error(errorMessage); error.name = 'BizError'; error.info = { errorCode, errorMessage, showType, data }; throw error; // Throw custom error } }, // Error catching and handling errorHandler: (error: any) => { if (error.response) { const { status } = error.response; // Ưu tiên: codeMessage → backend message → statusText // const errMsg = // codeMessage[status as keyof typeof codeMessage] || // data?.message || // statusText || // 'Unknown error'; if (status === HTTPSTATUS.HTTP_UNAUTHORIZED) { // 401 đã được xử lý trong responseInterceptors (refresh token + redirect nếu cần) return; } else if (status === HTTPSTATUS.HTTP_SERVERERROR) { // message.error('💥 Internal server error!'); } else { message.error(`❌ ${status}: ${error.message || 'Error'}`); } } else if (error.request) { message.error('🚨 No response from server!'); } else { message.error(`⚠️ Request setup error: ${error.message}`); } }, }, // Request interceptors requestInterceptors: [ (url: string, options: any) => { // console.log('URL Request:', url, options); // Chỉ cần thêm token, proxy sẽ xử lý việc redirect đến đúng port // URL sẽ bắt đầu với /api và proxy sẽ chuyển đến hostname:81/api const token = getAccessToken(); return { url: url, options: { ...options, headers: { ...options.headers, ...(token && !options.headers.Authorization ? { Authorization: `${token}` } : {}), }, }, }; }, ], // Unwrap data from backend response responseInterceptors: [ async (response: any, options: any) => { const isRefreshRequest = response.url?.includes(API_PATH_REFRESH_TOKEN); const alreadyRetried = options?.skipAuthRefresh === true; // Xử lý 401: đưa request vào hàng đợi, gọi refresh token, rồi gọi lại if ( response.status === HTTPSTATUS.HTTP_UNAUTHORIZED && // Không tự refresh cho chính API refresh token để tránh vòng lặp vô hạn !isRefreshRequest && !alreadyRetried ) { const newToken = await getValidAccessToken(); // Không refresh được => xoá dữ liệu, điều hướng về trang login if (!newToken) { const { pathname } = history.location; clearAllData(); if (pathname !== ROUTE_LOGIN) { history.push(`${ROUTE_LOGIN}?redirect=${pathname}`); } else { history.push(ROUTE_LOGIN); } return Promise.reject( new Error('Unauthorized and refresh token is invalid'), ); } const newOptions = { ...options, headers: { ...(options.headers || {}), Authorization: `${newToken}`, }, skipAuthRefresh: true, }; // Gọi lại request gốc với accessToken mới return request(response.url, newOptions); } if ( response.status === HTTPSTATUS.HTTP_UNAUTHORIZED && (isRefreshRequest || alreadyRetried) ) { clearAllData(); history.push(ROUTE_LOGIN); return Promise.reject(new Error('Unauthorized')); } // console.log('Response from server: ', response); return { status: response.status, statusText: response.statusText, headers: response.headers, config: response.config, data: response.data, }; }, ] as any, };