Files
TEDI-DDP-FE/src/services/index.ts
T
2026-10-26 16:03:54 +07:00

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);
}
};