Files
36-character-flower/src/lib/auth/auth-session.ts
JiaJun a6b34660ad refactor(game): 优化游戏组件并实现国际化支持
- 在AppBootResourceGate组件中集成react-i18next实现资源加载文本的国际化
- 修改DesktopAnimal组件中的loading dots key以提高渲染性能
- 在DesktopControl组件中添加useRef和useEffect钩子管理定时器清理逻辑
- 将DesktopTitle组件重构为MessageBroadcast组件并增强其响应式设计
- 更新DesktopSupportModal组件中的客户服务文本为国际化格式
- 在AuthSession模块中实现本地存储数据清理时保留关键偏好设置
- 调整多个游戏组件的样式类以改进移动端适配效果
- 移除未使用的桌面提取功能相关代码文件
- 更新GitNexus索引统计数据反映最新的代码变更
2026-06-04 18:01:40 +08:00

233 lines
5.4 KiB
TypeScript

import {
APP_PREFERENCES_STORAGE_KEY,
AUDIO_PREFERENCES_STORAGE_KEY,
LOGIN_PROMPT_DEDUP_MS,
} from '@/constants'
import i18n from '@/i18n'
import { notify } from '@/lib/notify'
import { queryClient } from '@/lib/query/query-client'
import { useAuthStore } from '@/store/auth'
import { useModalStore } from '@/store/modal'
import type {
AuthSessionInput,
AuthUser,
ClearAuthenticatedSessionOptions,
CurrentUserInitializer,
RefreshSessionHandler,
UnauthorizedSessionOptions,
} from '@/type'
let currentUserInitializer: CurrentUserInitializer | null = null
let refreshSessionHandler: RefreshSessionHandler | null = null
let authInitializationPromise: Promise<void> | null = null
let refreshSessionPromise: Promise<boolean> | null = null
let lastLoginPromptAt = 0
const PRESERVED_LOCAL_STORAGE_KEYS = [
APP_PREFERENCES_STORAGE_KEY,
AUDIO_PREFERENCES_STORAGE_KEY,
]
function clearBrowserStorageData() {
if (typeof localStorage !== 'undefined') {
const preserved = PRESERVED_LOCAL_STORAGE_KEYS.map(
(key) => [key, localStorage.getItem(key)] as const,
)
localStorage.clear()
for (const [key, value] of preserved) {
if (value !== null) {
localStorage.setItem(key, value)
}
}
}
if (typeof sessionStorage !== 'undefined') {
sessionStorage.clear()
}
}
function hasClearableSessionState() {
const snapshot = useAuthStore.getState()
return Boolean(
snapshot.status !== 'anonymous' ||
snapshot.accessToken ||
snapshot.refreshToken ||
snapshot.currentUser ||
snapshot.apiAuthToken ||
snapshot.apiAuthTokenExpiresAt ||
snapshot.apiAuthServerTime,
)
}
function hasRecordedUnauthorizedSession() {
const snapshot = useAuthStore.getState()
return snapshot.status === 'anonymous' && Boolean(snapshot.lastUnauthorizedAt)
}
export function registerCurrentUserInitializer(
initializer: CurrentUserInitializer | null,
) {
currentUserInitializer = initializer
}
export function registerRefreshSessionHandler(
handler: RefreshSessionHandler | null,
) {
refreshSessionHandler = handler
}
export function isAuthenticated() {
const snapshot = useAuthStore.getState()
return snapshot.status === 'authenticated' && Boolean(snapshot.accessToken)
}
export function clearAuthenticatedSession({
clearBrowserStorage = true,
clearQueryCache = true,
}: ClearAuthenticatedSessionOptions = {}) {
const alreadyUnauthorized = hasRecordedUnauthorizedSession()
if (!alreadyUnauthorized) {
useAuthStore.getState().markUnauthorized()
}
if (clearQueryCache && !alreadyUnauthorized) {
queryClient.clear()
}
if (clearBrowserStorage && !alreadyUnauthorized) {
clearBrowserStorageData()
}
}
export function handleUnauthorizedSession({
clearBrowserStorage = false,
openLoginModal = false,
showLoginRequiredToast = false,
}: UnauthorizedSessionOptions = {}) {
clearAuthenticatedSession({
clearBrowserStorage,
clearQueryCache: hasClearableSessionState(),
})
if (!openLoginModal && !showLoginRequiredToast) {
return
}
const modalStore = openLoginModal ? useModalStore.getState() : null
if (modalStore?.modals.desktopLogin) {
return
}
const now = Date.now()
const shouldPrompt = now - lastLoginPromptAt > LOGIN_PROMPT_DEDUP_MS
if (!shouldPrompt) {
return
}
lastLoginPromptAt = now
if (showLoginRequiredToast) {
notify.warning(i18n.t('commonUi.toast.loginRequired'))
}
if (openLoginModal) {
modalStore?.openExclusiveModal('desktopLogin')
}
}
export function handleInvalidTokenSession() {
handleUnauthorizedSession({
clearBrowserStorage: true,
openLoginModal: true,
showLoginRequiredToast: true,
})
}
export async function initializeAuthSession() {
if (authInitializationPromise) {
return authInitializationPromise
}
authInitializationPromise = (async () => {
await useAuthStore.persist.rehydrate()
const snapshot = useAuthStore.getState()
if (
!snapshot.accessToken ||
snapshot.currentUser ||
!currentUserInitializer
) {
return
}
const currentUser = await currentUserInitializer()
useAuthStore.getState().setCurrentUser(currentUser)
})().finally(() => {
authInitializationPromise = null
})
return authInitializationPromise
}
export async function hydrateCurrentUser(initializer: CurrentUserInitializer) {
const currentUser = await initializer()
useAuthStore.getState().setCurrentUser(currentUser)
return currentUser
}
export async function tryRefreshAuthSession() {
if (refreshSessionPromise) {
return refreshSessionPromise
}
const snapshot = useAuthStore.getState()
if (!snapshot.refreshToken || !refreshSessionHandler) {
return false
}
const refreshToken = snapshot.refreshToken
refreshSessionPromise = (async () => {
try {
const nextSession = await refreshSessionHandler(refreshToken)
if (!nextSession?.accessToken) {
handleUnauthorizedSession()
return false
}
useAuthStore.getState().startSession({
accessToken: nextSession.accessToken,
accessTokenExpiresAt:
nextSession.accessTokenExpiresAt ?? snapshot.accessTokenExpiresAt,
currentUser: nextSession.currentUser ?? snapshot.currentUser,
refreshToken: nextSession.refreshToken ?? snapshot.refreshToken,
})
return true
} catch {
handleUnauthorizedSession()
return false
} finally {
refreshSessionPromise = null
}
})()
return refreshSessionPromise
}