Files
gameno-dashboard/src/api/axiosInstance.js
T
kavehhn 73670703bb fix: align FilePond upload with the Node.js API and proxy gallery previews
Use the filepond-image field, parse the API envelope, and load private files through the authenticated content endpoint so uploads persist and images display.
2026-08-15 18:46:26 +03:30

135 lines
4.1 KiB
JavaScript

// /src/api/axiosInstance.js
import axios from 'axios';
import Cookies from 'js-cookie';
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api';
const axiosInstance = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
timeout: 15000
});
// Request interceptor: attach token
axiosInstance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken') || Cookies.get('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Ask backend for Persian (or stored) localized error messages
const lang = localStorage.getItem('lang') || 'fa';
config.headers['Accept-Language'] = lang;
if (!config.params) config.params = {};
if (config.params.lang == null) {
config.params.lang = lang;
}
const isFormData = typeof FormData !== 'undefined' && config.data instanceof FormData;
if (isFormData) {
// Let the browser set multipart boundary — a bare multipart/form-data header breaks Multer
if (typeof config.headers?.delete === 'function') {
config.headers.delete('Content-Type');
config.headers.delete('content-type');
} else {
delete config.headers['Content-Type'];
delete config.headers['content-type'];
}
config.timeout = Math.max(config.timeout || 0, 120000);
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor: auto-refresh token on 401
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
axiosInstance.interceptors.response.use(
(response) => response.data,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (originalRequest.url.includes('/auth/login') || originalRequest.url.includes('/auth/refresh')) {
return Promise.reject(error);
}
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return axiosInstance(originalRequest);
})
.catch((err) => Promise.reject(err));
}
originalRequest._retry = true;
isRefreshing = true;
const refreshToken = localStorage.getItem('refreshToken') || Cookies.get('refreshToken');
if (!refreshToken) {
isRefreshing = false;
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(error);
}
try {
const refreshResponse = await axios.post(`${API_BASE_URL}/auth/refresh`, { refreshToken });
const data = refreshResponse.data?.data || refreshResponse.data;
const newAccessToken = data.accessToken;
const newRefreshToken = data.refreshToken;
localStorage.setItem('accessToken', newAccessToken);
Cookies.set('accessToken', newAccessToken);
if (newRefreshToken) {
localStorage.setItem('refreshToken', newRefreshToken);
Cookies.set('refreshToken', newRefreshToken);
}
axiosInstance.defaults.headers.common.Authorization = `Bearer ${newAccessToken}`;
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
processQueue(null, newAccessToken);
return axiosInstance(originalRequest);
} catch (refreshErr) {
processQueue(refreshErr, null);
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
Cookies.remove('accessToken');
Cookies.remove('refreshToken');
window.location.href = '/login';
return Promise.reject(refreshErr);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
export default axiosInstance;