167 lines
6.2 KiB
JavaScript
167 lines
6.2 KiB
JavaScript
import { refresh as refreshRequest } from '@/api/auth.js'
|
|
import { isAccessTokenExpiring, session, signOut, updateTokenPair, getSessionRevision } from '@/stores/session.js'
|
|
import { requireOnline } from '@/utils/network.js'
|
|
|
|
let refreshPromise = null
|
|
let refreshRevision = null
|
|
let redirectingToLogin = false
|
|
|
|
function responseCode(response) {
|
|
return String(response?.data?.code ?? response?.statusCode ?? '')
|
|
}
|
|
|
|
function isUnauthorized(responseOrError) {
|
|
const statusCode = Number(responseOrError?.statusCode ?? responseOrError?.response?.statusCode ?? 0)
|
|
const code = String(responseOrError?.code ?? responseCode(responseOrError))
|
|
return statusCode === 401 || code === '401' || code === 'A0401'
|
|
}
|
|
|
|
function sessionExpiredError(source) {
|
|
const error = new Error('登录已过期,请重新登录')
|
|
error.code = source?.code || 'A0401'
|
|
error.statusCode = source?.statusCode || 401
|
|
return error
|
|
}
|
|
|
|
function redirectAfterSessionExpiry() {
|
|
if (redirectingToLogin) return
|
|
redirectingToLogin = true
|
|
if (typeof uni !== 'undefined' && typeof uni.showToast === 'function') {
|
|
uni.showToast({ title: '登录已过期,请重新登录', icon: 'none', duration: 2200 })
|
|
}
|
|
if (typeof uni !== 'undefined' && typeof uni.reLaunch === 'function') {
|
|
uni.reLaunch({
|
|
url: '/pages/login/login',
|
|
complete() { setTimeout(() => { redirectingToLogin = false }, 1000) }
|
|
})
|
|
}
|
|
}
|
|
|
|
function expireSession(source, observedRefreshToken = session.refreshToken) {
|
|
if (observedRefreshToken && session.refreshToken && observedRefreshToken !== session.refreshToken) return session.token
|
|
const hadSession = Boolean(session.currentUser || session.token || session.refreshToken)
|
|
signOut()
|
|
if (hadSession) redirectAfterSessionExpiry()
|
|
throw sessionExpiredError(source)
|
|
}
|
|
|
|
function networkError(error, fallback = '网络连接失败') {
|
|
const next = new Error(error?.errMsg || fallback)
|
|
next.network = true
|
|
return next
|
|
}
|
|
|
|
function rawRequest(options, accessToken) {
|
|
const baseUrl = session.apiBase || '/api/platform/v1'
|
|
const headers = { ...(options.header || {}) }
|
|
if (accessToken) headers.Authorization = `Bearer ${accessToken}`
|
|
if (session.requestId) headers['X-Request-Id'] = session.requestId
|
|
return new Promise((resolve, reject) => {
|
|
uni.request({
|
|
...options,
|
|
url: `${baseUrl}${options.url}`,
|
|
header: headers,
|
|
success: resolve,
|
|
fail: (error) => reject(networkError(error))
|
|
})
|
|
})
|
|
}
|
|
|
|
function requireSameSession(revision) {
|
|
if (getSessionRevision() !== revision) throw new Error('登录会话已变更,请重新操作')
|
|
}
|
|
|
|
async function refreshAccessToken() {
|
|
const revision = getSessionRevision()
|
|
const observedRefreshToken = session.refreshToken
|
|
if (!observedRefreshToken) return expireSession(null, observedRefreshToken)
|
|
if (!refreshPromise || refreshRevision !== revision) {
|
|
refreshRevision = revision
|
|
refreshPromise = (async () => {
|
|
if (session.refreshToken !== observedRefreshToken && session.token) return session.token
|
|
try {
|
|
const tokens = await refreshRequest(observedRefreshToken)
|
|
requireSameSession(revision)
|
|
if (session.refreshToken !== observedRefreshToken) {
|
|
if (session.token) return session.token
|
|
throw sessionExpiredError()
|
|
}
|
|
updateTokenPair(tokens)
|
|
redirectingToLogin = false
|
|
return session.token
|
|
} catch (error) {
|
|
requireSameSession(revision)
|
|
if (isUnauthorized(error)) return expireSession(error, observedRefreshToken)
|
|
throw error
|
|
}
|
|
})()
|
|
const pending = refreshPromise
|
|
const release = () => { if (refreshPromise === pending) { refreshPromise = null; refreshRevision = null } }
|
|
pending.then(release, release) // Original pending promise still propagates failures to callers.
|
|
}
|
|
return refreshPromise
|
|
}
|
|
|
|
async function validAccessToken() {
|
|
if (!session.token || isAccessTokenExpiring(session.token)) return refreshAccessToken()
|
|
return session.token
|
|
}
|
|
|
|
export function ensureAccessToken() {
|
|
return validAccessToken()
|
|
}
|
|
|
|
async function runAuthenticated(execute) {
|
|
requireOnline()
|
|
const revision = getSessionRevision()
|
|
let accessToken = await validAccessToken()
|
|
requireSameSession(revision)
|
|
let response = await execute(accessToken)
|
|
requireSameSession(revision)
|
|
if (!isUnauthorized(response)) return response
|
|
accessToken = session.token && session.token !== accessToken ? session.token : await refreshAccessToken()
|
|
requireSameSession(revision)
|
|
const activeRefreshToken = session.refreshToken
|
|
response = await execute(accessToken)
|
|
requireSameSession(revision)
|
|
if (isUnauthorized(response)) expireSession(response, activeRefreshToken)
|
|
return response
|
|
}
|
|
|
|
export function apiRequest(options) {
|
|
return runAuthenticated((accessToken) => rawRequest(options, accessToken)).then((response) => {
|
|
const body = response.data || {}
|
|
const code = responseCode(response)
|
|
if (code !== '200') {
|
|
const error = new Error(body.message || '服务端请求失败')
|
|
error.code = body.code || response.statusCode
|
|
error.statusCode = response.statusCode
|
|
error.requestId = body.requestId || response.header?.['X-Request-Id'] || response.header?.['x-request-id']
|
|
error.traceId = body.traceId || response.header?.['X-Trace-Id'] || response.header?.['x-trace-id']
|
|
throw error
|
|
}
|
|
return body.data
|
|
})
|
|
}
|
|
|
|
export function apiDownload(options) {
|
|
return runAuthenticated((accessToken) => new Promise((resolve, reject) => {
|
|
const headers = { ...(options.header || {}) }
|
|
if (accessToken) headers.Authorization = `Bearer ${accessToken}`
|
|
if (session.requestId) headers['X-Request-Id'] = session.requestId
|
|
uni.downloadFile({ ...options, header: headers, success: resolve, fail: (error) => reject(networkError(error, '文件下载失败')) })
|
|
}))
|
|
}
|
|
|
|
export function apiUpload(options, onProgress) {
|
|
return runAuthenticated((accessToken) => new Promise((resolve, reject) => {
|
|
const headers = { ...(options.header || {}) }
|
|
if (accessToken) headers.Authorization = `Bearer ${accessToken}`
|
|
if (session.requestId) headers['X-Request-Id'] = session.requestId
|
|
const task = uni.uploadFile({ ...options, header: headers, success: resolve, fail: (error) => reject(networkError(error)) })
|
|
if (task && typeof task.onProgressUpdate === 'function' && typeof onProgress === 'function') task.onProgressUpdate(({ progress }) => onProgress(progress))
|
|
}))
|
|
}
|
|
|
|
export { isUnauthorized }
|