首页页面
This commit is contained in:
101
src/api/auth.ts
Normal file
101
src/api/auth.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { ApiResponse } from "@/lib/api"
|
||||
import { callByDevice, callByUser } from "./base"
|
||||
|
||||
// 用户类型
|
||||
export interface User {
|
||||
id: number
|
||||
username: string
|
||||
// ...
|
||||
}
|
||||
|
||||
export type LoginMode = "phone_code" | "password"
|
||||
|
||||
export async function login(props: {
|
||||
username: string
|
||||
password: string
|
||||
remember: boolean
|
||||
mode: LoginMode
|
||||
}): Promise<ApiResponse> {
|
||||
// 使用 callByDevice 去拿 Token (设备级认证)
|
||||
const result = await callByDevice<{
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
}>("/api/auth/token", {
|
||||
...props,
|
||||
grant_type: "password",
|
||||
login_type: props.mode,
|
||||
})
|
||||
|
||||
if (!result.success) return result
|
||||
|
||||
// 登录成功,存入 localStorage
|
||||
const { access_token, refresh_token, expires_in } = result.data
|
||||
localStorage.setItem("auth_token", access_token)
|
||||
localStorage.setItem("auth_refresh", refresh_token)
|
||||
localStorage.setItem(
|
||||
"auth_expires_in",
|
||||
String(Date.now() + expires_in * 1000),
|
||||
)
|
||||
|
||||
return { success: true, data: undefined }
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
const accessToken = localStorage.getItem("auth_token")
|
||||
const refreshToken = localStorage.getItem("auth_refresh")
|
||||
|
||||
if (accessToken && refreshToken) {
|
||||
await callByUser("/api/auth/revoke", {
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
})
|
||||
}
|
||||
|
||||
// 清除前端本地缓存
|
||||
localStorage.removeItem("auth_token")
|
||||
localStorage.removeItem("auth_refresh")
|
||||
localStorage.removeItem("auth_expires_in")
|
||||
|
||||
// 强制跳转到登录页
|
||||
window.location.href = "/login"
|
||||
}
|
||||
|
||||
// 获取当前用户信息 (校验 Token 是否有效)
|
||||
export async function getProfile() {
|
||||
return await callByUser<User>("/api/auth/introspect")
|
||||
}
|
||||
|
||||
// 刷新 Token
|
||||
export async function refreshAuth(): Promise<{
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
} | null> {
|
||||
const refreshToken = localStorage.getItem("auth_refresh")
|
||||
if (!refreshToken) return null
|
||||
|
||||
const resp = await callByDevice<{
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
}>(`/api/auth/token`, {
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
})
|
||||
|
||||
if (!resp.success) {
|
||||
localStorage.removeItem("auth_refresh")
|
||||
return null
|
||||
}
|
||||
|
||||
// 刷新成功,更新本地缓存
|
||||
const { access_token, refresh_token, expires_in } = resp.data
|
||||
localStorage.setItem("auth_token", access_token)
|
||||
localStorage.setItem("auth_refresh", refresh_token)
|
||||
localStorage.setItem(
|
||||
"auth_expires_in",
|
||||
String(Date.now() + expires_in * 1000),
|
||||
)
|
||||
|
||||
return { access_token, refresh_token }
|
||||
}
|
||||
158
src/api/base.ts
Normal file
158
src/api/base.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
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 }
|
||||
Reference in New Issue
Block a user