- 创建API相关常量文件,包括响应码、HTTP状态码、请求头等 - 将认证相关常量从auth模块提取到独立的常量文件 - 在API客户端中使用新定义的常量替换硬编码值 - 更新认证API和服务中对常量的引用 - 在国际化配置中创建统一的文案常量以减少重复 - 将认证表单验证规则改为使用常量配置
222 lines
5.3 KiB
TypeScript
222 lines
5.3 KiB
TypeScript
import { LOGIN_PROMPT_DEDUP_MS } from '@/constants'
|
|
import i18n from '@/i18n'
|
|
import { notify } from '@/lib/notify'
|
|
import { queryClient } from '@/lib/query/query-client'
|
|
import type { AuthSessionInput, AuthUser } from '@/store/auth'
|
|
import { useAuthStore } from '@/store/auth'
|
|
import { useModalStore } from '@/store/modal'
|
|
|
|
export type CurrentUserInitializer = () => Promise<AuthUser | null>
|
|
export type RefreshSessionHandler = (
|
|
refreshToken: string,
|
|
) => Promise<AuthSessionInput | null>
|
|
|
|
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
|
|
|
|
interface ClearAuthenticatedSessionOptions {
|
|
clearBrowserStorage?: boolean
|
|
clearQueryCache?: boolean
|
|
}
|
|
|
|
interface UnauthorizedSessionOptions extends ClearAuthenticatedSessionOptions {
|
|
openLoginModal?: boolean
|
|
showLoginRequiredToast?: boolean
|
|
}
|
|
|
|
function clearBrowserStorageData() {
|
|
if (typeof localStorage !== 'undefined') {
|
|
localStorage.clear()
|
|
}
|
|
|
|
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
|
|
}
|