Files
new-api/web/src/lib/http.ts
t0ng7u 347c31f93c 🔐 feat(auth): integrate backend session login + 2FA; add HTTP client and route guards
- 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.
2025-09-26 01:03:17 +08:00

122 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 统一 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' })
}