373 lines
8.4 KiB
TypeScript
373 lines
8.4 KiB
TypeScript
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<T = unknown> {
|
|
success?: boolean;
|
|
error?: ApiError | null;
|
|
data?: T;
|
|
items?: T[];
|
|
pagination?: unknown;
|
|
}
|
|
|
|
interface HttpRequestOptions<T> {
|
|
http: Promise<ApiResponse<T>>;
|
|
|
|
setLoading?: React.Dispatch<React.SetStateAction<boolean>>;
|
|
|
|
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<T = unknown>(url: string, config?: any): Promise<ApiResponse<T>>;
|
|
|
|
post<T = unknown>(
|
|
url: string,
|
|
data?: any,
|
|
config?: any,
|
|
): Promise<ApiResponse<T>>;
|
|
|
|
put<T = unknown>(
|
|
url: string,
|
|
data?: any,
|
|
config?: any,
|
|
): Promise<ApiResponse<T>>;
|
|
|
|
delete<T = unknown>(url: string, config?: any): Promise<ApiResponse<T>>;
|
|
};
|
|
|
|
/* =========================================================
|
|
* 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<string | null> | null = null;
|
|
|
|
const refreshAccessToken = async (): Promise<string | null> => {
|
|
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<InternalAxiosRequestConfig> => {
|
|
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<ApiResponse>) => {
|
|
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 <T>({
|
|
http,
|
|
setLoading,
|
|
msgSuccess = "Thành công",
|
|
showMessageSuccess = false,
|
|
showMessageFailed = true,
|
|
onError,
|
|
}: HttpRequestOptions<T>): Promise<T> => {
|
|
try {
|
|
setLoading?.(true);
|
|
|
|
await delay(300);
|
|
|
|
const res = await http;
|
|
|
|
const safeRes =
|
|
res ??
|
|
({
|
|
success: false,
|
|
error: {
|
|
code: "UNKNOWN_ERROR",
|
|
message: "Unknown error",
|
|
},
|
|
} as ApiResponse<T>);
|
|
|
|
/* ================= 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);
|
|
}
|
|
};
|