78 lines
2.4 KiB
TypeScript
78 lines
2.4 KiB
TypeScript
import axios, { type InternalAxiosRequestConfig } from 'axios'
|
|||
|
|
import { parseApiJsonResponse } from '@chongchong/shared'
|
||
|
|
import { refreshAuthenticatedSession } from './http'
|
||
|
|
|
||
|
|
interface CachedOverseasToken {
|
||
|
|
accessToken: string
|
||
|
|
refreshAfter: number
|
||
|
|
}
|
||
|
|
|
||
|
|
type RetryableOverseasRequestConfig = InternalAxiosRequestConfig & { _overseasAuthRetry?: boolean }
|
||
|
|
|
||
|
|
const TOKEN_REFRESH_SAFETY_WINDOW_MS = 30_000
|
||
|
|
|
||
|
|
let cachedToken: CachedOverseasToken | null = null
|
||
|
|
|
||
|
|
export const overseasHttp = axios.create({
|
||
|
|
baseURL: import.meta.env.VITE_OVERSEAS_API_BASE_URL || '/overseas-api',
|
||
|
|
timeout: 15_000,
|
||
|
|
transformResponse: [(data: unknown) => parseApiJsonResponse(data)],
|
||
|
|
})
|
||
|
|
|
||
|
|
function currentOverseasAccessToken() {
|
||
|
|
return cachedToken && cachedToken.refreshAfter > Date.now() ? cachedToken.accessToken : ''
|
||
|
|
}
|
||
|
|
|
||
|
|
async function requireOverseasAccessToken() {
|
||
|
|
const current = currentOverseasAccessToken()
|
||
|
|
if (current) return current
|
||
|
|
await refreshAuthenticatedSession()
|
||
|
|
const refreshed = currentOverseasAccessToken()
|
||
|
|
if (!refreshed) throw new Error('当前环境未签发海外访问令牌')
|
||
|
|
return refreshed
|
||
|
|
}
|
||
|
|
|
||
|
|
export function replaceOverseasAccessToken(accessToken: string | null, expiresIn: number) {
|
||
|
|
if (!accessToken && expiresIn === 0) {
|
||
|
|
cachedToken = null
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if (!accessToken || !Number.isFinite(expiresIn) || expiresIn <= 0) {
|
||
|
|
throw new Error('海外访问令牌响应无效')
|
||
|
|
}
|
||
|
|
cachedToken = {
|
||
|
|
accessToken,
|
||
|
|
refreshAfter: Date.now() + Math.max(0, expiresIn * 1000 - TOKEN_REFRESH_SAFETY_WINDOW_MS),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function clearOverseasAccessToken() {
|
||
|
|
cachedToken = null
|
||
|
|
}
|
||
|
|
|
||
|
|
overseasHttp.interceptors.request.use(async (config) => {
|
||
|
|
const accessToken = await requireOverseasAccessToken()
|
||
|
|
config.headers.Authorization = `Bearer ${accessToken}`
|
||
|
|
return config
|
||
|
|
})
|
||
|
|
|
||
|
|
overseasHttp.interceptors.response.use(
|
||
|
|
(response) => response.data,
|
||
|
|
async (error) => {
|
||
|
|
const config = error.config as RetryableOverseasRequestConfig | undefined
|
||
|
|
if (error.response?.status !== 401 || !config || config._overseasAuthRetry) {
|
||
|
|
return Promise.reject(error)
|
||
|
|
}
|
||
|
|
config._overseasAuthRetry = true
|
||
|
|
clearOverseasAccessToken()
|
||
|
|
try {
|
||
|
|
await refreshAuthenticatedSession()
|
||
|
|
const accessToken = await requireOverseasAccessToken()
|
||
|
|
config.headers.Authorization = `Bearer ${accessToken}`
|
||
|
|
return overseasHttp.request(config)
|
||
|
|
} catch {
|
||
|
|
return Promise.reject(error)
|
||
|
|
}
|
||
|
|
},
|
||
|
|
)
|