194 lines
5.9 KiB
TypeScript
194 lines
5.9 KiB
TypeScript
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<string | null> | null = null;
|
|
|
|
async function getValidAccessToken(): Promise<string | null> {
|
|
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) => {
|
|
const token = getAccessToken();
|
|
// console.log('Token: ', token);
|
|
|
|
return {
|
|
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();
|
|
console.log('Access Token hết hạn, đang refresh...');
|
|
|
|
// Không refresh được => xoá dữ liệu, điều hướng về trang login
|
|
if (!newToken) {
|
|
console.log('Access Token hết hạn và không thể refresh');
|
|
const { pathname } = history.location;
|
|
clearAllData();
|
|
if (pathname !== ROUTE_LOGIN) {
|
|
console.log(
|
|
'Request dev: Chuyển về trang login và có pathname ',
|
|
pathname,
|
|
);
|
|
|
|
history.push(`${ROUTE_LOGIN}?redirect=${pathname}`);
|
|
} else {
|
|
console.log(
|
|
'Request dev: Chuyển về trang login và không có pathname',
|
|
);
|
|
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,
|
|
};
|