feat(auth): 集成认证授权功能并优化API客户端
- 实现了完整的登录注册认证流程,包括密码验证和用户资料获取 - 集成了JWT令牌管理和自动刷新机制,支持设备ID生成和管理 - 添加了WebSocket连接配置和API基础URL环境变量设置 - 实现了API客户端的请求拦截器,包括令牌验证和错误处理逻辑 - 集成了MD5加密和认证令牌缓存机制,提升安全性 - 添加了多语言国际化支持,包括英语、中文、马来语和印尼语 - 实现了认证状态管理和本地存储持久化功能 - 添加了表单验证schema和错误处理机制,增强用户体验
This commit is contained in:
@@ -1,42 +1,77 @@
|
||||
import { create } from 'zustand'
|
||||
import { createJSONStorage, persist } from 'zustand/middleware'
|
||||
|
||||
import { AUTH_STORAGE_KEY } from '@/constants'
|
||||
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
|
||||
}
|
||||
@@ -47,10 +82,25 @@ function resolveAuthStatus(accessToken: string | null): AuthStatus {
|
||||
|
||||
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) => ({
|
||||
@@ -58,9 +108,24 @@ export const useAuthStore = create<AuthState>()(
|
||||
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,
|
||||
})
|
||||
@@ -73,20 +138,24 @@ export const useAuthStore = create<AuthState>()(
|
||||
},
|
||||
startSession: ({
|
||||
accessToken,
|
||||
accessTokenExpiresAt = null,
|
||||
currentUser = null,
|
||||
refreshToken = null,
|
||||
}) => {
|
||||
set({
|
||||
accessToken,
|
||||
accessTokenExpiresAt,
|
||||
currentUser,
|
||||
refreshToken,
|
||||
status: 'authenticated',
|
||||
isHydrated: true,
|
||||
})
|
||||
},
|
||||
updateTokens: ({ accessToken, refreshToken }) => {
|
||||
updateTokens: ({ accessToken, accessTokenExpiresAt, refreshToken }) => {
|
||||
set((state) => ({
|
||||
accessToken,
|
||||
accessTokenExpiresAt:
|
||||
accessTokenExpiresAt ?? state.accessTokenExpiresAt,
|
||||
refreshToken: refreshToken ?? state.refreshToken,
|
||||
status: 'authenticated',
|
||||
isHydrated: true,
|
||||
@@ -101,6 +170,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
clearAccessToken: () => {
|
||||
set({
|
||||
accessToken: null,
|
||||
accessTokenExpiresAt: null,
|
||||
status: 'anonymous',
|
||||
isHydrated: true,
|
||||
})
|
||||
@@ -126,6 +196,10 @@ export const useAuthStore = create<AuthState>()(
|
||||
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,
|
||||
}),
|
||||
@@ -141,3 +215,53 @@ export const useAuthStore = create<AuthState>()(
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user