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 { 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) {

View File

@@ -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<Props> = ({ children }) => (
<SessionContextProvider>
<Router basename={SubpathUtil.getRouterBasename()}>
<StyledEngineProvider injectFirst>
<AppThemeContextProvider>
<QueryParamProvider adapter={ReactRouter6Adapter}>
<NavBarContextProvider>
<AppPageHistoryContextProvider>
<ActiveDeviceContextProvider>
<SnackbarProvider
Components={{
default: SnackbarWithDescription,
info: SnackbarWithDescription,
success: SnackbarWithDescription,
warning: SnackbarWithDescription,
error: SnackbarWithDescription,
}}
>
<AppHotkeysProvider>{children}</AppHotkeysProvider>
</SnackbarProvider>
</ActiveDeviceContextProvider>
</AppPageHistoryContextProvider>
</NavBarContextProvider>
</QueryParamProvider>
</AppThemeContextProvider>
</StyledEngineProvider>
</Router>
</SessionContextProvider>
<Router basename={SubpathUtil.getRouterBasename()}>
<StyledEngineProvider injectFirst>
<AppThemeContextProvider>
<QueryParamProvider adapter={ReactRouter6Adapter}>
<NavBarContextProvider>
<AppPageHistoryContextProvider>
<ActiveDeviceContextProvider>
<SnackbarProvider
Components={{
default: SnackbarWithDescription,
info: SnackbarWithDescription,
success: SnackbarWithDescription,
warning: SnackbarWithDescription,
error: SnackbarWithDescription,
}}
>
<AppHotkeysProvider>{children}</AppHotkeysProvider>
</SnackbarProvider>
</ActiveDeviceContextProvider>
</AppPageHistoryContextProvider>
</NavBarContextProvider>
</QueryParamProvider>
</AppThemeContextProvider>
</StyledEngineProvider>
</Router>
);

View File

@@ -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<number, () => 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<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 {
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;

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 { 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,

View File

@@ -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 {