From 4aaebc507e93d98850858021ce17f64c4cabc59c Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 22 Feb 2026 13:40:34 +0800 Subject: [PATCH] fix(web): skip GetCurrentUser on init when no token is stored MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When no token exists in sessionStorage, AuthContext.initialize() was still calling GetCurrentUser, triggering the auth interceptor to attempt RefreshToken and retry — producing a burst of 5+ auth API calls in under a second that reverse proxies with rate limiting (e.g. CrowdSec) flag as brute force. Add hasStoredToken() to auth-state and bail out of initialize() early when there is definitively no session to restore. The refresh flow for expired tokens is preserved since hasStoredToken() checks for presence regardless of expiry. Fixes #5647 --- web/src/auth-state.ts | 13 +++++++++++++ web/src/contexts/AuthContext.tsx | 17 ++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/web/src/auth-state.ts b/web/src/auth-state.ts index 058b96900..8729febab 100644 --- a/web/src/auth-state.ts +++ b/web/src/auth-state.ts @@ -62,6 +62,19 @@ export const isTokenExpired = (bufferMs: number = 30000): boolean => { return new Date() >= new Date(tokenExpiresAt.getTime() - bufferMs); }; +// Returns true if a token exists in sessionStorage, even if it is expired. +// Used to decide whether to attempt GetCurrentUser on app init — if no token +// was ever stored, the user is definitively not logged in and there is nothing +// to refresh, so we can skip the network round-trip entirely. +export const hasStoredToken = (): boolean => { + if (accessToken) return true; + try { + return !!sessionStorage.getItem(SESSION_TOKEN_KEY); + } catch { + return false; + } +}; + export const clearAccessToken = (): void => { accessToken = null; tokenExpiresAt = null; diff --git a/web/src/contexts/AuthContext.tsx b/web/src/contexts/AuthContext.tsx index 4615d2215..0ce0741dd 100644 --- a/web/src/contexts/AuthContext.tsx +++ b/web/src/contexts/AuthContext.tsx @@ -1,6 +1,6 @@ import { useQueryClient } from "@tanstack/react-query"; import { createContext, type ReactNode, useCallback, useContext, useMemo, useState } from "react"; -import { clearAccessToken } from "@/auth-state"; +import { clearAccessToken, hasStoredToken } from "@/auth-state"; import { authServiceClient, shortcutServiceClient, userServiceClient } from "@/connect"; import { userKeys } from "@/hooks/useUserQueries"; import type { Shortcut } from "@/types/proto/api/v1/shortcut_service_pb"; @@ -52,6 +52,21 @@ export function AuthProvider({ children }: { children: ReactNode }) { const initialize = useCallback(async () => { setState((prev) => ({ ...prev, isLoading: true })); + + // If there is no stored token at all, the user is not authenticated. + // Skip the network call — there is nothing to refresh and no session to restore. + if (!hasStoredToken()) { + setState({ + currentUser: undefined, + userGeneralSetting: undefined, + userWebhooksSetting: undefined, + shortcuts: [], + isInitialized: true, + isLoading: false, + }); + return; + } + try { const { user: currentUser } = await authServiceClient.getCurrentUser({});