114 lines
2.7 KiB
TypeScript
114 lines
2.7 KiB
TypeScript
import {
|
|
getAccessToken,
|
|
getRefreshToken,
|
|
setTokens,
|
|
clearTokens,
|
|
} from './token-store'
|
|
|
|
interface ApiResponse<T = unknown> {
|
|
success: boolean
|
|
data: T
|
|
message?: string
|
|
statusCode?: number
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
httpStatus: number
|
|
code: number
|
|
constructor(message: string, httpStatus: number, code?: number) {
|
|
super(message)
|
|
this.name = 'ApiError'
|
|
this.httpStatus = httpStatus
|
|
this.code = code ?? httpStatus
|
|
}
|
|
}
|
|
|
|
let refreshPromise: Promise<boolean> | null = null
|
|
|
|
async function tryRefresh(): Promise<boolean> {
|
|
const token = getRefreshToken()
|
|
if (!token) return false
|
|
|
|
if (!refreshPromise) {
|
|
refreshPromise = (async () => {
|
|
try {
|
|
const res = await fetch('/admin-api/auth/refresh', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ refreshToken: token }),
|
|
})
|
|
if (!res.ok) return false
|
|
const json: ApiResponse<{ accessToken: string; refreshToken: string }> = await res.json()
|
|
if (json.success) {
|
|
setTokens(json.data.accessToken, json.data.refreshToken)
|
|
return true
|
|
}
|
|
return false
|
|
} catch {
|
|
return false
|
|
} finally {
|
|
refreshPromise = null
|
|
}
|
|
})()
|
|
}
|
|
|
|
return refreshPromise
|
|
}
|
|
|
|
async function request<T>(
|
|
path: string,
|
|
options: RequestInit = {},
|
|
retried = false,
|
|
): Promise<T> {
|
|
const token = getAccessToken()
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...(options.headers as Record<string, string>),
|
|
}
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`
|
|
}
|
|
|
|
const res = await fetch(path, { ...options, headers })
|
|
|
|
if (res.status === 401 && !retried) {
|
|
const refreshed = await tryRefresh()
|
|
if (refreshed) {
|
|
return request<T>(path, options, true)
|
|
}
|
|
clearTokens()
|
|
window.location.href = '/login'
|
|
throw new ApiError('登录已过期', 401)
|
|
}
|
|
|
|
const json: ApiResponse<T> = await res.json()
|
|
if (!json.success) {
|
|
throw new ApiError(json.message || '请求失败', res.status, json.statusCode)
|
|
}
|
|
return json.data
|
|
}
|
|
|
|
export const api = {
|
|
get<T>(path: string) {
|
|
return request<T>(path)
|
|
},
|
|
post<T>(path: string, body?: unknown) {
|
|
return request<T>(path, {
|
|
method: 'POST',
|
|
body: body != null ? JSON.stringify(body) : undefined,
|
|
})
|
|
},
|
|
put<T>(path: string, body?: unknown) {
|
|
return request<T>(path, {
|
|
method: 'PUT',
|
|
body: body != null ? JSON.stringify(body) : undefined,
|
|
})
|
|
},
|
|
delete<T>(path: string, body?: unknown) {
|
|
return request<T>(path, {
|
|
method: 'DELETE',
|
|
body: body != null ? JSON.stringify(body) : undefined,
|
|
})
|
|
},
|
|
}
|