更新登录功能
This commit is contained in:
228
src/api/base.ts
228
src/api/base.ts
@@ -3,53 +3,32 @@ import {
|
||||
type ApiResponse,
|
||||
CLIENT_ID,
|
||||
CLIENT_SECRET,
|
||||
UnauthorizedError,
|
||||
} from "@/lib/api"
|
||||
import { clearSession, getUserToken } from "./token"
|
||||
|
||||
let deviceToken: string | null = null
|
||||
let deviceTokenExpire: Date | null = null
|
||||
type CallMode = "public" | "device" | "user"
|
||||
|
||||
async function call<T = undefined>(
|
||||
async function request<R = undefined>(
|
||||
endpoint: string,
|
||||
body?: string,
|
||||
auth?: string,
|
||||
): Promise<ApiResponse<T>> {
|
||||
init: {
|
||||
method: "GET" | "POST"
|
||||
body?: string
|
||||
auth?: string
|
||||
},
|
||||
): Promise<ApiResponse<R>> {
|
||||
try {
|
||||
const headers: HeadersInit = {
|
||||
"Content-Type": "application/json",
|
||||
const headers: HeadersInit = {}
|
||||
if (init.body !== undefined) {
|
||||
headers["Content-Type"] = "application/json"
|
||||
}
|
||||
if (auth) headers["Authorization"] = auth
|
||||
if (init.auth) headers.Authorization = init.auth
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
method: "POST",
|
||||
method: init.method,
|
||||
headers,
|
||||
body,
|
||||
body: init.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")) {
|
||||
@@ -61,11 +40,18 @@ async function call<T = undefined>(
|
||||
message: text || "请求失败",
|
||||
}
|
||||
}
|
||||
return { success: true, data: undefined as T }
|
||||
return { success: true, data: undefined as R }
|
||||
}
|
||||
|
||||
if (contentType.includes("application/json")) {
|
||||
const json = await response.json()
|
||||
const json = (await response.json()) as R & {
|
||||
message?: string
|
||||
error_description?: string
|
||||
Code?: number
|
||||
Message?: string
|
||||
Data?: unknown
|
||||
data?: unknown
|
||||
}
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -73,6 +59,20 @@ async function call<T = undefined>(
|
||||
message: json.message || json.error_description || "请求失败",
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof json.Code === "number") {
|
||||
if (json.Code === 10000) {
|
||||
return {
|
||||
success: true,
|
||||
data: (json.Data ?? json.data ?? undefined) as R,
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: json.Message || "请求失败",
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data: json }
|
||||
}
|
||||
|
||||
@@ -87,72 +87,124 @@ async function call<T = undefined>(
|
||||
}
|
||||
}
|
||||
|
||||
// ======================
|
||||
// Public
|
||||
// ======================
|
||||
let deviceToken: string | null = null
|
||||
let deviceTokenExpire = 0
|
||||
let deviceTokenPromise: Promise<string | null> | null = null
|
||||
|
||||
async function acquireDeviceToken(): Promise<string | null> {
|
||||
if (deviceToken && Date.now() < deviceTokenExpire) return deviceToken
|
||||
if (!deviceTokenPromise) {
|
||||
const basic = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)
|
||||
deviceTokenPromise = request<{
|
||||
access_token: string
|
||||
expires_in: number
|
||||
}>("/api/auth/token", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ grant_type: "client_credentials" }),
|
||||
auth: `Basic ${basic}`,
|
||||
})
|
||||
.then(resp => {
|
||||
if (!resp.success) {
|
||||
deviceToken = null
|
||||
return null
|
||||
}
|
||||
deviceToken = resp.data.access_token
|
||||
deviceTokenExpire = Date.now() + (resp.data.expires_in - 60) * 1000
|
||||
return resp.data.access_token
|
||||
})
|
||||
.finally(() => {
|
||||
deviceTokenPromise = null
|
||||
})
|
||||
}
|
||||
return deviceTokenPromise
|
||||
}
|
||||
|
||||
const inflight = new Map<string, Promise<ApiResponse<unknown>>>()
|
||||
|
||||
function dedupGet<R>(
|
||||
endpoint: string,
|
||||
run: () => Promise<ApiResponse<R>>,
|
||||
): Promise<ApiResponse<R>> {
|
||||
const existing = inflight.get(endpoint)
|
||||
if (existing) return existing as Promise<ApiResponse<R>>
|
||||
const promise = run().finally(() => inflight.delete(endpoint))
|
||||
inflight.set(endpoint, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
async function call<R = undefined>(
|
||||
mode: CallMode,
|
||||
endpoint: string,
|
||||
data?: unknown,
|
||||
method: "GET" | "POST" = "POST",
|
||||
): Promise<ApiResponse<R>> {
|
||||
const body = data === undefined ? undefined : JSON.stringify(data)
|
||||
|
||||
const run = async (): Promise<ApiResponse<R>> => {
|
||||
let auth: string | undefined
|
||||
if (mode === "user") {
|
||||
const token = getUserToken()
|
||||
if (!token) {
|
||||
return {
|
||||
success: false,
|
||||
status: 401,
|
||||
message: "未登录或会话已过期",
|
||||
}
|
||||
}
|
||||
auth = `Bearer ${token}`
|
||||
} else if (mode === "device") {
|
||||
const token = await acquireDeviceToken()
|
||||
if (!token) {
|
||||
return {
|
||||
success: false,
|
||||
status: 401,
|
||||
message: "设备凭证获取失败",
|
||||
}
|
||||
}
|
||||
auth = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const resp = await request<R>(endpoint, { method, body, auth })
|
||||
|
||||
if (!resp.success && resp.status === 401) {
|
||||
if (mode === "user") {
|
||||
clearSession()
|
||||
} else if (mode === "device") {
|
||||
deviceToken = null
|
||||
deviceTokenExpire = 0
|
||||
}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
if (method === "GET" && data === undefined) {
|
||||
return dedupGet<R>(endpoint, run)
|
||||
}
|
||||
return run()
|
||||
}
|
||||
|
||||
async function callPublic<R = undefined>(
|
||||
endpoint: string,
|
||||
data?: unknown,
|
||||
method: "GET" | "POST" = "POST",
|
||||
): Promise<ApiResponse<R>> {
|
||||
return call(endpoint, data ? JSON.stringify(data) : undefined)
|
||||
return call<R>("public", endpoint, data, method)
|
||||
}
|
||||
|
||||
// ======================
|
||||
// Device
|
||||
// ======================
|
||||
async function callByDevice<R = undefined>(
|
||||
endpoint: string,
|
||||
data?: unknown,
|
||||
method: "GET" | "POST" = "POST",
|
||||
): 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}`,
|
||||
)
|
||||
return call<R>("device", endpoint, data, method)
|
||||
}
|
||||
|
||||
// ======================
|
||||
// User
|
||||
// ======================
|
||||
async function callByUser<R = undefined>(
|
||||
endpoint: string,
|
||||
data?: unknown,
|
||||
method: "GET" | "POST" = "POST",
|
||||
): 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}`,
|
||||
)
|
||||
return call<R>("user", endpoint, data, method)
|
||||
}
|
||||
|
||||
export { callByDevice, callByUser, callPublic }
|
||||
|
||||
Reference in New Issue
Block a user