import axios from 'axios'; import router from './router'; import { clearStaffSession, reconcileStaffSessionFromToken, useAuthStore } from './stores/auth'; import { ensureStaffSession, resetStaffSessionHydration } from './utils/session-hydrate'; import { ADMIN_LOCALE_STORAGE_KEY } from './i18n'; const api = axios.create({ baseURL: '/api' }); let handlingAuthInvalid = false; let handling403Portal = false; const PORTAL_MISMATCH_CODES = new Set(['ADMIN_ACCESS_ONLY', 'AGENT_ACCESS_ONLY']); function requestPath(config: { url?: string; baseURL?: string } | undefined): string { if (!config?.url) return ''; const base = config.baseURL ?? ''; return `${base}${config.url}`; } function isLoginRequest(config: { url?: string; baseURL?: string } | undefined): boolean { return requestPath(config).includes('/auth/login'); } function isInvalidCredentialsResponse(data: unknown): boolean { return ( typeof data === 'object' && data !== null && 'code' in data && (data as { code?: unknown }).code === 'INVALID_CREDENTIALS' ); } async function redirectToLoginForInvalidSession() { if (handlingAuthInvalid) return; handlingAuthInvalid = true; try { clearStaffSession(); resetStaffSessionHydration(); if (router.currentRoute.value.path !== '/login') { await router.replace('/login'); } } finally { handlingAuthInvalid = false; } } let _lastReconciledToken: string | null = null; api.interceptors.request.use((config) => { const t = localStorage.getItem('manage_token'); if (t) config.headers.Authorization = `Bearer ${t}`; const locale = localStorage.getItem(ADMIN_LOCALE_STORAGE_KEY) || 'zh-CN'; config.headers['X-Locale'] = locale; // 只在 token 变更时才重新 decode JWT + reconcile,避免并发请求时多次执行同步开销 if (t !== _lastReconciledToken) { reconcileStaffSessionFromToken(); _lastReconciledToken = t; } const auth = useAuthStore(); const path = requestPath(config); if (path.includes('/admin/') && auth.isAgent.value) { return Promise.reject( Object.assign(new Error('Agent portal blocked admin API'), { isPortalMismatch: true, blockedPath: path, }), ); } if (path.includes('/agent/') && auth.isAdmin.value) { return Promise.reject( Object.assign(new Error('Admin portal blocked agent API'), { isPortalMismatch: true, blockedPath: path, }), ); } return config; }); api.interceptors.response.use( async (res) => { if (!isLoginRequest(res.config) && isInvalidCredentialsResponse(res.data)) { await redirectToLoginForInvalidSession(); return Promise.reject( Object.assign(new Error((res.data as { error?: string }).error || 'Invalid credentials'), { response: res, config: res.config, }), ); } return res; }, async (err) => { if (err.isPortalMismatch) { await ensureStaffSession(); if (router.currentRoute.value.path !== '/login') { await router.replace('/'); } return Promise.reject(err); } if ( !isLoginRequest(err.config) && (err.response?.status === 401 || isInvalidCredentialsResponse(err.response?.data)) ) { await redirectToLoginForInvalidSession(); } if ( err.response?.status === 403 && !handling403Portal && PORTAL_MISMATCH_CODES.has(err.response?.data?.code) ) { handling403Portal = true; await ensureStaffSession(); handling403Portal = false; const auth = useAuthStore(); const path = requestPath(err.config); const isAdminApi = path.includes('/admin/'); const isAgentApi = path.includes('/agent/'); if ((isAdminApi && auth.isAgent.value) || (isAgentApi && auth.isAdmin.value)) { if (router.currentRoute.value.path !== '/login') { await router.replace('/'); } return Promise.reject(err); } if (err.config) { return api.request(err.config); } clearStaffSession(); if (router.currentRoute.value.path !== '/login') { await router.replace('/login'); } } return Promise.reject(err); }, ); export default api;