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
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 {
"status": "healthy",
"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,
},
"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

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" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag</title>
<script type="module" crossorigin src="/webui/assets/index-BRzImUsU.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-BcBS1RaQ.css">
<script type="module" crossorigin src="/webui/assets/index-DlScqWrq.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-Cq65VeVX.css">
</head>
<body>
<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 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,64 @@ 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;
const interval = setInterval(async () => {
await useBackendState.getState().check()
}, healthCheckInterval * 1000)
return () => clearInterval(interval)
}, [enableHealthCheck])
// 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]);
// 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(
(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 { 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,58 +17,48 @@ 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
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);
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) {
console.error('Auth initialization error:', error)
if (isMounted && !isAuthenticated) {
if (!isAuthenticated) {
useAuthStore.getState().logout()
}
} finally {
// Only update state if component is still mounted
if (isMounted) {
setInitializing(false)
}
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 +69,7 @@ const AppContent = () => {
<Route path="/login" element={<LoginPage />} />
<Route
path="/*"
element={
<ProtectedRoute>
<App />
</ProtectedRoute>
}
element={isAuthenticated ? <App /> : null}
/>
</Routes>
)

View File

@@ -41,6 +41,10 @@ export type LightragStatus = {
graph_storage: string
vector_storage: string
}
update_status?: Record<string, any>
core_version?: string
api_version?: string
auth_mode?: 'enabled' | 'disabled'
}
export type LightragDocumentsScanProgress = {
@@ -183,8 +187,9 @@ axiosInstance.interceptors.response.use(
}
// For other APIs, navigate to login page
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(
`${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 { 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')
@@ -25,9 +26,14 @@ const LoginPage = () => {
// Check if authentication is configured, skip login if not
useEffect(() => {
let isMounted = true; // Flag to prevent state updates after unmount
const checkAuthConfig = async () => {
// Prevent duplicate calls in Vite dev mode
if (authCheckRef.current) {
return;
}
authCheckRef.current = true;
try {
// If already authenticated, redirect to home
if (isAuthenticated) {
@@ -38,8 +44,10 @@ const LoginPage = () => {
// Check auth status
const status = await getAuthStatus()
// Only proceed if component is still mounted
if (!isMounted) return;
// Set session flag for version check to avoid duplicate checks in App component
if (status.core_version || status.api_version) {
sessionStorage.setItem('VERSION_CHECKED_FROM_LOGIN', 'true');
}
if (!status.auth_configured && status.access_token) {
// If auth is not configured, use the guest token and redirect
@@ -48,16 +56,18 @@ const LoginPage = () => {
toast.info(status.message)
}
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) {
console.error('Failed to check auth configuration:', error)
} finally {
// Only update state if component is still mounted
if (isMounted) {
setCheckingAuth(false)
}
// Also set checkingAuth to false in case of error
setCheckingAuth(false);
}
// Removed finally block as we're setting checkingAuth earlier
}
// Execute immediately
@@ -65,7 +75,6 @@ const LoginPage = () => {
// Cleanup function to prevent state updates after unmount
return () => {
isMounted = false;
}
}, [isAuthenticated, login, navigate])
@@ -89,6 +98,11 @@ const LoginPage = () => {
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
toast.info(response.message || t('login.authDisabled', 'Authentication is disabled. Using guest access.'))

View File

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

View File

@@ -23,6 +23,7 @@ interface AuthState {
apiVersion: string | null;
login: (token: string, isGuest?: boolean, coreVersion?: string | null, apiVersion?: string | null) => void;
logout: () => void;
setVersion: (coreVersion: string | null, apiVersion: string | null) => void;
}
const useBackendStateStoreBase = create<BackendState>()((set) => ({
@@ -35,6 +36,14 @@ const useBackendStateStoreBase = create<BackendState>()((set) => ({
check: async () => {
const health = await checkHealth()
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({
health: true,
message: null,
@@ -148,6 +157,22 @@ export const useAuthStore = create<AuthState>(set => {
coreVersion: coreVersion,
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
});
}
};
});