From ea51ff05c1bfdbdee2091fe31c82f6172539b33f Mon Sep 17 00:00:00 2001 From: yangdx Date: Sun, 23 Mar 2025 00:05:04 +0800 Subject: [PATCH] Refactor auth and version checks for improved reliability. - Prevent duplicate version checks in Vite dev mode - Simplify protected route handling - Add session flags for version tracking --- lightrag_webui/src/App.tsx | 68 ++++++++++-- lightrag_webui/src/AppRouter.tsx | 127 +++------------------- lightrag_webui/src/features/LoginPage.tsx | 34 +++++- 3 files changed, 101 insertions(+), 128 deletions(-) diff --git a/lightrag_webui/src/App.tsx b/lightrag_webui/src/App.tsx index 4596f684..b3e1acca 100644 --- a/lightrag_webui/src/App.tsx +++ b/lightrag_webui/src/App.tsx @@ -1,13 +1,13 @@ -import { useState, useCallback } from 'react' +import { useState, useCallback, useEffect, useRef } from 'react' import ThemeProvider from '@/components/ThemeProvider' import TabVisibilityProvider from '@/contexts/TabVisibilityProvider' import MessageAlert from '@/components/MessageAlert' import ApiKeyAlert from '@/components/ApiKeyAlert' import StatusIndicator from '@/components/graph/StatusIndicator' import { healthCheckInterval } from '@/lib/constants' -import { useBackendState } from '@/stores/state' +import { useBackendState, useAuthStore } from '@/stores/state' import { useSettingsStore } from '@/stores/settings' -import { useEffect } from 'react' +import { getAuthStatus } from '@/api/lightrag' import SiteHeader from '@/features/SiteHeader' import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag' @@ -23,17 +23,63 @@ function App() { const enableHealthCheck = useSettingsStore.use.enableHealthCheck() const currentTab = useSettingsStore.use.currentTab() const [apiKeyInvalid, setApiKeyInvalid] = useState(false) + const versionCheckRef = useRef(false); // Prevent duplicate calls in Vite dev mode - // Health check + // Health check - can be disabled useEffect(() => { - // Check immediately - useBackendState.getState().check() + // Only execute if health check is enabled + if (!enableHealthCheck) return; + + // Health check function + const performHealthCheck = async () => { + await useBackendState.getState().check(); + }; + + // Execute immediately + performHealthCheck(); + + // Set interval for periodic execution + const interval = setInterval(performHealthCheck, healthCheckInterval * 1000); + return () => clearInterval(interval); + }, [enableHealthCheck]); - const interval = setInterval(async () => { - await useBackendState.getState().check() - }, healthCheckInterval * 1000) - return () => clearInterval(interval) - }, [enableHealthCheck]) + // Version check - independent and executed only once + useEffect(() => { + const checkVersion = async () => { + // Prevent duplicate calls in Vite dev mode + if (versionCheckRef.current) return; + versionCheckRef.current = true; + + // Check if version info was already obtained in login page + const versionCheckedFromLogin = sessionStorage.getItem('VERSION_CHECKED_FROM_LOGIN') === 'true'; + if (versionCheckedFromLogin) return; + + // Get version info + const token = localStorage.getItem('LIGHTRAG-API-TOKEN'); + if (!token) return; + + try { + const status = await getAuthStatus(); + if (status.core_version || status.api_version) { + // Update version info while maintaining login state + useAuthStore.getState().login( + token, + useAuthStore.getState().isGuestMode, + status.core_version, + status.api_version + ); + + // Set flag to indicate version info has been checked + sessionStorage.setItem('VERSION_CHECKED_FROM_LOGIN', 'true'); + } + } catch (error) { + console.error('Failed to get version info:', error); + } + }; + + // Execute version check + checkVersion(); + }, []); // Empty dependency array ensures it only runs once on mount const handleTabChange = useCallback( (tab: string) => useSettingsStore.getState().setCurrentTab(tab as any), diff --git a/lightrag_webui/src/AppRouter.tsx b/lightrag_webui/src/AppRouter.tsx index f6d1d08b..1d957174 100644 --- a/lightrag_webui/src/AppRouter.tsx +++ b/lightrag_webui/src/AppRouter.tsx @@ -2,98 +2,11 @@ import { HashRouter as Router, Routes, Route, useNavigate } from 'react-router-d import { useEffect, useState } from 'react' import { useAuthStore } from '@/stores/state' import { navigationService } from '@/services/navigation' -import { getAuthStatus } from '@/api/lightrag' -import { toast } from 'sonner' import { Toaster } from 'sonner' import App from './App' import LoginPage from '@/features/LoginPage' import ThemeProvider from '@/components/ThemeProvider' -interface ProtectedRouteProps { - children: React.ReactNode -} - -const ProtectedRoute = ({ children }: ProtectedRouteProps) => { - const { isAuthenticated } = useAuthStore() - const [isChecking, setIsChecking] = useState(true) - const navigate = useNavigate() - - // Set navigate function for navigation service - useEffect(() => { - navigationService.setNavigate(navigate) - }, [navigate]) - - useEffect(() => { - let isMounted = true; // Flag to prevent state updates after unmount - - // This effect will run when the component mounts - // and will check if authentication is required - const checkAuthStatus = async () => { - try { - // Skip check if already authenticated - if (isAuthenticated) { - if (isMounted) setIsChecking(false); - return; - } - - const status = await getAuthStatus() - - // Only proceed if component is still mounted - if (!isMounted) return; - - if (!status.auth_configured && status.access_token) { - // If auth is not configured, use the guest token - useAuthStore.getState().login(status.access_token, true, status.core_version, status.api_version) - if (status.message) { - toast.info(status.message) - } - } - } catch (error) { - console.error('Failed to check auth status:', error) - } finally { - // Only update state if component is still mounted - if (isMounted) { - setIsChecking(false) - } - } - } - - // Execute immediately - checkAuthStatus() - - // Cleanup function to prevent state updates after unmount - return () => { - isMounted = false; - } - }, [isAuthenticated]) - - // Handle navigation when authentication status changes - useEffect(() => { - if (!isChecking && !isAuthenticated) { - const currentPath = window.location.hash.slice(1); // Remove the '#' from hash - const isLoginPage = currentPath === '/login'; - - if (!isLoginPage) { - // Use navigation service for redirection - console.log('Not authenticated, redirecting to login'); - navigationService.navigateToLogin(); - } - } - }, [isChecking, isAuthenticated]); - - // Show nothing while checking auth status or when not authenticated on login page - if (isChecking || (!isAuthenticated && window.location.hash.slice(1) === '/login')) { - return null; - } - - // Show children only when authenticated - if (!isAuthenticated) { - return null; - } - - return <>{children}; -} - const AppContent = () => { const [initializing, setInitializing] = useState(true) const { isAuthenticated } = useAuthStore() @@ -104,34 +17,20 @@ const AppContent = () => { navigationService.setNavigate(navigate) }, [navigate]) - // Check token validity and auth configuration on app initialization + // Token validity check useEffect(() => { - let isMounted = true; // Flag to prevent state updates after unmount + let isMounted = true; const checkAuth = async () => { try { const token = localStorage.getItem('LIGHTRAG-API-TOKEN') - // If we have a token, we're already authenticated if (token && isAuthenticated) { if (isMounted) setInitializing(false); return; } - // If no token or not authenticated, check if auth is configured - const status = await getAuthStatus() - - // Only proceed if component is still mounted - if (!isMounted) return; - - if (!status.auth_configured && status.access_token) { - // If auth is not configured, use the guest token - useAuthStore.getState().login(status.access_token, true, status.core_version, status.api_version) - if (status.message) { - toast.info(status.message) - } - } else if (!token) { - // Only logout if we don't have a token + if (!token) { useAuthStore.getState().logout() } } catch (error) { @@ -140,22 +39,30 @@ const AppContent = () => { useAuthStore.getState().logout() } } finally { - // Only update state if component is still mounted if (isMounted) { setInitializing(false) } } } - // Execute immediately checkAuth() - // Cleanup function to prevent state updates after unmount return () => { isMounted = false; } }, [isAuthenticated]) + // Redirect effect for protected routes + useEffect(() => { + if (!initializing && !isAuthenticated) { + const currentPath = window.location.hash.slice(1); + if (currentPath !== '/login') { + console.log('Not authenticated, redirecting to login'); + navigate('/login'); + } + } + }, [initializing, isAuthenticated, navigate]); + // Show nothing while initializing if (initializing) { return null @@ -166,11 +73,7 @@ const AppContent = () => { } /> - - - } + element={isAuthenticated ? : null} /> ) diff --git a/lightrag_webui/src/features/LoginPage.tsx b/lightrag_webui/src/features/LoginPage.tsx index f8ca35f1..5b06c428 100644 --- a/lightrag_webui/src/features/LoginPage.tsx +++ b/lightrag_webui/src/features/LoginPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { useAuthStore } from '@/stores/state' import { loginToServer, getAuthStatus } from '@/api/lightrag' @@ -18,6 +18,7 @@ const LoginPage = () => { const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [checkingAuth, setCheckingAuth] = useState(true) + const authCheckRef = useRef(false); // Prevent duplicate calls in Vite dev mode useEffect(() => { console.log('LoginPage mounted') @@ -28,6 +29,13 @@ const LoginPage = () => { let isMounted = true; // Flag to prevent state updates after unmount const checkAuthConfig = async () => { + // Prevent duplicate calls in Vite dev mode + if (authCheckRef.current) { + if (isMounted) setCheckingAuth(false); + return; + } + authCheckRef.current = true; + try { // If already authenticated, redirect to home if (isAuthenticated) { @@ -37,6 +45,17 @@ const LoginPage = () => { // Check auth status const status = await getAuthStatus() + + // Set checkingAuth to false immediately after getAuthStatus + // This allows the login page to render while other processing continues + if (isMounted) { + setCheckingAuth(false); + } + + // Set session flag for version check to avoid duplicate checks in App component + if (isMounted && (status.core_version || status.api_version)) { + sessionStorage.setItem('VERSION_CHECKED_FROM_LOGIN', 'true'); + } // Only proceed if component is still mounted if (!isMounted) return; @@ -48,16 +67,16 @@ const LoginPage = () => { toast.info(status.message) } navigate('/') - return // Exit early, no need to set checkingAuth to false + return } } catch (error) { console.error('Failed to check auth configuration:', error) - } finally { - // Only update state if component is still mounted + // Also set checkingAuth to false in case of error if (isMounted) { - setCheckingAuth(false) + setCheckingAuth(false); } } + // Removed finally block as we're setting checkingAuth earlier } // Execute immediately @@ -88,6 +107,11 @@ const LoginPage = () => { // Check authentication mode const isGuestMode = response.auth_mode === 'disabled' login(response.access_token, isGuestMode, response.core_version, response.api_version) + + // Set session flag for version check + if (response.core_version || response.api_version) { + sessionStorage.setItem('VERSION_CHECKED_FROM_LOGIN', 'true'); + } if (isGuestMode) { // Show authentication disabled notification