import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig, } from "axios"; import { delay } from "@/common/funcs/delay"; import { getKeyCert } from "@/common/funcs/optionConvert"; import { toastInfo, toastSuccess, toastWarn } from "@/common/funcs/toast"; import { logout, setToken } from "@/redux/reducer/auth"; import { setInfoUser } from "@/redux/reducer/user"; import { store } from "@/redux/store"; /* ========================================================= * TYPES * =======================================================*/ interface ApiError { code: string | number; message: string; } export interface ApiResponse { success?: boolean; error?: ApiError | null; data?: T; items?: T[]; pagination?: unknown; } interface HttpRequestOptions { http: Promise>; setLoading?: React.Dispatch>; onError?: () => void; showMessageSuccess?: boolean; showMessageFailed?: boolean; msgSuccess?: string; isDropdown?: boolean; isPagination?: boolean; isData?: boolean; } /* ========================================================= * AXIOS CLIENT * =======================================================*/ const axiosClient = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL_DEV, timeout: 15000, timeoutErrorMessage: "Timeout error request", headers: { "content-type": "application/json", }, }) as AxiosInstance & { get(url: string, config?: any): Promise>; post( url: string, data?: any, config?: any, ): Promise>; put( url: string, data?: any, config?: any, ): Promise>; delete(url: string, config?: any): Promise>; }; /* ========================================================= * REFRESH CLIENT * =======================================================*/ const refreshClient = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL_DEV, timeout: 15000, }); /* ========================================================= * REFRESH REQUEST INTERCEPTOR * =======================================================*/ refreshClient.interceptors.request.use((config) => { if (!(config.data instanceof FormData)) { config.data = { ...getKeyCert(), ...(config.data || {}), }; } return config; }); /* ========================================================= * REFRESH TOKEN * =======================================================*/ let refreshPromise: Promise | null = null; const refreshAccessToken = async (): Promise => { if (refreshPromise) { return refreshPromise; } refreshPromise = (async () => { try { const state = store.getState(); const refreshToken = state.user?.infoUser?.refreshToken || null; if (!refreshToken) { return null; } const response = await refreshClient.post<{ accessToken: string; refreshToken: string; accessTokenExpiresAt: string; sessionId: string; }>("/auth/refresh", { refreshToken, }); const responseData = response.data; const newToken = responseData.accessToken; if (!newToken) { return null; } /* UPDATE ACCESS TOKEN */ store.dispatch(setToken(newToken)); /* UPDATE USER INFO */ const prev = store.getState().user.infoUser; store.dispatch( setInfoUser({ accessToken: newToken, refreshToken: responseData?.refreshToken || prev?.refreshToken || "", userId: prev?.userId || "", }), ); return newToken; } catch (error) { console.error("Refresh token failed:", error); return null; } finally { refreshPromise = null; } })(); return refreshPromise; }; /* ========================================================= * REQUEST INTERCEPTOR * =======================================================*/ axiosClient.interceptors.request.use( async ( config: InternalAxiosRequestConfig, ): Promise => { const token = store.getState().auth.token; config.headers = config.headers || {}; /* AUTHORIZATION */ if (token) { config.headers.Authorization = `Bearer ${token}`; } /* AUTO ADD CERT */ if (!(config.data instanceof FormData)) { config.data = { ...getKeyCert(), ...(config.data || {}), }; } return config; }, ); /* ========================================================= * RESPONSE INTERCEPTOR * =======================================================*/ axiosClient.interceptors.response.use( (response) => { return ( response.data || { success: false, error: { code: "UNKNOWN_ERROR", message: "Unknown error", }, } ); }, async (error: AxiosError) => { const status = error.response?.status; const data = error.response?.data; const config = error.config as InternalAxiosRequestConfig & { _retry?: boolean; }; /* ===================================================== * AUTO REFRESH TOKEN * ===================================================*/ if (status === 401 && !config._retry) { config._retry = true; const newToken = await refreshAccessToken(); if (newToken) { config.headers.Authorization = `Bearer ${newToken}`; return axiosClient(config); } /* REFRESH FAILED */ store.dispatch(logout()); store.dispatch(setInfoUser(null)); } /* ===================================================== * HANDLE EMPTY RESPONSE * ===================================================*/ if ((status === 401 || status === 403) && !data) { return Promise.reject({ status, success: false, error: { code: status, message: error.response?.statusText || "Unauthorized", }, }); } // If server provided a body (data), forward it and attach HTTP status so // callers receive the actual error and the numeric status code. const serverBody = error.response?.data; let payload: any; if (serverBody && typeof serverBody === "object") { payload = { ...serverBody, status }; } else if (serverBody) { payload = { status, success: false, error: { code: status ?? "SERVER_ERROR", message: String(serverBody), }, }; } else { payload = { status, success: false, error: { code: status ?? "SERVER_ERROR", message: error.response?.statusText || error.message || "Có lỗi xảy ra", }, }; } return Promise.reject(payload); }, ); export default axiosClient; /* ========================================================= * HTTP REQUEST * =======================================================*/ export const httpRequest = async ({ http, setLoading, msgSuccess = "Thành công", showMessageSuccess = false, showMessageFailed = true, onError, }: HttpRequestOptions): Promise => { try { setLoading?.(true); await delay(300); const res = await http; const safeRes = res ?? ({ success: false, error: { code: "UNKNOWN_ERROR", message: "Unknown error", }, } as ApiResponse); /* ================= SUCCESS ================= */ if ( safeRes.success === true || (safeRes.success === undefined && !safeRes.error) ) { if (showMessageSuccess) { toastSuccess({ msg: msgSuccess, }); } // Nếu API có data thì trả data // Nếu API trả trực tiếp object thì trả chính object đó return (safeRes.data !== undefined ? safeRes.data : safeRes) as T; } /* ================= FAIL ================= */ const error = { message: safeRes.error?.message || "Có lỗi xảy ra", code: safeRes.error?.code, }; onError?.(); throw error; } catch (error: any) { console.log("HTTP ERROR:", error); const message = error?.error?.message || error?.message || "Có lỗi xảy ra"; /* SHOW TOAST */ if (showMessageFailed) { toastWarn({ msg: message, }); } throw error; } finally { setLoading?.(false); } };