Merge pull request #1162 from danielaskdd/improve-version-check

Refactor auth and version checks for improved reliability
This commit is contained in:
Daniel.y
2025-03-23 03:21:28 +08:00
committed by GitHub
12 changed files with 301 additions and 313 deletions

View File

@@ -1 +1 @@
__api_version__ = "1.2.1" __api_version__ = "1.2.2"

View File

@@ -417,6 +417,13 @@ def create_app(args):
# Get update flags status for all namespaces # Get update flags status for all namespaces
update_status = await get_all_update_flags_status() update_status = await get_all_update_flags_status()
username = os.getenv("AUTH_USERNAME")
password = os.getenv("AUTH_PASSWORD")
if not (username and password):
auth_mode = "disabled"
else:
auth_mode = "enabled"
return { return {
"status": "healthy", "status": "healthy",
"working_directory": str(args.working_dir), "working_directory": str(args.working_dir),
@@ -438,6 +445,9 @@ def create_app(args):
"enable_llm_cache_for_extract": args.enable_llm_cache_for_extract, "enable_llm_cache_for_extract": args.enable_llm_cache_for_extract,
}, },
"update_status": update_status, "update_status": update_status,
"core_version": core_version,
"api_version": __api_version__,
"auth_mode": auth_mode,
} }
# Custom StaticFiles class to prevent caching of HTML files # Custom StaticFiles class to prevent caching of HTML files

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,8 +8,8 @@
<link rel="icon" type="image/svg+xml" href="logo.png" /> <link rel="icon" type="image/svg+xml" href="logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag</title> <title>Lightrag</title>
<script type="module" crossorigin src="/webui/assets/index-BRzImUsU.js"></script> <script type="module" crossorigin src="/webui/assets/index-DlScqWrq.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-BcBS1RaQ.css"> <link rel="stylesheet" crossorigin href="/webui/assets/index-Cq65VeVX.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

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,64 @@ 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) {
const isGuestMode = status.auth_mode === 'disabled' || useAuthStore.getState().isGuestMode;
// Update version info while maintaining login state
useAuthStore.getState().login(
token,
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,58 +17,48 @@ 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
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); 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) {
console.error('Auth initialization error:', error) console.error('Auth initialization error:', error)
if (isMounted && !isAuthenticated) { if (!isAuthenticated) {
useAuthStore.getState().logout() useAuthStore.getState().logout()
} }
} finally { } finally {
// Only update state if component is still mounted setInitializing(false)
if (isMounted) {
setInitializing(false)
}
} }
} }
// Execute immediately
checkAuth() checkAuth()
// Cleanup function to prevent state updates after unmount
return () => { return () => {
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 +69,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

@@ -41,6 +41,10 @@ export type LightragStatus = {
graph_storage: string graph_storage: string
vector_storage: string vector_storage: string
} }
update_status?: Record<string, any>
core_version?: string
api_version?: string
auth_mode?: 'enabled' | 'disabled'
} }
export type LightragDocumentsScanProgress = { export type LightragDocumentsScanProgress = {
@@ -183,8 +187,9 @@ axiosInstance.interceptors.response.use(
} }
// For other APIs, navigate to login page // For other APIs, navigate to login page
navigationService.navigateToLogin(); navigationService.navigateToLogin();
// Return a never-resolving promise to prevent further execution
return new Promise(() => {}); // return a reject Promise
return Promise.reject(new Error('Authentication required'));
} }
throw new Error( throw new Error(
`${error.response.status} ${error.response.statusText}\n${JSON.stringify( `${error.response.status} ${error.response.statusText}\n${JSON.stringify(

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')
@@ -25,9 +26,14 @@ const LoginPage = () => {
// Check if authentication is configured, skip login if not // Check if authentication is configured, skip login if not
useEffect(() => { useEffect(() => {
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) {
return;
}
authCheckRef.current = true;
try { try {
// If already authenticated, redirect to home // If already authenticated, redirect to home
if (isAuthenticated) { if (isAuthenticated) {
@@ -38,8 +44,10 @@ const LoginPage = () => {
// Check auth status // Check auth status
const status = await getAuthStatus() const status = await getAuthStatus()
// Only proceed if component is still mounted // Set session flag for version check to avoid duplicate checks in App component
if (!isMounted) return; if (status.core_version || status.api_version) {
sessionStorage.setItem('VERSION_CHECKED_FROM_LOGIN', 'true');
}
if (!status.auth_configured && status.access_token) { if (!status.auth_configured && status.access_token) {
// If auth is not configured, use the guest token and redirect // If auth is not configured, use the guest token and redirect
@@ -48,16 +56,18 @@ const LoginPage = () => {
toast.info(status.message) toast.info(status.message)
} }
navigate('/') navigate('/')
return // Exit early, no need to set checkingAuth to false return
} }
// Only set checkingAuth to false if we need to show the login page
setCheckingAuth(false);
} 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 setCheckingAuth(false);
if (isMounted) {
setCheckingAuth(false)
}
} }
// Removed finally block as we're setting checkingAuth earlier
} }
// Execute immediately // Execute immediately
@@ -65,7 +75,6 @@ const LoginPage = () => {
// Cleanup function to prevent state updates after unmount // Cleanup function to prevent state updates after unmount
return () => { return () => {
isMounted = false;
} }
}, [isAuthenticated, login, navigate]) }, [isAuthenticated, login, navigate])
@@ -89,6 +98,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.'))

View File

@@ -67,14 +67,10 @@ class NavigationService {
return; return;
} }
// First navigate to login page this.resetAllApplicationState();
this.navigate('/login'); useAuthStore.getState().logout();
// Then reset state after navigation this.navigate('/login');
setTimeout(() => {
this.resetAllApplicationState();
useAuthStore.getState().logout();
}, 0);
} }
navigateToHome() { navigateToHome() {

View File

@@ -23,6 +23,7 @@ interface AuthState {
apiVersion: string | null; apiVersion: string | null;
login: (token: string, isGuest?: boolean, coreVersion?: string | null, apiVersion?: string | null) => void; login: (token: string, isGuest?: boolean, coreVersion?: string | null, apiVersion?: string | null) => void;
logout: () => void; logout: () => void;
setVersion: (coreVersion: string | null, apiVersion: string | null) => void;
} }
const useBackendStateStoreBase = create<BackendState>()((set) => ({ const useBackendStateStoreBase = create<BackendState>()((set) => ({
@@ -35,6 +36,14 @@ const useBackendStateStoreBase = create<BackendState>()((set) => ({
check: async () => { check: async () => {
const health = await checkHealth() const health = await checkHealth()
if (health.status === 'healthy') { if (health.status === 'healthy') {
// Update version information if health check returns it
if (health.core_version || health.api_version) {
useAuthStore.getState().setVersion(
health.core_version || null,
health.api_version || null
);
}
set({ set({
health: true, health: true,
message: null, message: null,
@@ -148,6 +157,22 @@ export const useAuthStore = create<AuthState>(set => {
coreVersion: coreVersion, coreVersion: coreVersion,
apiVersion: apiVersion apiVersion: apiVersion
}); });
},
setVersion: (coreVersion, apiVersion) => {
// Update localStorage
if (coreVersion) {
localStorage.setItem('LIGHTRAG-CORE-VERSION', coreVersion);
}
if (apiVersion) {
localStorage.setItem('LIGHTRAG-API-VERSION', apiVersion);
}
// Update state
set({
coreVersion: coreVersion,
apiVersion: apiVersion
});
} }
}; };
}); });