feat: initial commit - oncology literature search platform
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s

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:
34047007@qq.com
2026-07-27 07:59:18 +08:00
commit a6cd99a4ca
473 changed files with 151472 additions and 0 deletions
+96
View File
@@ -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)
},
)