feat: initial commit - oncology literature search platform
OncoLit: a multi-tenant oncology literature search, feed, and collaboration platform. Built with FastAPI + Vue 3 + PostgreSQL. Includes PubMed pipeline, drug approvals, AI summaries, and systematic review tools.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import axios from 'axios'
|
||||
import { router } from '../router'
|
||||
import { useGlobalLoading } from '../composables/useGlobalLoading'
|
||||
|
||||
const { increment, decrement } = useGlobalLoading()
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE || '/api/v1',
|
||||
timeout: 15000,
|
||||
})
|
||||
|
||||
// 内存级 access token(不写入任何持久存储)
|
||||
let _accessToken = ''
|
||||
|
||||
export function setAccessToken(token: string) { _accessToken = token }
|
||||
export function clearAccessToken() { _accessToken = '' }
|
||||
export function getAccessToken(): string { return _accessToken }
|
||||
|
||||
function getCookie(name: string): string | null {
|
||||
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`))
|
||||
if (!match || match[1] === undefined) return null
|
||||
return decodeURIComponent(match[1])
|
||||
}
|
||||
|
||||
// 请求拦截器:注入 JWT + CSRF Token + 计数在途请求
|
||||
api.interceptors.request.use((config) => {
|
||||
increment()
|
||||
if (_accessToken) {
|
||||
config.headers.Authorization = `Bearer ${_accessToken}`
|
||||
}
|
||||
// CSRF double-submit: 对 state-changing 请求注入 csrf_token
|
||||
if (config.method && !['GET', 'HEAD', 'OPTIONS'].includes(config.method.toUpperCase())) {
|
||||
const csrfToken = getCookie('csrf_token')
|
||||
if (csrfToken) {
|
||||
config.headers['X-CSRF-Token'] = csrfToken
|
||||
}
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// ── Refresh mutex ──
|
||||
// 防止并发 401 导致多条 refresh 请求竞争:同一时间只有一条 refresh 请求,
|
||||
// 其余 401 排队等待其完成后再重试。
|
||||
let _refreshing: Promise<boolean> | null = null
|
||||
|
||||
// 响应拦截器:自动刷新 token + 全局错误处理 + 递减在途请求
|
||||
api.interceptors.response.use(
|
||||
(res) => { decrement(); return res },
|
||||
async (error) => {
|
||||
decrement()
|
||||
const originalRequest = error.config
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true
|
||||
|
||||
// refresh_token 由浏览器自动携带(HttpOnly cookie)
|
||||
// 已有 refresh 请求在进行中,等待其完成
|
||||
if (_refreshing) {
|
||||
const ok = await _refreshing
|
||||
if (ok) {
|
||||
originalRequest.headers.Authorization = `Bearer ${getAccessToken()}`
|
||||
return api(originalRequest)
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// 发起 refresh 请求,cookie 自动携带
|
||||
_refreshing = axios
|
||||
.post('/api/v1/auth/refresh')
|
||||
.then((res) => {
|
||||
if (!res.data.access_token) {
|
||||
clearAccessToken()
|
||||
return false
|
||||
}
|
||||
setAccessToken(res.data.access_token)
|
||||
return true
|
||||
})
|
||||
.catch(() => {
|
||||
clearAccessToken()
|
||||
if (router.currentRoute.value.meta?.requiresAuth) {
|
||||
router.push('/auth/login')
|
||||
}
|
||||
return false
|
||||
})
|
||||
.finally(() => {
|
||||
_refreshing = null
|
||||
})
|
||||
|
||||
const ok = await _refreshing
|
||||
if (ok) {
|
||||
originalRequest.headers.Authorization = `Bearer ${getAccessToken()}`
|
||||
return api(originalRequest)
|
||||
}
|
||||
}
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user