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
This commit is contained in:
yangdx
2025-03-23 00:05:04 +08:00
parent 21ec8166f4
commit ea51ff05c1
3 changed files with 101 additions and 128 deletions

View File

@@ -1,13 +1,13 @@
import { useState, useCallback } from 'react' import { useState, useCallback, useEffect, useRef } from 'react'
import ThemeProvider from '@/components/ThemeProvider' import ThemeProvider from '@/components/ThemeProvider'
import TabVisibilityProvider from '@/contexts/TabVisibilityProvider' import TabVisibilityProvider from '@/contexts/TabVisibilityProvider'
import MessageAlert from '@/components/MessageAlert' import MessageAlert from '@/components/MessageAlert'
import ApiKeyAlert from '@/components/ApiKeyAlert' import ApiKeyAlert from '@/components/ApiKeyAlert'
import StatusIndicator from '@/components/graph/StatusIndicator' import StatusIndicator from '@/components/graph/StatusIndicator'
import { healthCheckInterval } from '@/lib/constants' import { healthCheckInterval } from '@/lib/constants'
import { useBackendState } from '@/stores/state' import { useBackendState, useAuthStore } from '@/stores/state'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { useEffect } from 'react' import { getAuthStatus } from '@/api/lightrag'
import SiteHeader from '@/features/SiteHeader' import SiteHeader from '@/features/SiteHeader'
import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag' import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
@@ -23,17 +23,63 @@ function App() {
const enableHealthCheck = useSettingsStore.use.enableHealthCheck() const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
const currentTab = useSettingsStore.use.currentTab() const currentTab = useSettingsStore.use.currentTab()
const [apiKeyInvalid, setApiKeyInvalid] = useState(false) const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
const versionCheckRef = useRef(false); // Prevent duplicate calls in Vite dev mode
// Health check // Health check - can be disabled
useEffect(() => { useEffect(() => {
// Check immediately // Only execute if health check is enabled
useBackendState.getState().check() 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 () => { // Version check - independent and executed only once
await useBackendState.getState().check() useEffect(() => {
}, healthCheckInterval * 1000) const checkVersion = async () => {
return () => clearInterval(interval) // Prevent duplicate calls in Vite dev mode
}, [enableHealthCheck]) 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( const handleTabChange = useCallback(
(tab: string) => useSettingsStore.getState().setCurrentTab(tab as any), (tab: string) => useSettingsStore.getState().setCurrentTab(tab as any),

View File

@@ -2,98 +2,11 @@ import { HashRouter as Router, Routes, Route, useNavigate } from 'react-router-d
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useAuthStore } from '@/stores/state' import { useAuthStore } from '@/stores/state'
import { navigationService } from '@/services/navigation' import { navigationService } from '@/services/navigation'
import { getAuthStatus } from '@/api/lightrag'
import { toast } from 'sonner'
import { Toaster } from 'sonner' import { Toaster } from 'sonner'
import App from './App' import App from './App'
import LoginPage from '@/features/LoginPage' import LoginPage from '@/features/LoginPage'
import ThemeProvider from '@/components/ThemeProvider' 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 AppContent = () => {
const [initializing, setInitializing] = useState(true) const [initializing, setInitializing] = useState(true)
const { isAuthenticated } = useAuthStore() const { isAuthenticated } = useAuthStore()
@@ -104,34 +17,20 @@ const AppContent = () => {
navigationService.setNavigate(navigate) navigationService.setNavigate(navigate)
}, [navigate]) }, [navigate])
// Check token validity and auth configuration on app initialization // Token validity check
useEffect(() => { useEffect(() => {
let isMounted = true; // Flag to prevent state updates after unmount let isMounted = true;
const checkAuth = async () => { const checkAuth = async () => {
try { try {
const token = localStorage.getItem('LIGHTRAG-API-TOKEN') const token = localStorage.getItem('LIGHTRAG-API-TOKEN')
// If we have a token, we're already authenticated
if (token && isAuthenticated) { if (token && isAuthenticated) {
if (isMounted) setInitializing(false); if (isMounted) setInitializing(false);
return; return;
} }
// If no token or not authenticated, check if auth is configured if (!token) {
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
useAuthStore.getState().logout() useAuthStore.getState().logout()
} }
} catch (error) { } catch (error) {
@@ -140,22 +39,30 @@ const AppContent = () => {
useAuthStore.getState().logout() useAuthStore.getState().logout()
} }
} finally { } finally {
// Only update state if component is still mounted
if (isMounted) { if (isMounted) {
setInitializing(false) setInitializing(false)
} }
} }
} }
// Execute immediately
checkAuth() checkAuth()
// Cleanup function to prevent state updates after unmount
return () => { return () => {
isMounted = false; isMounted = false;
} }
}, [isAuthenticated]) }, [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 // Show nothing while initializing
if (initializing) { if (initializing) {
return null return null
@@ -166,11 +73,7 @@ const AppContent = () => {
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route <Route
path="/*" path="/*"
element={ element={isAuthenticated ? <App /> : null}
<ProtectedRoute>
<App />
</ProtectedRoute>
}
/> />
</Routes> </Routes>
) )

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useRef } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useAuthStore } from '@/stores/state' import { useAuthStore } from '@/stores/state'
import { loginToServer, getAuthStatus } from '@/api/lightrag' import { loginToServer, getAuthStatus } from '@/api/lightrag'
@@ -18,6 +18,7 @@ const LoginPage = () => {
const [username, setUsername] = useState('') const [username, setUsername] = useState('')
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [checkingAuth, setCheckingAuth] = useState(true) const [checkingAuth, setCheckingAuth] = useState(true)
const authCheckRef = useRef(false); // Prevent duplicate calls in Vite dev mode
useEffect(() => { useEffect(() => {
console.log('LoginPage mounted') console.log('LoginPage mounted')
@@ -28,6 +29,13 @@ const LoginPage = () => {
let isMounted = true; // Flag to prevent state updates after unmount let isMounted = true; // Flag to prevent state updates after unmount
const checkAuthConfig = async () => { const checkAuthConfig = async () => {
// Prevent duplicate calls in Vite dev mode
if (authCheckRef.current) {
if (isMounted) setCheckingAuth(false);
return;
}
authCheckRef.current = true;
try { try {
// If already authenticated, redirect to home // If already authenticated, redirect to home
if (isAuthenticated) { if (isAuthenticated) {
@@ -37,6 +45,17 @@ const LoginPage = () => {
// Check auth status // Check auth status
const status = await getAuthStatus() 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 // Only proceed if component is still mounted
if (!isMounted) return; if (!isMounted) return;
@@ -48,16 +67,16 @@ const LoginPage = () => {
toast.info(status.message) toast.info(status.message)
} }
navigate('/') navigate('/')
return // Exit early, no need to set checkingAuth to false return
} }
} catch (error) { } catch (error) {
console.error('Failed to check auth configuration:', error) console.error('Failed to check auth configuration:', error)
} finally { // Also set checkingAuth to false in case of error
// Only update state if component is still mounted
if (isMounted) { if (isMounted) {
setCheckingAuth(false) setCheckingAuth(false);
} }
} }
// Removed finally block as we're setting checkingAuth earlier
} }
// Execute immediately // Execute immediately
@@ -88,6 +107,11 @@ const LoginPage = () => {
// Check authentication mode // Check authentication mode
const isGuestMode = response.auth_mode === 'disabled' const isGuestMode = response.auth_mode === 'disabled'
login(response.access_token, isGuestMode, response.core_version, response.api_version) 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) { if (isGuestMode) {
// Show authentication disabled notification // Show authentication disabled notification