From 258d1a496af217ab6c8bce9bfcbfa99ce89e7d08 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:13:50 +0200 Subject: [PATCH] Remove "auth required flag" from session storage With 12ef012e4b92eae9e041cfe75663d0748a67a050 this has no benefit anymore, because the app now always checks if authentication is enabled by the server first before any other request can be sent. --- src/App.tsx | 12 +- src/base/contexts/AppContext.tsx | 53 ++++---- src/features/authentication/AuthManager.ts | 116 +++++++++++------- .../authentication/SessionContext.tsx | 42 ------- .../authentication/components/AuthGuard.tsx | 5 +- .../authentication/screens/LoginPage.tsx | 5 +- 6 files changed, 103 insertions(+), 130 deletions(-) delete mode 100644 src/features/authentication/SessionContext.tsx 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 {