admin-projects/src/services/http-client.ts
WangDL 4dad572731 feat: add admin layout, auth, user management, and routing
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 17:19:58 +08:00

114 lines
2.7 KiB
TypeScript

import {
getAccessToken,
getRefreshToken,
setTokens,
clearTokens,
} from './token-store'
interface ApiResponse<T = unknown> {
success: boolean
data: T
message?: string
statusCode?: number
}
export class ApiError extends Error {
httpStatus: number
code: number
constructor(message: string, httpStatus: number, code?: number) {
super(message)
this.name = 'ApiError'
this.httpStatus = httpStatus
this.code = code ?? httpStatus
}
}
let refreshPromise: Promise<boolean> | null = null
async function tryRefresh(): Promise<boolean> {
const token = getRefreshToken()
if (!token) return false
if (!refreshPromise) {
refreshPromise = (async () => {
try {
const res = await fetch('/admin-api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: token }),
})
if (!res.ok) return false
const json: ApiResponse<{ accessToken: string; refreshToken: string }> = await res.json()
if (json.success) {
setTokens(json.data.accessToken, json.data.refreshToken)
return true
}
return false
} catch {
return false
} finally {
refreshPromise = null
}
})()
}
return refreshPromise
}
async function request<T>(
path: string,
options: RequestInit = {},
retried = false,
): Promise<T> {
const token = getAccessToken()
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(options.headers as Record<string, string>),
}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
const res = await fetch(path, { ...options, headers })
if (res.status === 401 && !retried) {
const refreshed = await tryRefresh()
if (refreshed) {
return request<T>(path, options, true)
}
clearTokens()
window.location.href = '/login'
throw new ApiError('登录已过期', 401)
}
const json: ApiResponse<T> = await res.json()
if (!json.success) {
throw new ApiError(json.message || '请求失败', res.status, json.statusCode)
}
return json.data
}
export const api = {
get<T>(path: string) {
return request<T>(path)
},
post<T>(path: string, body?: unknown) {
return request<T>(path, {
method: 'POST',
body: body != null ? JSON.stringify(body) : undefined,
})
},
put<T>(path: string, body?: unknown) {
return request<T>(path, {
method: 'PUT',
body: body != null ? JSON.stringify(body) : undefined,
})
},
delete<T>(path: string, body?: unknown) {
return request<T>(path, {
method: 'DELETE',
body: body != null ? JSON.stringify(body) : undefined,
})
},
}