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 | null = null let refreshSessionPromise: Promise | 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 }