159 lines
4.0 KiB
TypeScript
159 lines
4.0 KiB
TypeScript
|
|
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 }
|