更新登录功能

This commit is contained in:
Eamon
2026-08-13 18:28:20 +08:00
parent 115ee3bfeb
commit 4235458bea
25 changed files with 1900 additions and 873 deletions

View File

@@ -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 }