Files
mahjong-web/src/api/auth.ts
wsy182 0f1684b8d7 feat(game): 更新游戏页面功能和认证刷新机制
- 将开发环境代理目标从 192.168.1.5 改为 127.0.0.1
- 重构 auth.ts 文件中的代码缩进格式
- 实现自动令牌刷新机制,支持 JWT 过期时间检测
- 添加 WebSocket 连接的令牌强制刷新逻辑
- 新增 WindSquare 组件显示方位风向图标
- 实现动态座位风向计算和显示功能
- 优化 WebSocket URL 构建方式,移除查询参数中的令牌传递
- 添加登录失效时自动跳转到登录页面的功能
- 限制玩家名称显示长度为4个字符
- 改进 WebSocket 错误处理和重连机制
2026-03-26 13:28:35 +08:00

208 lines
6.5 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.
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)
}