Add centralized API client with axios interceptors, remove vue-axios
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
33
src/api/auth.js
Normal file
33
src/api/auth.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import axios from 'axios';
|
||||
import { getCookie, setCookie, setCookieWithoutExpiration } from '@/utils/cookieUtil.js';
|
||||
|
||||
/**
|
||||
* Refresh the access token using the refresh token cookie.
|
||||
* Uses plain axios (not apiClient) to avoid interceptor loops.
|
||||
* @returns {Promise<string>} The new access token.
|
||||
*/
|
||||
export async function refreshTokenAndGetNew() {
|
||||
const api = '/api/oauth/token';
|
||||
const config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
};
|
||||
const data = {
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: getCookie('luciaRefreshToken'),
|
||||
};
|
||||
|
||||
const response = await axios.post(api, data, config);
|
||||
const newAccessToken = response.data.access_token;
|
||||
const newRefreshToken = response.data.refresh_token;
|
||||
|
||||
setCookieWithoutExpiration('luciaToken', newAccessToken);
|
||||
// Expire in ~6 months
|
||||
const expiredMs = new Date();
|
||||
expiredMs.setMonth(expiredMs.getMonth() + 6);
|
||||
const days = Math.ceil((expiredMs.getTime() - Date.now()) / (24 * 60 * 60 * 1000));
|
||||
setCookie('luciaRefreshToken', newRefreshToken, days);
|
||||
|
||||
return newAccessToken;
|
||||
}
|
||||
78
src/api/client.js
Normal file
78
src/api/client.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import axios from 'axios';
|
||||
import { getCookie, deleteCookie } from '@/utils/cookieUtil.js';
|
||||
|
||||
const apiClient = axios.create();
|
||||
|
||||
// Request interceptor: automatically attach Authorization header
|
||||
apiClient.interceptors.request.use((config) => {
|
||||
const token = getCookie('luciaToken');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// Response interceptor: handle 401 by attempting token refresh
|
||||
let isRefreshing = false;
|
||||
let pendingRequests = [];
|
||||
|
||||
function onRefreshSuccess(newToken) {
|
||||
pendingRequests.forEach((cb) => cb(newToken));
|
||||
pendingRequests = [];
|
||||
}
|
||||
|
||||
function onRefreshFailure(error) {
|
||||
pendingRequests.forEach((cb) => cb(null, error));
|
||||
pendingRequests = [];
|
||||
}
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
// Only attempt refresh on 401, and not for auth endpoints or already-retried requests
|
||||
if (
|
||||
error.response?.status !== 401 ||
|
||||
originalRequest._retried ||
|
||||
originalRequest.url === '/api/oauth/token'
|
||||
) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (isRefreshing) {
|
||||
// Queue this request until the refresh completes
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingRequests.push((newToken, err) => {
|
||||
if (err) return reject(err);
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
||||
originalRequest._retried = true;
|
||||
resolve(apiClient(originalRequest));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
isRefreshing = true;
|
||||
originalRequest._retried = true;
|
||||
|
||||
try {
|
||||
// Dynamic import to avoid circular dependency with login store
|
||||
const { refreshTokenAndGetNew } = await import('@/api/auth.js');
|
||||
const newToken = await refreshTokenAndGetNew();
|
||||
isRefreshing = false;
|
||||
onRefreshSuccess(newToken);
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
||||
return apiClient(originalRequest);
|
||||
} catch (refreshError) {
|
||||
isRefreshing = false;
|
||||
onRefreshFailure(refreshError);
|
||||
|
||||
// Refresh failed: clear auth and redirect to login
|
||||
deleteCookie('luciaToken');
|
||||
window.location.href = '/login';
|
||||
return Promise.reject(refreshError);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default apiClient;
|
||||
Reference in New Issue
Block a user