Files
36-character-flower/src/store/auth/auth-store.ts
JiaJun 5dd4e31db4 feat(auth): 集成认证授权功能并优化API客户端
- 实现了完整的登录注册认证流程,包括密码验证和用户资料获取
- 集成了JWT令牌管理和自动刷新机制,支持设备ID生成和管理
- 添加了WebSocket连接配置和API基础URL环境变量设置
- 实现了API客户端的请求拦截器,包括令牌验证和错误处理逻辑
- 集成了MD5加密和认证令牌缓存机制,提升安全性
- 添加了多语言国际化支持,包括英语、中文、马来语和印尼语
- 实现了认证状态管理和本地存储持久化功能
- 添加了表单验证schema和错误处理机制,增强用户体验
2026-05-16 09:03:55 +08:00

268 lines
6.9 KiB
TypeScript

import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware'
import { APP_PREFERENCES_STORAGE_KEY, AUTH_STORAGE_KEY } from '@/constants'
/**@description 未登录 | 已登录 | 正在从存储恢复数据 */
export type AuthStatus = 'anonymous' | 'authenticated' | 'restoring'
export interface AuthUser {
createTime?: number
channelId?: number
coin?: string
currentStreak?: number
email?: string
headImage?: string
id: string
isJackpot?: boolean
lastBetPeriodNo?: string
name?: string
oddsFactor?: number
phone?: string
registerInviteCode?: string
riskFlags?: number
roles?: string[]
streakLevel?: number
username?: string
uuid?: string
}
export interface AuthSessionInput {
accessToken: string
accessTokenExpiresAt?: number | null
currentUser?: AuthUser | null
refreshToken?: string | null
}
interface PersistedAuthState {
accessToken: string | null
/** @description 用户登录态 `user-token` 的绝对过期时间戳(毫秒)。 */
accessTokenExpiresAt: number | null
/** @description `/api/v1/authToken` 返回的服务端时间戳(秒),用于后续校时或服务端时间基准判断。 */
apiAuthServerTime: number | null
apiAuthToken: string | null
/** @description 接口鉴权 `auth-token` 的绝对过期时间戳(毫秒)。 */
apiAuthTokenExpiresAt: number | null
currentUser: AuthUser | null
refreshToken: string | null
}
interface PersistedAppPreferenceState {
appLanguage: string | null
deviceId: string | null
}
interface AuthState extends PersistedAuthState {
clearApiAuthToken: () => void
clearAccessToken: () => void
clearSession: () => void
finishHydration: () => void
isHydrated: boolean
lastUnauthorizedAt: string | null
markUnauthorized: () => void
setApiAuthToken: (token: {
expiresAt: number
serverTime: number
value: string
}) => void
setAccessToken: (token: string) => void
setCurrentUser: (user: AuthUser | null) => void
startSession: (session: AuthSessionInput) => void
status: AuthStatus
updateTokens: (tokens: {
accessToken: string
accessTokenExpiresAt?: number | null
refreshToken?: string | null
}) => void
}
function resolveAuthStatus(accessToken: string | null): AuthStatus {
return accessToken ? 'authenticated' : 'anonymous'
}
const initialPersistedState: PersistedAuthState = {
accessToken: null,
accessTokenExpiresAt: null,
apiAuthServerTime: null,
apiAuthToken: null,
apiAuthTokenExpiresAt: null,
refreshToken: null,
currentUser: null,
}
function generateDeviceId() {
if (
typeof crypto !== 'undefined' &&
typeof crypto.randomUUID === 'function'
) {
return `web_${crypto.randomUUID()}`
}
return `web_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
...initialPersistedState,
status: 'restoring',
isHydrated: false,
lastUnauthorizedAt: null,
setApiAuthToken: ({ expiresAt, serverTime, value }) => {
set({
apiAuthServerTime: serverTime,
apiAuthToken: value,
apiAuthTokenExpiresAt: expiresAt,
})
},
clearApiAuthToken: () => {
set({
apiAuthServerTime: null,
apiAuthToken: null,
apiAuthTokenExpiresAt: null,
})
},
setAccessToken: (token) => {
set({
accessToken: token,
accessTokenExpiresAt: null,
status: 'authenticated',
isHydrated: true,
})
},
setCurrentUser: (currentUser) => {
set((state) => ({
currentUser,
status: resolveAuthStatus(state.accessToken),
}))
},
startSession: ({
accessToken,
accessTokenExpiresAt = null,
currentUser = null,
refreshToken = null,
}) => {
set({
accessToken,
accessTokenExpiresAt,
currentUser,
refreshToken,
status: 'authenticated',
isHydrated: true,
})
},
updateTokens: ({ accessToken, accessTokenExpiresAt, refreshToken }) => {
set((state) => ({
accessToken,
accessTokenExpiresAt:
accessTokenExpiresAt ?? state.accessTokenExpiresAt,
refreshToken: refreshToken ?? state.refreshToken,
status: 'authenticated',
isHydrated: true,
}))
},
finishHydration: () => {
set((state) => ({
isHydrated: true,
status: resolveAuthStatus(state.accessToken),
}))
},
clearAccessToken: () => {
set({
accessToken: null,
accessTokenExpiresAt: null,
status: 'anonymous',
isHydrated: true,
})
},
clearSession: () => {
set({
...initialPersistedState,
status: 'anonymous',
isHydrated: true,
})
},
markUnauthorized: () => {
set({
...initialPersistedState,
status: 'anonymous',
isHydrated: true,
lastUnauthorizedAt: new Date().toISOString(),
})
},
}),
{
name: AUTH_STORAGE_KEY,
storage: createJSONStorage(() => sessionStorage),
partialize: (state) => ({
accessToken: state.accessToken,
accessTokenExpiresAt: state.accessTokenExpiresAt,
apiAuthServerTime: state.apiAuthServerTime,
apiAuthToken: state.apiAuthToken,
apiAuthTokenExpiresAt: state.apiAuthTokenExpiresAt,
currentUser: state.currentUser,
refreshToken: state.refreshToken,
}),
onRehydrateStorage: () => (state, error) => {
if (error) {
state?.clearSession()
return
}
state?.finishHydration()
},
},
),
)
interface AppPreferenceStoreState extends PersistedAppPreferenceState {
getOrCreateDeviceId: () => string
setAppLanguage: (language: string) => void
}
export const useAppPreferenceStore = create<AppPreferenceStoreState>()(
persist(
(set, get) => ({
appLanguage: null,
deviceId: null,
getOrCreateDeviceId: () => {
const deviceId = get().deviceId
if (deviceId) {
return deviceId
}
const nextDeviceId = generateDeviceId()
set({ deviceId: nextDeviceId })
return nextDeviceId
},
setAppLanguage: (language) => {
set({ appLanguage: language })
},
}),
{
name: APP_PREFERENCES_STORAGE_KEY,
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
appLanguage: state.appLanguage,
deviceId: state.deviceId,
}),
},
),
)
export function getAuthDeviceId() {
return useAppPreferenceStore.getState().getOrCreateDeviceId()
}
export function getStoredAppLanguage() {
return useAppPreferenceStore.getState().appLanguage
}
export function setStoredAppLanguage(language: string) {
useAppPreferenceStore.getState().setAppLanguage(language)
}