- 将开发环境代理目标从 192.168.1.5 改为 127.0.0.1 - 重构 auth.ts 文件中的代码缩进格式 - 实现自动令牌刷新机制,支持 JWT 过期时间检测 - 添加 WebSocket 连接的令牌强制刷新逻辑 - 新增 WindSquare 组件显示方位风向图标 - 实现动态座位风向计算和显示功能 - 优化 WebSocket URL 构建方式,移除查询参数中的令牌传递 - 添加登录失效时自动跳转到登录页面的功能 - 限制玩家名称显示长度为4个字符 - 改进 WebSocket 错误处理和重连机制
208 lines
6.5 KiB
TypeScript
208 lines
6.5 KiB
TypeScript
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<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 refreshBody = {
|
||
refreshToken: input.refreshToken
|
||
}
|
||
|
||
// 兼容不同后端实现:
|
||
// 1) 有的要求 Authorization + refresh token
|
||
// 2) 有的只接受 refresh token,不接受 Authorization
|
||
let payload: Record<string, unknown>
|
||
try {
|
||
payload = await request<Record<string, unknown>>(
|
||
buildUrl(REFRESH_PATH),
|
||
refreshBody,
|
||
{
|
||
Authorization: createAuthHeader(input.token, input.tokenType),
|
||
},
|
||
)
|
||
} catch {
|
||
payload = await request<Record<string, unknown>>(
|
||
buildUrl(REFRESH_PATH),
|
||
refreshBody,
|
||
)
|
||
}
|
||
|
||
return parseAuthResult(payload)
|
||
}
|