mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-30 19:01:46 +00:00
- add lib/http.ts: unified fetch wrapper with: - 30s timeout via AbortController - JSON/text parsing and normalized HttpError - Authorization header from cookie - New-Api-User header from persisted user id - get/post/put/patch/del helpers - add types/api.ts: ApiResponse, UserBasic, UserSelf, LoginResponse, Verify2FAResponse, SelfResponse - add lib/auth.ts: persist/restore user in localStorage (get/set/clear helpers) - refactor stores/auth-store.ts: - implement login (POST /api/user/login?turnstile=...), 2FA verify (POST /api/user/login/2fa) - implement fetchSelf (GET /api/user/self), logout (GET /api/user/logout) - persist user on successful login/verify/self - refactor auth/sign-in form: - switch field to "username" (supports username or email), loosen validation - call auth.login, handle 2FA branch → redirect to /otp - refactor OTP form: - call auth.verify2FA; on success, toast + redirect to home - add route guards: - /_authenticated: beforeLoad redirect to /sign-in if no user; silently refresh session via auth.fetchSelf - /(auth)/sign-in: beforeLoad redirect to / if already logged in - update sign-out dialog: use auth.logout and preserve redirect - cleanup: remove mock auth, no direct axios/fetch usage outside http.ts, no Semi UI imports in new code Notes - Backend expects username or email in "username" field. - Turnstile support: pass token via ?turnstile=... during login when enabled. Security - Ensure New-Api-User header matches logged-in user id per backend middleware. - Session-based auth with optional Bearer token via cookie. No breaking changes.
122 lines
2.9 KiB
TypeScript
122 lines
2.9 KiB
TypeScript
// 统一 http 封装:超时、JSON、错误语义、鉴权头
|
||
import { getStoredUserId } from '@/lib/auth'
|
||
import { getCookie } from '@/lib/cookies'
|
||
|
||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
||
|
||
type JsonBody = Record<string, unknown> | Array<unknown> | undefined
|
||
|
||
export interface HttpError {
|
||
status: number
|
||
message: string
|
||
details?: unknown
|
||
}
|
||
|
||
async function safeMessage(res: Response): Promise<string> {
|
||
try {
|
||
const data = await res.clone().json()
|
||
if (typeof data?.message === 'string') return data.message
|
||
return JSON.stringify(data)
|
||
} catch {
|
||
try {
|
||
return await res.clone().text()
|
||
} catch {
|
||
return res.statusText || 'Request failed'
|
||
}
|
||
}
|
||
}
|
||
|
||
const DEFAULT_TIMEOUT_MS = 30000
|
||
const ACCESS_TOKEN = 'thisisjustarandomstring'
|
||
|
||
export async function http<T>(
|
||
input: RequestInfo | URL,
|
||
init: RequestInit & { timeoutMs?: number; asText?: boolean } = {}
|
||
): Promise<T> {
|
||
const controller = new AbortController()
|
||
const timeout = setTimeout(
|
||
() => controller.abort(),
|
||
init.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
||
)
|
||
|
||
const headers: HeadersInit = {
|
||
'Content-Type': 'application/json',
|
||
...(init.headers || {}),
|
||
}
|
||
|
||
// 从 cookie 读取 token,并自动附带
|
||
const token = getCookie(ACCESS_TOKEN)
|
||
if (token) {
|
||
;(headers as Record<string, string>)['Authorization'] =
|
||
`Bearer ${JSON.parse(token)}`
|
||
}
|
||
|
||
// 携带 New-Api-User 以通过后端鉴权
|
||
const userId = getStoredUserId()
|
||
if (typeof userId === 'number' && userId > 0) {
|
||
;(headers as Record<string, string>)['New-Api-User'] = String(userId)
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(input, {
|
||
...init,
|
||
headers,
|
||
signal: controller.signal,
|
||
})
|
||
|
||
if (!response.ok) {
|
||
const message = await safeMessage(response)
|
||
const error: HttpError = { status: response.status, message }
|
||
throw error
|
||
}
|
||
|
||
if (init.asText) {
|
||
return (await response.text()) as unknown as T
|
||
}
|
||
// 缺省按 JSON 解析
|
||
return (await response.json()) as T
|
||
} finally {
|
||
clearTimeout(timeout)
|
||
}
|
||
}
|
||
|
||
export async function get<T>(url: string, init?: RequestInit) {
|
||
return http<T>(url, { ...init, method: 'GET' })
|
||
}
|
||
|
||
export async function post<T>(
|
||
url: string,
|
||
body?: JsonBody,
|
||
init?: RequestInit
|
||
) {
|
||
return http<T>(url, {
|
||
...init,
|
||
method: 'POST',
|
||
body: body ? JSON.stringify(body) : undefined,
|
||
})
|
||
}
|
||
|
||
export async function put<T>(url: string, body?: JsonBody, init?: RequestInit) {
|
||
return http<T>(url, {
|
||
...init,
|
||
method: 'PUT',
|
||
body: body ? JSON.stringify(body) : undefined,
|
||
})
|
||
}
|
||
|
||
export async function patch<T>(
|
||
url: string,
|
||
body?: JsonBody,
|
||
init?: RequestInit
|
||
) {
|
||
return http<T>(url, {
|
||
...init,
|
||
method: 'PATCH',
|
||
body: body ? JSON.stringify(body) : undefined,
|
||
})
|
||
}
|
||
|
||
export async function del<T>(url: string, init?: RequestInit) {
|
||
return http<T>(url, { ...init, method: 'DELETE' })
|
||
}
|