first commit

This commit is contained in:
2026-02-18 14:30:42 +08:00
commit f79920ad6a
212 changed files with 3850 additions and 0 deletions

185
src/api/auth.ts Normal file
View File

@@ -0,0 +1,185 @@
export interface AuthUser {
id?: string | number
username?: string
nickname?: string
}
export interface AuthSessionInput {
token: string
tokenType?: string
refreshToken?: string
}
export interface AuthResult {
token: string
tokenType?: string
refreshToken?: string
expiresIn?: number
user?: AuthUser
}
interface ApiErrorPayload {
message?: string
error?: string
}
const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? '').trim().replace(/\/$/, '')
const LOGIN_PATH = import.meta.env.VITE_LOGIN_PATH ?? '/api/v1/auth/login'
const REGISTER_PATH = import.meta.env.VITE_REGISTER_PATH ?? '/api/v1/auth/register'
const REFRESH_PATH = import.meta.env.VITE_REFRESH_PATH ?? '/api/v1/auth/refresh'
const LOGIN_BEARER_TOKEN = (import.meta.env.VITE_LOGIN_BEARER_TOKEN ?? '').trim()
function buildUrl(path: string): string {
if (/^https?:\/\//.test(path)) {
return path
}
const normalizedPath = path.startsWith('/') ? path : `/${path}`
if (!API_BASE_URL) {
return normalizedPath
}
// Avoid duplicated API prefix, e.g. base: /api/v1 + path: /api/v1/auth/login
try {
const baseUrl = new URL(API_BASE_URL)
const basePath = baseUrl.pathname.replace(/\/$/, '')
if (basePath && normalizedPath.startsWith(`${basePath}/`)) {
return `${API_BASE_URL}${normalizedPath.slice(basePath.length)}`
}
} catch {
// API_BASE_URL may be a relative path; fallback to direct join.
}
return `${API_BASE_URL}${normalizedPath}`
}
async function request<T>(
url: string,
body: Record<string, unknown>,
extraHeaders?: Record<string, string>,
): Promise<T> {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...extraHeaders,
},
body: JSON.stringify(body),
})
const payload = (await response.json().catch(() => ({}))) as T & ApiErrorPayload
if (!response.ok) {
throw new Error(payload.message ?? payload.error ?? '请求失败,请稍后再试')
}
return payload
}
function createAuthHeader(token: string, tokenType = 'Bearer'): string {
const normalizedToken = token.trim()
if (/^\S+\s+\S+/.test(normalizedToken)) {
return normalizedToken
}
return `${tokenType || 'Bearer'} ${normalizedToken}`
}
function extractToken(payload: Record<string, unknown>): string {
const candidate =
payload.token ??
payload.accessToken ??
payload.access_token ??
(payload.data as Record<string, unknown> | undefined)?.token ??
(payload.data as Record<string, unknown> | undefined)?.accessToken ??
(payload.data as Record<string, unknown> | undefined)?.access_token
if (typeof candidate !== 'string' || candidate.length === 0) {
throw new Error('登录成功,但后端未返回 token 字段')
}
return candidate
}
function extractTokenType(payload: Record<string, unknown>): string | undefined {
const candidate =
payload.token_type ??
payload.tokenType ??
(payload.data as Record<string, unknown> | undefined)?.token_type ??
(payload.data as Record<string, unknown> | undefined)?.tokenType
return typeof candidate === 'string' && candidate.length > 0 ? candidate : undefined
}
function extractRefreshToken(payload: Record<string, unknown>): string | undefined {
const candidate =
payload.refresh_token ??
payload.refreshToken ??
(payload.data as Record<string, unknown> | undefined)?.refresh_token ??
(payload.data as Record<string, unknown> | undefined)?.refreshToken
return typeof candidate === 'string' && candidate.length > 0 ? candidate : undefined
}
function extractExpiresIn(payload: Record<string, unknown>): number | undefined {
const candidate =
payload.expires_in ??
payload.expiresIn ??
(payload.data as Record<string, unknown> | undefined)?.expires_in ??
(payload.data as Record<string, unknown> | undefined)?.expiresIn
return typeof candidate === 'number' && Number.isFinite(candidate) ? candidate : undefined
}
function extractUser(payload: Record<string, unknown>): AuthUser | undefined {
const user = payload.user ?? (payload.data as Record<string, unknown> | undefined)?.user
return typeof user === 'object' && user !== null ? (user as AuthUser) : undefined
}
function parseAuthResult(payload: Record<string, unknown>): AuthResult {
return {
token: extractToken(payload),
tokenType: extractTokenType(payload),
refreshToken: extractRefreshToken(payload),
expiresIn: extractExpiresIn(payload),
user: extractUser(payload),
}
}
export async function register(input: {
username: string
phone: string
email: string
password: string
}): Promise<void> {
await request<Record<string, unknown>>(buildUrl(REGISTER_PATH), input)
}
export async function login(input: { loginId: string; password: string }): Promise<AuthResult> {
const payload = await request<Record<string, unknown>>(
buildUrl(LOGIN_PATH),
{
login_id: input.loginId,
password: input.password,
},
LOGIN_BEARER_TOKEN ? { Authorization: `Bearer ${LOGIN_BEARER_TOKEN}` } : undefined,
)
return parseAuthResult(payload)
}
export async function refreshAccessToken(input: AuthSessionInput): Promise<AuthResult> {
if (!input.refreshToken) {
throw new Error('缺少 refresh_token无法刷新登录状态')
}
const payload = await request<Record<string, unknown>>(
buildUrl(REFRESH_PATH),
{
refreshToken: input.refreshToken,
},
{
Authorization: createAuthHeader(input.token, input.tokenType),
},
)
return parseAuthResult(payload)
}