Merge pull request #1162 from danielaskdd/improve-version-check
Refactor auth and version checks for improved reliability
This commit is contained in:
@@ -1 +1 @@
|
||||
__api_version__ = "1.2.1"
|
||||
__api_version__ = "1.2.2"
|
||||
|
@@ -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
|
||||
|
1
lightrag/api/webui/assets/index-BcBS1RaQ.css
generated
1
lightrag/api/webui/assets/index-BcBS1RaQ.css
generated
File diff suppressed because one or more lines are too long
1
lightrag/api/webui/assets/index-Cq65VeVX.css
generated
Normal file
1
lightrag/api/webui/assets/index-Cq65VeVX.css
generated
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
lightrag/api/webui/index.html
generated
4
lightrag/api/webui/index.html
generated
@@ -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>
|
||||
|
@@ -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),
|
||||
|
@@ -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>
|
||||
)
|
||||
|
@@ -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(
|
||||
|
@@ -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.'))
|
||||
|
@@ -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() {
|
||||
|
@@ -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
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
Reference in New Issue
Block a user