feat:update web

This commit is contained in:
TuanVT
2026-05-24 23:33:43 +07:00
parent b6ccde2c74
commit b0baa509d9
58 changed files with 4920 additions and 292 deletions
+218 -81
View File
@@ -1,7 +1,6 @@
import axios, {
AxiosError,
AxiosInstance,
AxiosResponse,
InternalAxiosRequestConfig,
} from "axios";
@@ -9,17 +8,22 @@ import { delay } from "@/common/funcs/delay";
import { getKeyCert } from "@/common/funcs/optionConvert";
import { toastInfo, toastSuccess, toastWarn } from "@/common/funcs/toast";
import { logout } from "@/redux/reducer/auth";
import { logout, setToken } from "@/redux/reducer/auth";
import { setInfoUser } from "@/redux/reducer/user";
import { store } from "@/redux/store";
/* =========================================================
* TYPES
* =======================================================*/
interface ApiError {
code: number;
code: string | number;
message: string;
}
interface ApiResponse<T = unknown> {
error: ApiError;
export interface ApiResponse<T = unknown> {
success?: boolean;
error?: ApiError | null;
data?: T;
items?: T[];
pagination?: unknown;
@@ -42,16 +46,20 @@ interface HttpRequestOptions<T> {
isData?: boolean;
}
const axiosClient = axios.create({
headers: {
"content-type": "application/json",
},
/* =========================================================
* AXIOS CLIENT
* =======================================================*/
baseURL: "/api-proxy",
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>>;
@@ -70,16 +78,120 @@ const axiosClient = axios.create({
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<{
token?: string;
accessToken?: string;
refreshToken?: string;
}>("/Auth/refresh", {
identifier: refreshToken,
});
const responseData = response.data;
const newToken = responseData?.token || 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 || "",
accessExpiresAt: prev?.accessExpiresAt || "",
refreshExpiresAt: prev?.refreshExpiresAt || "",
avatar: prev?.avatar || "",
fullname: prev?.fullname || "",
}),
);
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(),
@@ -91,106 +203,131 @@ axiosClient.interceptors.request.use(
},
);
/* =========================================================
* RESPONSE INTERCEPTOR
* =======================================================*/
axiosClient.interceptors.response.use(
(response) => {
return response.data;
return (
response.data || {
success: false,
error: {
code: "UNKNOWN_ERROR",
message: "Unknown error",
},
}
);
},
(error: AxiosError<ApiError>) => {
return Promise.reject(error.response?.data || 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({
success: false,
error: {
code: status,
message: error.response?.statusText || "Unauthorized",
},
});
}
return Promise.reject(
data || {
success: false,
error: {
code: "SERVER_ERROR",
message: "Có lỗi xảy ra",
},
},
);
},
);
export default axiosClient;
/* =========================================================
* HTTP REQUEST
* =======================================================*/
export const httpRequest = async <T>({
http,
setLoading,
msgSuccess = "Thành công",
showMessageSuccess = false,
showMessageFailed = false,
onError,
isDropdown = false,
isPagination = false,
isData = false,
}: HttpRequestOptions<T>) => {
}: HttpRequestOptions<T>): Promise<T> => {
try {
setLoading?.(true);
await delay(500);
await delay(300);
const res = await http;
if (res.error.code === 0) {
const safeRes =
res ??
({
success: false,
error: {
code: "UNKNOWN_ERROR",
message: "Unknown error",
},
} as ApiResponse<T>);
// ================= SUCCESS =================
if (safeRes.success) {
if (showMessageSuccess) {
toastSuccess({
msg: msgSuccess || res.error.message,
});
toastSuccess({ msg: msgSuccess });
}
if (isDropdown) {
return res.items || [];
}
if (isPagination) {
return {
...res.data,
items: res.items || [],
pagination: res.pagination,
};
}
if (isData) {
return res;
}
return res.data || true;
return safeRes.data as T;
}
// ================= FAIL =================
const error = {
message: safeRes.error?.message || "Có lỗi xảy ra",
code: safeRes.error?.code,
};
onError?.();
throw new Error(res.error.message);
} catch (error: unknown) {
const err = error as AxiosError<ApiResponse> | Error;
const errorData = (err as AxiosError<ApiResponse>)?.response?.data;
if (errorData?.error?.code === 401 || errorData?.error?.code === 403) {
store.dispatch(logout());
store.dispatch(setInfoUser(null));
if (showMessageFailed) {
toastWarn({
msg: errorData?.error?.message || "Có lỗi đã xảy ra!",
});
}
} else if (err instanceof Error) {
if (err.message === "ERR_NETWORK" || err.message === "ECONNABORTED") {
if (showMessageFailed) {
toastInfo({
msg: "Kiểm tra kết nối internet",
});
}
} else {
if (showMessageFailed) {
toastWarn({
msg: err.message || "Có lỗi đã xảy ra!",
});
}
}
} else {
if (showMessageFailed) {
toastWarn({
msg: "Có lỗi đã xảy ra!",
});
}
}
throw error; // 👈 QUAN TRỌNG
} catch (error) {
throw error; // 👈 QUAN TRỌNG: KHÔNG NUỐT
} finally {
setLoading?.(false);
}