Remove "auth required flag" from session storage

With 12ef012e4b 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.
This commit is contained in:
schroda
2025-10-16 20:13:50 +02:00
parent f63c461fb3
commit 258d1a496a
6 changed files with 103 additions and 130 deletions

View File

@@ -24,13 +24,13 @@ import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts'; import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
import { MediaQuery } from '@/base/utils/MediaQuery.tsx'; import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
import { BrowseTab } from '@/features/browse/Browse.types.ts'; import { BrowseTab } from '@/features/browse/Browse.types.ts';
import { useSessionContext } from '@/features/authentication/SessionContext.tsx';
import { LoginPage } from '@/features/authentication/screens/LoginPage.tsx'; import { LoginPage } from '@/features/authentication/screens/LoginPage.tsx';
import { AuthGuard } from '@/features/authentication/components/AuthGuard.tsx'; import { AuthGuard } from '@/features/authentication/components/AuthGuard.tsx';
import { SearchParam } from '@/base/Base.types.ts'; import { SearchParam } from '@/base/Base.types.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { ReactRouter } from '@/lib/react-router/ReactRouter.ts'; import { ReactRouter } from '@/lib/react-router/ReactRouter.ts';
import { GlobalDialog } from '@/base/global-dialog/GlobalDialog.tsx'; 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 { Browse } = loadable(() => import('@/features/browse/screens/Browse.tsx'), lazyLoadFallback);
const { DownloadQueue } = loadable(() => import('@/features/downloads/screens/DownloadQueue.tsx'), lazyLoadFallback); const { DownloadQueue } = loadable(() => import('@/features/downloads/screens/DownloadQueue.tsx'), lazyLoadFallback);
@@ -103,8 +103,8 @@ const ScrollToTop = () => {
}; };
const InitialBackgroundRequests = () => { const InitialBackgroundRequests = () => {
const { isAuthRequired, accessToken } = useSessionContext(); const { isAuthRequired, accessToken } = AuthManager.useSession();
const skipConnection = isAuthRequired == null || (!!isAuthRequired && !accessToken); const skipConnection = isAuthRequired == null || (!isAuthRequired && !accessToken);
const [fetchExtensionList] = requestManager.useExtensionListFetch(); const [fetchExtensionList] = requestManager.useExtensionListFetch();
@@ -129,9 +129,9 @@ const InitialBackgroundRequests = () => {
*/ */
const BackgroundSubscriptions = () => { const BackgroundSubscriptions = () => {
// Listen to session changes // 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 // Load the full download status once on startup to fill the cache
requestManager.useGetDownloadStatus({ nextFetchPolicy: 'standby' }); requestManager.useGetDownloadStatus({ nextFetchPolicy: 'standby' });
@@ -153,7 +153,7 @@ const ReactRouterSetter = () => {
}; };
const PrivateRoutes = () => { const PrivateRoutes = () => {
const { isAuthRequired, accessToken, refreshToken } = useSessionContext(); const { isAuthRequired, accessToken, refreshToken } = AuthManager.useSession();
const isAuthenticated = !isAuthRequired || (isAuthRequired && (accessToken || refreshToken)); const isAuthenticated = !isAuthRequired || (isAuthRequired && (accessToken || refreshToken));
if (!isAuthenticated) { if (!isAuthenticated) {

View File

@@ -18,7 +18,6 @@ import { SnackbarWithDescription } from '@/base/components/feedback/SnackbarWith
import { AppPageHistoryContextProvider } from '@/base/contexts/AppPageHistoryContext.tsx'; import { AppPageHistoryContextProvider } from '@/base/contexts/AppPageHistoryContext.tsx';
import { AppThemeContextProvider } from '@/features/theme/AppThemeContext.tsx'; import { AppThemeContextProvider } from '@/features/theme/AppThemeContext.tsx';
import { NavBarContextProvider } from '@/features/navigation-bar/NavbarContext.tsx'; import { NavBarContextProvider } from '@/features/navigation-bar/NavbarContext.tsx';
import { SessionContextProvider } from '@/features/authentication/SessionContext.tsx';
import { SubpathUtil } from '@/lib/utils/SubpathUtil.ts'; import { SubpathUtil } from '@/lib/utils/SubpathUtil.ts';
interface Props { interface Props {
@@ -26,31 +25,29 @@ interface Props {
} }
export const AppContext: React.FC<Props> = ({ children }) => ( export const AppContext: React.FC<Props> = ({ children }) => (
<SessionContextProvider> <Router basename={SubpathUtil.getRouterBasename()}>
<Router basename={SubpathUtil.getRouterBasename()}> <StyledEngineProvider injectFirst>
<StyledEngineProvider injectFirst> <AppThemeContextProvider>
<AppThemeContextProvider> <QueryParamProvider adapter={ReactRouter6Adapter}>
<QueryParamProvider adapter={ReactRouter6Adapter}> <NavBarContextProvider>
<NavBarContextProvider> <AppPageHistoryContextProvider>
<AppPageHistoryContextProvider> <ActiveDeviceContextProvider>
<ActiveDeviceContextProvider> <SnackbarProvider
<SnackbarProvider Components={{
Components={{ default: SnackbarWithDescription,
default: SnackbarWithDescription, info: SnackbarWithDescription,
info: SnackbarWithDescription, success: SnackbarWithDescription,
success: SnackbarWithDescription, warning: SnackbarWithDescription,
warning: SnackbarWithDescription, error: SnackbarWithDescription,
error: SnackbarWithDescription, }}
}} >
> <AppHotkeysProvider>{children}</AppHotkeysProvider>
<AppHotkeysProvider>{children}</AppHotkeysProvider> </SnackbarProvider>
</SnackbarProvider> </ActiveDeviceContextProvider>
</ActiveDeviceContextProvider> </AppPageHistoryContextProvider>
</AppPageHistoryContextProvider> </NavBarContextProvider>
</NavBarContextProvider> </QueryParamProvider>
</QueryParamProvider> </AppThemeContextProvider>
</AppThemeContextProvider> </StyledEngineProvider>
</StyledEngineProvider> </Router>
</Router>
</SessionContextProvider>
); );

View File

@@ -6,34 +6,71 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { useSyncExternalStore } from 'react';
import { AppStorage } from '@/lib/storage/AppStorage.ts'; import { AppStorage } from '@/lib/storage/AppStorage.ts';
import { useSessionStorage } from '@/base/hooks/useStorage.tsx';
let notifierValue = 0;
export class AuthManager { export class AuthManager {
static readonly AUTH_REQUIRED_KEY = 'auth-required';
static readonly REFRESH_TOKEN_KEY = 'auth-refresh-token'; 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<number, () => void>();
private static accessToken: string | null = null; private static accessToken: string | null = null;
private static authInitialized: boolean = false; private static authInitialized: boolean = false;
private static authRequired: boolean | null = null;
private static refreshingToken: boolean = false; 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<typeof AuthManager.getRefreshToken>;
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 { static isAuthRequired(): boolean | null {
return AppStorage.session.getItemParsed(AuthManager.AUTH_REQUIRED_KEY, null); return AuthManager.authRequired;
} }
static setAuthRequired(value: boolean | null): void { static isRefreshingToken(): boolean {
AppStorage.session.setItem(AuthManager.AUTH_REQUIRED_KEY, value); return AuthManager.refreshingToken;
}
static useIsAuthRequired(): boolean | null {
const [value] = useSessionStorage(AuthManager.AUTH_REQUIRED_KEY, null);
return value;
} }
static getAccessToken(): string | null { 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 { static setAccessToken(token: string): void {
AuthManager.accessToken = token; AuthManager.accessToken = token;
AuthManager.refreshReactSessionContext(); AuthManager.notify();
} }
static setRefreshToken(token: string): void { static setRefreshToken(token: string): void {
AppStorage.session.setItem(AuthManager.REFRESH_TOKEN_KEY, token); AppStorage.session.setItem(AuthManager.REFRESH_TOKEN_KEY, token);
AuthManager.refreshReactSessionContext(); AuthManager.notify();
} }
static setTokens(accessToken: string, refreshToken: string): void { static setTokens(accessToken: string, refreshToken: string): void {
@@ -68,12 +120,12 @@ export class AuthManager {
static removeAccessToken(): void { static removeAccessToken(): void {
AuthManager.accessToken = null; AuthManager.accessToken = null;
AuthManager.refreshReactSessionContext(); AuthManager.notify();
} }
static removeRefreshToken(): void { static removeRefreshToken(): void {
AppStorage.session.setItem(AuthManager.REFRESH_TOKEN_KEY, undefined); AppStorage.session.setItem(AuthManager.REFRESH_TOKEN_KEY, undefined);
AuthManager.refreshReactSessionContext(); AuthManager.notify();
} }
static removeTokens(): void { static removeTokens(): void {
@@ -81,36 +133,6 @@ export class AuthManager {
AuthManager.removeRefreshToken(); 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 { static shouldQueueRequests(): boolean {
const isLoginRequired = AuthManager.isAuthRequired() === true && AuthManager.getAccessToken() === null; const isLoginRequired = AuthManager.isAuthRequired() === true && AuthManager.getAccessToken() === null;

View File

@@ -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<TSessionContext>({
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 <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;
};
export const useSessionContext = () => useContext(SessionContext);

View File

@@ -7,15 +7,12 @@
*/ */
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { useSessionContext } from '@/features/authentication/SessionContext.tsx';
import { SplashScreen } from '@/features/authentication/components/SplashScreen.tsx'; import { SplashScreen } from '@/features/authentication/components/SplashScreen.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts';
import { AuthManager } from '@/features/authentication/AuthManager.ts'; import { AuthManager } from '@/features/authentication/AuthManager.ts';
AuthManager.setAuthRequired(null);
export const AuthGuard = ({ children }: { children: ReactNode }) => { export const AuthGuard = ({ children }: { children: ReactNode }) => {
const { isAuthRequired } = useSessionContext(); const { isAuthRequired } = AuthManager.useSession();
requestManager.useGetAbout({ requestManager.useGetAbout({
skip: isAuthRequired !== null, skip: isAuthRequired !== null,

View File

@@ -19,7 +19,6 @@ import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/base/utils/Toast.ts'; import { makeToast } from '@/base/utils/Toast.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { AuthManager } from '@/features/authentication/AuthManager.ts'; import { AuthManager } from '@/features/authentication/AuthManager.ts';
import { useSessionContext } from '@/features/authentication/SessionContext.tsx';
import { AppRoutes } from '@/base/AppRoute.constants.ts'; import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx'; import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx';
import { SearchParam } from '@/base/Base.types.ts'; import { SearchParam } from '@/base/Base.types.ts';
@@ -31,7 +30,7 @@ export const LoginPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { setOverride } = useNavBarContext(); const { setOverride } = useNavBarContext();
const navigate = useNavigate(); const navigate = useNavigate();
const { isAuthRequired, accessToken, refreshToken } = useSessionContext(); const { isAuthRequired, accessToken, refreshToken } = AuthManager.useSession();
const [redirect] = useQueryParam(SearchParam.REDIRECT, StringParam); const [redirect] = useQueryParam(SearchParam.REDIRECT, StringParam);
const [loginUser, { loading: isLoading }] = requestManager.useLoginUser(); const [loginUser, { loading: isLoading }] = requestManager.useLoginUser();
@@ -39,7 +38,7 @@ export const LoginPage = () => {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const isAuthenticated = !isAuthRequired || (!!isAuthRequired && !!accessToken && !!refreshToken); const isAuthenticated = !isAuthRequired || (isAuthRequired && !!accessToken && !!refreshToken);
const doLogin = async () => { const doLogin = async () => {
try { try {