- 实现了完整的登录注册认证流程,包括密码验证和用户资料获取 - 集成了JWT令牌管理和自动刷新机制,支持设备ID生成和管理 - 添加了WebSocket连接配置和API基础URL环境变量设置 - 实现了API客户端的请求拦截器,包括令牌验证和错误处理逻辑 - 集成了MD5加密和认证令牌缓存机制,提升安全性 - 添加了多语言国际化支持,包括英语、中文、马来语和印尼语 - 实现了认证状态管理和本地存储持久化功能 - 添加了表单验证schema和错误处理机制,增强用户体验
268 lines
6.9 KiB
TypeScript
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)
|
|
}
|