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 } if (API_BASE_URL.startsWith('/')) { const basePath = API_BASE_URL.startsWith('/') ? API_BASE_URL : `/${API_BASE_URL}` if (normalizedPath === basePath || normalizedPath.startsWith(`${basePath}/`)) { return normalizedPath } return `${basePath}${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( url: string, body: Record, extraHeaders?: Record, ): Promise { 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 { const candidate = payload.token ?? payload.accessToken ?? payload.access_token ?? (payload.data as Record | undefined)?.token ?? (payload.data as Record | undefined)?.accessToken ?? (payload.data as Record | undefined)?.access_token if (typeof candidate !== 'string' || candidate.length === 0) { throw new Error('登录成功,但后端未返回 token 字段') } return candidate } function extractTokenType(payload: Record): string | undefined { const candidate = payload.token_type ?? payload.tokenType ?? (payload.data as Record | undefined)?.token_type ?? (payload.data as Record | undefined)?.tokenType return typeof candidate === 'string' && candidate.length > 0 ? candidate : undefined } function extractRefreshToken(payload: Record): string | undefined { const candidate = payload.refresh_token ?? payload.refreshToken ?? (payload.data as Record | undefined)?.refresh_token ?? (payload.data as Record | undefined)?.refreshToken return typeof candidate === 'string' && candidate.length > 0 ? candidate : undefined } function extractExpiresIn(payload: Record): number | undefined { const candidate = payload.expires_in ?? payload.expiresIn ?? (payload.data as Record | undefined)?.expires_in ?? (payload.data as Record | undefined)?.expiresIn return typeof candidate === 'number' && Number.isFinite(candidate) ? candidate : undefined } function extractUser(payload: Record): AuthUser | undefined { const user = payload.user ?? (payload.data as Record | undefined)?.user return typeof user === 'object' && user !== null ? (user as AuthUser) : undefined } function parseAuthResult(payload: Record): 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 { await request>(buildUrl(REGISTER_PATH), input) } export async function login(input: { loginId: string; password: string }): Promise { const payload = await request>( 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 { if (!input.refreshToken) { throw new Error('缺少 refresh_token,无法刷新登录状态') } const refreshBody = { refreshToken: input.refreshToken } // 兼容不同后端实现: // 1) 有的要求 Authorization + refresh token // 2) 有的只接受 refresh token,不接受 Authorization let payload: Record try { payload = await request>( buildUrl(REFRESH_PATH), refreshBody, { Authorization: createAuthHeader(input.token, input.tokenType), }, ) } catch { payload = await request>( buildUrl(REFRESH_PATH), refreshBody, ) } return parseAuthResult(payload) }