diff --git a/src/App.tsx b/src/App.tsx index 69d831af..d59d3b35 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,13 +24,13 @@ import { AppRoutes } from '@/base/AppRoute.constants.ts'; import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts'; import { MediaQuery } from '@/base/utils/MediaQuery.tsx'; import { BrowseTab } from '@/features/browse/Browse.types.ts'; -import { useSessionContext } from '@/features/authentication/SessionContext.tsx'; import { LoginPage } from '@/features/authentication/screens/LoginPage.tsx'; import { AuthGuard } from '@/features/authentication/components/AuthGuard.tsx'; import { SearchParam } from '@/base/Base.types.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { ReactRouter } from '@/lib/react-router/ReactRouter.ts'; import { GlobalDialog } from '@/base/global-dialog/GlobalDialog.tsx'; +import { AuthManager } from '@/features/authentication/AuthManager.ts'; const { Browse } = loadable(() => import('@/features/browse/screens/Browse.tsx'), lazyLoadFallback); const { DownloadQueue } = loadable(() => import('@/features/downloads/screens/DownloadQueue.tsx'), lazyLoadFallback); @@ -103,8 +103,8 @@ const ScrollToTop = () => { }; const InitialBackgroundRequests = () => { - const { isAuthRequired, accessToken } = useSessionContext(); - const skipConnection = isAuthRequired == null || (!!isAuthRequired && !accessToken); + const { isAuthRequired, accessToken } = AuthManager.useSession(); + const skipConnection = isAuthRequired == null || (!isAuthRequired && !accessToken); const [fetchExtensionList] = requestManager.useExtensionListFetch(); @@ -129,9 +129,9 @@ const InitialBackgroundRequests = () => { */ const BackgroundSubscriptions = () => { // Listen to session changes - const { isAuthRequired, accessToken } = useSessionContext(); + const { isAuthRequired, accessToken } = AuthManager.useSession(); - const skipConnection = isAuthRequired == null || (!!isAuthRequired && !accessToken); + const skipConnection = isAuthRequired == null || (!isAuthRequired && !accessToken); // Load the full download status once on startup to fill the cache requestManager.useGetDownloadStatus({ nextFetchPolicy: 'standby' }); @@ -153,7 +153,7 @@ const ReactRouterSetter = () => { }; const PrivateRoutes = () => { - const { isAuthRequired, accessToken, refreshToken } = useSessionContext(); + const { isAuthRequired, accessToken, refreshToken } = AuthManager.useSession(); const isAuthenticated = !isAuthRequired || (isAuthRequired && (accessToken || refreshToken)); if (!isAuthenticated) { diff --git a/src/base/contexts/AppContext.tsx b/src/base/contexts/AppContext.tsx index 0475f449..d0e13691 100644 --- a/src/base/contexts/AppContext.tsx +++ b/src/base/contexts/AppContext.tsx @@ -18,7 +18,6 @@ import { SnackbarWithDescription } from '@/base/components/feedback/SnackbarWith import { AppPageHistoryContextProvider } from '@/base/contexts/AppPageHistoryContext.tsx'; import { AppThemeContextProvider } from '@/features/theme/AppThemeContext.tsx'; import { NavBarContextProvider } from '@/features/navigation-bar/NavbarContext.tsx'; -import { SessionContextProvider } from '@/features/authentication/SessionContext.tsx'; import { SubpathUtil } from '@/lib/utils/SubpathUtil.ts'; interface Props { @@ -26,31 +25,29 @@ interface Props { } export const AppContext: React.FC = ({ children }) => ( - - - - - - - - - - {children} - - - - - - - - - + + + + + + + + + {children} + + + + + + + + ); diff --git a/src/features/authentication/AuthManager.ts b/src/features/authentication/AuthManager.ts index 2cd827b5..9348a30a 100644 --- a/src/features/authentication/AuthManager.ts +++ b/src/features/authentication/AuthManager.ts @@ -6,34 +6,71 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { useSyncExternalStore } from 'react'; import { AppStorage } from '@/lib/storage/AppStorage.ts'; -import { useSessionStorage } from '@/base/hooks/useStorage.tsx'; + +let notifierValue = 0; export class AuthManager { - static readonly AUTH_REQUIRED_KEY = 'auth-required'; - static readonly REFRESH_TOKEN_KEY = 'auth-refresh-token'; - static readonly REACT_SESSION_REFRESH_KEY = 'auth-react-session-refresh'; + private static subscribedCount: number = 0; + + private static subscribers = new Map void>(); private static accessToken: string | null = null; private static authInitialized: boolean = false; + private static authRequired: boolean | null = null; + private static refreshingToken: boolean = false; + private static subscribe(callback: () => void): () => void { + // eslint-disable-next-line no-plusplus + const key = AuthManager.subscribedCount++; + this.subscribers.set(key, callback); + + return () => this.unsubscribe(key); + } + + private static unsubscribe(key: number): void { + this.subscribers.delete(key); + } + + private static notify(): void { + notifierValue = (notifierValue + 1) % Number.MAX_SAFE_INTEGER; + this.subscribers.forEach((callback) => callback()); + } + + static useSession(): { + accessToken: typeof AuthManager.accessToken; + refreshToken: ReturnType; + isAuthRequired: typeof AuthManager.authRequired; + isInitialized: typeof AuthManager.authInitialized; + isRefreshingToken: typeof AuthManager.refreshingToken; + } { + useSyncExternalStore(AuthManager.subscribe.bind(AuthManager), () => notifierValue); + + return { + accessToken: AuthManager.accessToken, + refreshToken: AuthManager.getRefreshToken(), + isAuthRequired: AuthManager.authRequired, + isInitialized: AuthManager.authInitialized, + isRefreshingToken: AuthManager.refreshingToken, + }; + } + + static isAuthInitialized(): boolean { + return AuthManager.authInitialized; + } + static isAuthRequired(): boolean | null { - return AppStorage.session.getItemParsed(AuthManager.AUTH_REQUIRED_KEY, null); + return AuthManager.authRequired; } - static setAuthRequired(value: boolean | null): void { - AppStorage.session.setItem(AuthManager.AUTH_REQUIRED_KEY, value); - } - - static useIsAuthRequired(): boolean | null { - const [value] = useSessionStorage(AuthManager.AUTH_REQUIRED_KEY, null); - - return value; + static isRefreshingToken(): boolean { + return AuthManager.refreshingToken; } static getAccessToken(): string | null { @@ -51,14 +88,29 @@ export class AuthManager { }; } + static setAuthInitialized(value: boolean): void { + AuthManager.authInitialized = value; + AuthManager.notify(); + } + + static setAuthRequired(value: boolean | null): void { + AuthManager.authRequired = value; + AuthManager.notify(); + } + + static setIsRefreshingToken(value: boolean): void { + AuthManager.refreshingToken = value; + AuthManager.notify(); + } + static setAccessToken(token: string): void { AuthManager.accessToken = token; - AuthManager.refreshReactSessionContext(); + AuthManager.notify(); } static setRefreshToken(token: string): void { AppStorage.session.setItem(AuthManager.REFRESH_TOKEN_KEY, token); - AuthManager.refreshReactSessionContext(); + AuthManager.notify(); } static setTokens(accessToken: string, refreshToken: string): void { @@ -68,12 +120,12 @@ export class AuthManager { static removeAccessToken(): void { AuthManager.accessToken = null; - AuthManager.refreshReactSessionContext(); + AuthManager.notify(); } static removeRefreshToken(): void { AppStorage.session.setItem(AuthManager.REFRESH_TOKEN_KEY, undefined); - AuthManager.refreshReactSessionContext(); + AuthManager.notify(); } static removeTokens(): void { @@ -81,36 +133,6 @@ export class AuthManager { AuthManager.removeRefreshToken(); } - private static getNextReactSessionContextId(): number { - const id = AppStorage.session.getItemParsed(AuthManager.REACT_SESSION_REFRESH_KEY, 0); - - return (id + 1) % Number.MAX_SAFE_INTEGER; - } - - static refreshReactSessionContext(): void { - AppStorage.session.setItem(AuthManager.REACT_SESSION_REFRESH_KEY, AuthManager.getNextReactSessionContextId()); - } - - static useListenToReactSessionContextRefreshEvent(): void { - useSessionStorage(AuthManager.REACT_SESSION_REFRESH_KEY, 0); - } - - static isAuthInitialized(): boolean { - return AuthManager.authInitialized; - } - - static setAuthInitialized(value: boolean): void { - AuthManager.authInitialized = value; - } - - static isRefreshingToken(): boolean { - return AuthManager.refreshingToken; - } - - static setIsRefreshingToken(value: boolean): void { - AuthManager.refreshingToken = value; - } - static shouldQueueRequests(): boolean { const isLoginRequired = AuthManager.isAuthRequired() === true && AuthManager.getAccessToken() === null; diff --git a/src/features/authentication/SessionContext.tsx b/src/features/authentication/SessionContext.tsx deleted file mode 100644 index 2c0b2da0..00000000 --- a/src/features/authentication/SessionContext.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { createContext, ReactNode, useContext, useMemo } from 'react'; -import { AuthManager } from '@/features/authentication/AuthManager.ts'; - -interface TSessionContext { - isAuthRequired: boolean | null; - accessToken: string | null; - refreshToken: string | null; -} - -const SessionContext = createContext({ - isAuthRequired: null, - accessToken: null, - refreshToken: null, -}); - -export const SessionContextProvider = ({ children }: { children: ReactNode }) => { - AuthManager.useListenToReactSessionContextRefreshEvent(); - - const isAuthRequired = AuthManager.useIsAuthRequired(); - const { accessToken, refreshToken } = AuthManager.getTokens(); - - const value = useMemo( - () => ({ - isAuthRequired, - accessToken, - refreshToken, - }), - [isAuthRequired, accessToken, refreshToken], - ); - - return {children}; -}; - -export const useSessionContext = () => useContext(SessionContext); diff --git a/src/features/authentication/components/AuthGuard.tsx b/src/features/authentication/components/AuthGuard.tsx index 13f92c0a..34a7e144 100644 --- a/src/features/authentication/components/AuthGuard.tsx +++ b/src/features/authentication/components/AuthGuard.tsx @@ -7,15 +7,12 @@ */ import { ReactNode } from 'react'; -import { useSessionContext } from '@/features/authentication/SessionContext.tsx'; import { SplashScreen } from '@/features/authentication/components/SplashScreen.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { AuthManager } from '@/features/authentication/AuthManager.ts'; -AuthManager.setAuthRequired(null); - export const AuthGuard = ({ children }: { children: ReactNode }) => { - const { isAuthRequired } = useSessionContext(); + const { isAuthRequired } = AuthManager.useSession(); requestManager.useGetAbout({ skip: isAuthRequired !== null, diff --git a/src/features/authentication/screens/LoginPage.tsx b/src/features/authentication/screens/LoginPage.tsx index f9d72073..899b804d 100644 --- a/src/features/authentication/screens/LoginPage.tsx +++ b/src/features/authentication/screens/LoginPage.tsx @@ -19,7 +19,6 @@ import { requestManager } from '@/lib/requests/RequestManager.ts'; import { makeToast } from '@/base/utils/Toast.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { AuthManager } from '@/features/authentication/AuthManager.ts'; -import { useSessionContext } from '@/features/authentication/SessionContext.tsx'; import { AppRoutes } from '@/base/AppRoute.constants.ts'; import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx'; import { SearchParam } from '@/base/Base.types.ts'; @@ -31,7 +30,7 @@ export const LoginPage = () => { const { t } = useTranslation(); const { setOverride } = useNavBarContext(); const navigate = useNavigate(); - const { isAuthRequired, accessToken, refreshToken } = useSessionContext(); + const { isAuthRequired, accessToken, refreshToken } = AuthManager.useSession(); const [redirect] = useQueryParam(SearchParam.REDIRECT, StringParam); const [loginUser, { loading: isLoading }] = requestManager.useLoginUser(); @@ -39,7 +38,7 @@ export const LoginPage = () => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); - const isAuthenticated = !isAuthRequired || (!!isAuthRequired && !!accessToken && !!refreshToken); + const isAuthenticated = !isAuthRequired || (isAuthRequired && !!accessToken && !!refreshToken); const doLogin = async () => { try {