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()( 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()( 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) }