Files
juip-web/src/api/base.ts

159 lines
4.0 KiB
TypeScript
Raw Normal View History

2026-07-30 13:55:28 +08:00
import {
API_BASE_URL,
type ApiResponse,
CLIENT_ID,
CLIENT_SECRET,
UnauthorizedError,
} from "@/lib/api"
let deviceToken: string | null = null
let deviceTokenExpire: Date | null = null
async function call<T = undefined>(
endpoint: string,
body?: string,
auth?: string,
): Promise<ApiResponse<T>> {
try {
const headers: HeadersInit = {
"Content-Type": "application/json",
}
if (auth) headers["Authorization"] = auth
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: "POST",
headers,
body,
})
if (response.status === 401) {
// 如果是 刷新Token 的接口本身报 401说明 RefreshToken 彻底失效,直接抛出错误
if (
endpoint.includes("/auth/token") ||
endpoint.includes("/auth/revoke")
) {
throw UnauthorizedError
}
// 动态引入 auth.ts 里的刷新函数,避免循环引用报错
const { refreshAuth } = await import("./auth")
// 尝试刷新 Token
const newTokens = await refreshAuth()
if (newTokens) {
// 刷新成功,用新 Token 重试当前请求(递归调用)
return call(endpoint, body, `Bearer ${newTokens.access_token}`)
} else {
// 刷新失败,抛出 401 错误,让上层处理跳转
throw UnauthorizedError
}
}
const contentType = response.headers.get("Content-Type") ?? "text/plain"
if (contentType.includes("text/plain")) {
const text = await response.text()
if (!response.ok) {
return {
success: false,
status: response.status,
message: text || "请求失败",
}
}
return { success: true, data: undefined as T }
}
if (contentType.includes("application/json")) {
const json = await response.json()
if (!response.ok) {
return {
success: false,
status: response.status,
message: json.message || json.error_description || "请求失败",
}
}
return { success: true, data: json }
}
throw new Error(`无法解析响应数据: ${contentType}`)
} catch (e) {
console.error("后端请求异常:", e)
return {
success: false,
status: 500,
message: (e as Error).message || "网络错误",
}
}
}
// ======================
// Public
// ======================
async function callPublic<R = undefined>(
endpoint: string,
data?: unknown,
): Promise<ApiResponse<R>> {
return call(endpoint, data ? JSON.stringify(data) : undefined)
}
// ======================
// Device
// ======================
async function callByDevice<R = undefined>(
endpoint: string,
data?: unknown,
): Promise<ApiResponse<R>> {
// 检查内存中的 Token 是否过期
if (!deviceToken || !deviceTokenExpire || new Date() >= deviceTokenExpire) {
// 用 btoa 生成 Basic 认证头
const basic = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)
const resp = await call<{
access_token: string
expires_in: number
}>(
"/api/auth/token",
JSON.stringify({ grant_type: "client_credentials" }),
`Basic ${basic}`,
)
if (!resp.success) {
return resp
}
deviceToken = resp.data.access_token
// 提前 60 秒过期,防止临界点失效
deviceTokenExpire = new Date(
Date.now() + (resp.data.expires_in - 60) * 1000,
)
}
return call(
endpoint,
data ? JSON.stringify(data) : undefined,
`Bearer ${deviceToken}`,
)
}
// ======================
// User
// ======================
async function callByUser<R = undefined>(
endpoint: string,
data?: unknown,
): Promise<ApiResponse<R>> {
// 这里我们假设用户登录后的 Token 存在 localStorage 里
// 如果你是用 Cookie 存 Token这里用 js-cookie 的 get 方法替换即可
const token = localStorage.getItem("auth_token")
if (!token) {
throw UnauthorizedError
}
return call(
endpoint,
data ? JSON.stringify(data) : undefined,
`Bearer ${token}`,
)
}
export { callByDevice, callByUser, callPublic }