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:
@@ -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;
|
||||||
|
|
||||||
const interval = setInterval(async () => {
|
// Health check function
|
||||||
await useBackendState.getState().check()
|
const performHealthCheck = async () => {
|
||||||
}, healthCheckInterval * 1000)
|
await useBackendState.getState().check();
|
||||||
return () => clearInterval(interval)
|
};
|
||||||
}, [enableHealthCheck])
|
|
||||||
|
// Execute immediately
|
||||||
|
performHealthCheck();
|
||||||
|
|
||||||
|
// Set interval for periodic execution
|
||||||
|
const interval = setInterval(performHealthCheck, 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(
|
const handleTabChange = useCallback(
|
||||||
(tab: string) => useSettingsStore.getState().setCurrentTab(tab as any),
|
(tab: string) => useSettingsStore.getState().setCurrentTab(tab as any),
|
||||||
|
@@ -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>
|
||||||
)
|
)
|
||||||
|
@@ -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) {
|
||||||
@@ -38,6 +46,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
|
||||||
@@ -89,6 +108,11 @@ const LoginPage = () => {
|
|||||||
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
|
||||||
toast.info(response.message || t('login.authDisabled', 'Authentication is disabled. Using guest access.'))
|
toast.info(response.message || t('login.authDisabled', 'Authentication is disabled. Using guest access.'))
|
||||||
|
Reference in New Issue
Block a user