yangdx
commited on
Commit
·
9564069
1
Parent(s):
3a62f77
Refactor auth and version checks for improved reliability.
Browse files- Prevent duplicate version checks in Vite dev mode
- Simplify protected route handling
- Add session flags for version tracking
- lightrag_webui/src/App.tsx +57 -11
- lightrag_webui/src/AppRouter.tsx +15 -112
- lightrag_webui/src/features/LoginPage.tsx +29 -5
lightrag_webui/src/App.tsx
CHANGED
@@ -1,13 +1,13 @@
|
|
1 |
-
import { useState, useCallback } from 'react'
|
2 |
import ThemeProvider from '@/components/ThemeProvider'
|
3 |
import TabVisibilityProvider from '@/contexts/TabVisibilityProvider'
|
4 |
import MessageAlert from '@/components/MessageAlert'
|
5 |
import ApiKeyAlert from '@/components/ApiKeyAlert'
|
6 |
import StatusIndicator from '@/components/graph/StatusIndicator'
|
7 |
import { healthCheckInterval } from '@/lib/constants'
|
8 |
-
import { useBackendState } from '@/stores/state'
|
9 |
import { useSettingsStore } from '@/stores/settings'
|
10 |
-
import {
|
11 |
import SiteHeader from '@/features/SiteHeader'
|
12 |
import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
|
13 |
|
@@ -23,17 +23,63 @@ function App() {
|
|
23 |
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
24 |
const currentTab = useSettingsStore.use.currentTab()
|
25 |
const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
|
|
|
26 |
|
27 |
-
// Health check
|
28 |
useEffect(() => {
|
29 |
-
//
|
30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
|
38 |
const handleTabChange = useCallback(
|
39 |
(tab: string) => useSettingsStore.getState().setCurrentTab(tab as any),
|
|
|
1 |
+
import { useState, useCallback, useEffect, useRef } from 'react'
|
2 |
import ThemeProvider from '@/components/ThemeProvider'
|
3 |
import TabVisibilityProvider from '@/contexts/TabVisibilityProvider'
|
4 |
import MessageAlert from '@/components/MessageAlert'
|
5 |
import ApiKeyAlert from '@/components/ApiKeyAlert'
|
6 |
import StatusIndicator from '@/components/graph/StatusIndicator'
|
7 |
import { healthCheckInterval } from '@/lib/constants'
|
8 |
+
import { useBackendState, useAuthStore } from '@/stores/state'
|
9 |
import { useSettingsStore } from '@/stores/settings'
|
10 |
+
import { getAuthStatus } from '@/api/lightrag'
|
11 |
import SiteHeader from '@/features/SiteHeader'
|
12 |
import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
|
13 |
|
|
|
23 |
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
24 |
const currentTab = useSettingsStore.use.currentTab()
|
25 |
const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
|
26 |
+
const versionCheckRef = useRef(false); // Prevent duplicate calls in Vite dev mode
|
27 |
|
28 |
+
// Health check - can be disabled
|
29 |
useEffect(() => {
|
30 |
+
// Only execute if health check is enabled
|
31 |
+
if (!enableHealthCheck) return;
|
32 |
+
|
33 |
+
// Health check function
|
34 |
+
const performHealthCheck = async () => {
|
35 |
+
await useBackendState.getState().check();
|
36 |
+
};
|
37 |
+
|
38 |
+
// Execute immediately
|
39 |
+
performHealthCheck();
|
40 |
+
|
41 |
+
// Set interval for periodic execution
|
42 |
+
const interval = setInterval(performHealthCheck, healthCheckInterval * 1000);
|
43 |
+
return () => clearInterval(interval);
|
44 |
+
}, [enableHealthCheck]);
|
45 |
|
46 |
+
// Version check - independent and executed only once
|
47 |
+
useEffect(() => {
|
48 |
+
const checkVersion = async () => {
|
49 |
+
// Prevent duplicate calls in Vite dev mode
|
50 |
+
if (versionCheckRef.current) return;
|
51 |
+
versionCheckRef.current = true;
|
52 |
+
|
53 |
+
// Check if version info was already obtained in login page
|
54 |
+
const versionCheckedFromLogin = sessionStorage.getItem('VERSION_CHECKED_FROM_LOGIN') === 'true';
|
55 |
+
if (versionCheckedFromLogin) return;
|
56 |
+
|
57 |
+
// Get version info
|
58 |
+
const token = localStorage.getItem('LIGHTRAG-API-TOKEN');
|
59 |
+
if (!token) return;
|
60 |
+
|
61 |
+
try {
|
62 |
+
const status = await getAuthStatus();
|
63 |
+
if (status.core_version || status.api_version) {
|
64 |
+
// Update version info while maintaining login state
|
65 |
+
useAuthStore.getState().login(
|
66 |
+
token,
|
67 |
+
useAuthStore.getState().isGuestMode,
|
68 |
+
status.core_version,
|
69 |
+
status.api_version
|
70 |
+
);
|
71 |
+
|
72 |
+
// Set flag to indicate version info has been checked
|
73 |
+
sessionStorage.setItem('VERSION_CHECKED_FROM_LOGIN', 'true');
|
74 |
+
}
|
75 |
+
} catch (error) {
|
76 |
+
console.error('Failed to get version info:', error);
|
77 |
+
}
|
78 |
+
};
|
79 |
+
|
80 |
+
// Execute version check
|
81 |
+
checkVersion();
|
82 |
+
}, []); // Empty dependency array ensures it only runs once on mount
|
83 |
|
84 |
const handleTabChange = useCallback(
|
85 |
(tab: string) => useSettingsStore.getState().setCurrentTab(tab as any),
|
lightrag_webui/src/AppRouter.tsx
CHANGED
@@ -2,98 +2,11 @@ import { HashRouter as Router, Routes, Route, useNavigate } from 'react-router-d
|
|
2 |
import { useEffect, useState } from 'react'
|
3 |
import { useAuthStore } from '@/stores/state'
|
4 |
import { navigationService } from '@/services/navigation'
|
5 |
-
import { getAuthStatus } from '@/api/lightrag'
|
6 |
-
import { toast } from 'sonner'
|
7 |
import { Toaster } from 'sonner'
|
8 |
import App from './App'
|
9 |
import LoginPage from '@/features/LoginPage'
|
10 |
import ThemeProvider from '@/components/ThemeProvider'
|
11 |
|
12 |
-
interface ProtectedRouteProps {
|
13 |
-
children: React.ReactNode
|
14 |
-
}
|
15 |
-
|
16 |
-
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
17 |
-
const { isAuthenticated } = useAuthStore()
|
18 |
-
const [isChecking, setIsChecking] = useState(true)
|
19 |
-
const navigate = useNavigate()
|
20 |
-
|
21 |
-
// Set navigate function for navigation service
|
22 |
-
useEffect(() => {
|
23 |
-
navigationService.setNavigate(navigate)
|
24 |
-
}, [navigate])
|
25 |
-
|
26 |
-
useEffect(() => {
|
27 |
-
let isMounted = true; // Flag to prevent state updates after unmount
|
28 |
-
|
29 |
-
// This effect will run when the component mounts
|
30 |
-
// and will check if authentication is required
|
31 |
-
const checkAuthStatus = async () => {
|
32 |
-
try {
|
33 |
-
// Skip check if already authenticated
|
34 |
-
if (isAuthenticated) {
|
35 |
-
if (isMounted) setIsChecking(false);
|
36 |
-
return;
|
37 |
-
}
|
38 |
-
|
39 |
-
const status = await getAuthStatus()
|
40 |
-
|
41 |
-
// Only proceed if component is still mounted
|
42 |
-
if (!isMounted) return;
|
43 |
-
|
44 |
-
if (!status.auth_configured && status.access_token) {
|
45 |
-
// If auth is not configured, use the guest token
|
46 |
-
useAuthStore.getState().login(status.access_token, true, status.core_version, status.api_version)
|
47 |
-
if (status.message) {
|
48 |
-
toast.info(status.message)
|
49 |
-
}
|
50 |
-
}
|
51 |
-
} catch (error) {
|
52 |
-
console.error('Failed to check auth status:', error)
|
53 |
-
} finally {
|
54 |
-
// Only update state if component is still mounted
|
55 |
-
if (isMounted) {
|
56 |
-
setIsChecking(false)
|
57 |
-
}
|
58 |
-
}
|
59 |
-
}
|
60 |
-
|
61 |
-
// Execute immediately
|
62 |
-
checkAuthStatus()
|
63 |
-
|
64 |
-
// Cleanup function to prevent state updates after unmount
|
65 |
-
return () => {
|
66 |
-
isMounted = false;
|
67 |
-
}
|
68 |
-
}, [isAuthenticated])
|
69 |
-
|
70 |
-
// Handle navigation when authentication status changes
|
71 |
-
useEffect(() => {
|
72 |
-
if (!isChecking && !isAuthenticated) {
|
73 |
-
const currentPath = window.location.hash.slice(1); // Remove the '#' from hash
|
74 |
-
const isLoginPage = currentPath === '/login';
|
75 |
-
|
76 |
-
if (!isLoginPage) {
|
77 |
-
// Use navigation service for redirection
|
78 |
-
console.log('Not authenticated, redirecting to login');
|
79 |
-
navigationService.navigateToLogin();
|
80 |
-
}
|
81 |
-
}
|
82 |
-
}, [isChecking, isAuthenticated]);
|
83 |
-
|
84 |
-
// Show nothing while checking auth status or when not authenticated on login page
|
85 |
-
if (isChecking || (!isAuthenticated && window.location.hash.slice(1) === '/login')) {
|
86 |
-
return null;
|
87 |
-
}
|
88 |
-
|
89 |
-
// Show children only when authenticated
|
90 |
-
if (!isAuthenticated) {
|
91 |
-
return null;
|
92 |
-
}
|
93 |
-
|
94 |
-
return <>{children}</>;
|
95 |
-
}
|
96 |
-
|
97 |
const AppContent = () => {
|
98 |
const [initializing, setInitializing] = useState(true)
|
99 |
const { isAuthenticated } = useAuthStore()
|
@@ -104,34 +17,20 @@ const AppContent = () => {
|
|
104 |
navigationService.setNavigate(navigate)
|
105 |
}, [navigate])
|
106 |
|
107 |
-
//
|
108 |
useEffect(() => {
|
109 |
-
let isMounted = true;
|
110 |
|
111 |
const checkAuth = async () => {
|
112 |
try {
|
113 |
const token = localStorage.getItem('LIGHTRAG-API-TOKEN')
|
114 |
|
115 |
-
// If we have a token, we're already authenticated
|
116 |
if (token && isAuthenticated) {
|
117 |
if (isMounted) setInitializing(false);
|
118 |
return;
|
119 |
}
|
120 |
|
121 |
-
|
122 |
-
const status = await getAuthStatus()
|
123 |
-
|
124 |
-
// Only proceed if component is still mounted
|
125 |
-
if (!isMounted) return;
|
126 |
-
|
127 |
-
if (!status.auth_configured && status.access_token) {
|
128 |
-
// If auth is not configured, use the guest token
|
129 |
-
useAuthStore.getState().login(status.access_token, true, status.core_version, status.api_version)
|
130 |
-
if (status.message) {
|
131 |
-
toast.info(status.message)
|
132 |
-
}
|
133 |
-
} else if (!token) {
|
134 |
-
// Only logout if we don't have a token
|
135 |
useAuthStore.getState().logout()
|
136 |
}
|
137 |
} catch (error) {
|
@@ -140,22 +39,30 @@ const AppContent = () => {
|
|
140 |
useAuthStore.getState().logout()
|
141 |
}
|
142 |
} finally {
|
143 |
-
// Only update state if component is still mounted
|
144 |
if (isMounted) {
|
145 |
setInitializing(false)
|
146 |
}
|
147 |
}
|
148 |
}
|
149 |
|
150 |
-
// Execute immediately
|
151 |
checkAuth()
|
152 |
|
153 |
-
// Cleanup function to prevent state updates after unmount
|
154 |
return () => {
|
155 |
isMounted = false;
|
156 |
}
|
157 |
}, [isAuthenticated])
|
158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
159 |
// Show nothing while initializing
|
160 |
if (initializing) {
|
161 |
return null
|
@@ -166,11 +73,7 @@ const AppContent = () => {
|
|
166 |
<Route path="/login" element={<LoginPage />} />
|
167 |
<Route
|
168 |
path="/*"
|
169 |
-
element={
|
170 |
-
<ProtectedRoute>
|
171 |
-
<App />
|
172 |
-
</ProtectedRoute>
|
173 |
-
}
|
174 |
/>
|
175 |
</Routes>
|
176 |
)
|
|
|
2 |
import { useEffect, useState } from 'react'
|
3 |
import { useAuthStore } from '@/stores/state'
|
4 |
import { navigationService } from '@/services/navigation'
|
|
|
|
|
5 |
import { Toaster } from 'sonner'
|
6 |
import App from './App'
|
7 |
import LoginPage from '@/features/LoginPage'
|
8 |
import ThemeProvider from '@/components/ThemeProvider'
|
9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
const AppContent = () => {
|
11 |
const [initializing, setInitializing] = useState(true)
|
12 |
const { isAuthenticated } = useAuthStore()
|
|
|
17 |
navigationService.setNavigate(navigate)
|
18 |
}, [navigate])
|
19 |
|
20 |
+
// Token validity check
|
21 |
useEffect(() => {
|
22 |
+
let isMounted = true;
|
23 |
|
24 |
const checkAuth = async () => {
|
25 |
try {
|
26 |
const token = localStorage.getItem('LIGHTRAG-API-TOKEN')
|
27 |
|
|
|
28 |
if (token && isAuthenticated) {
|
29 |
if (isMounted) setInitializing(false);
|
30 |
return;
|
31 |
}
|
32 |
|
33 |
+
if (!token) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
useAuthStore.getState().logout()
|
35 |
}
|
36 |
} catch (error) {
|
|
|
39 |
useAuthStore.getState().logout()
|
40 |
}
|
41 |
} finally {
|
|
|
42 |
if (isMounted) {
|
43 |
setInitializing(false)
|
44 |
}
|
45 |
}
|
46 |
}
|
47 |
|
|
|
48 |
checkAuth()
|
49 |
|
|
|
50 |
return () => {
|
51 |
isMounted = false;
|
52 |
}
|
53 |
}, [isAuthenticated])
|
54 |
|
55 |
+
// Redirect effect for protected routes
|
56 |
+
useEffect(() => {
|
57 |
+
if (!initializing && !isAuthenticated) {
|
58 |
+
const currentPath = window.location.hash.slice(1);
|
59 |
+
if (currentPath !== '/login') {
|
60 |
+
console.log('Not authenticated, redirecting to login');
|
61 |
+
navigate('/login');
|
62 |
+
}
|
63 |
+
}
|
64 |
+
}, [initializing, isAuthenticated, navigate]);
|
65 |
+
|
66 |
// Show nothing while initializing
|
67 |
if (initializing) {
|
68 |
return null
|
|
|
73 |
<Route path="/login" element={<LoginPage />} />
|
74 |
<Route
|
75 |
path="/*"
|
76 |
+
element={isAuthenticated ? <App /> : null}
|
|
|
|
|
|
|
|
|
77 |
/>
|
78 |
</Routes>
|
79 |
)
|
lightrag_webui/src/features/LoginPage.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import { useState, useEffect } from 'react'
|
2 |
import { useNavigate } from 'react-router-dom'
|
3 |
import { useAuthStore } from '@/stores/state'
|
4 |
import { loginToServer, getAuthStatus } from '@/api/lightrag'
|
@@ -18,6 +18,7 @@ const LoginPage = () => {
|
|
18 |
const [username, setUsername] = useState('')
|
19 |
const [password, setPassword] = useState('')
|
20 |
const [checkingAuth, setCheckingAuth] = useState(true)
|
|
|
21 |
|
22 |
useEffect(() => {
|
23 |
console.log('LoginPage mounted')
|
@@ -28,6 +29,13 @@ const LoginPage = () => {
|
|
28 |
let isMounted = true; // Flag to prevent state updates after unmount
|
29 |
|
30 |
const checkAuthConfig = async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
try {
|
32 |
// If already authenticated, redirect to home
|
33 |
if (isAuthenticated) {
|
@@ -37,6 +45,17 @@ const LoginPage = () => {
|
|
37 |
|
38 |
// Check auth status
|
39 |
const status = await getAuthStatus()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
|
41 |
// Only proceed if component is still mounted
|
42 |
if (!isMounted) return;
|
@@ -48,16 +67,16 @@ const LoginPage = () => {
|
|
48 |
toast.info(status.message)
|
49 |
}
|
50 |
navigate('/')
|
51 |
-
return
|
52 |
}
|
53 |
} catch (error) {
|
54 |
console.error('Failed to check auth configuration:', error)
|
55 |
-
|
56 |
-
// Only update state if component is still mounted
|
57 |
if (isMounted) {
|
58 |
-
setCheckingAuth(false)
|
59 |
}
|
60 |
}
|
|
|
61 |
}
|
62 |
|
63 |
// Execute immediately
|
@@ -88,6 +107,11 @@ const LoginPage = () => {
|
|
88 |
// Check authentication mode
|
89 |
const isGuestMode = response.auth_mode === 'disabled'
|
90 |
login(response.access_token, isGuestMode, response.core_version, response.api_version)
|
|
|
|
|
|
|
|
|
|
|
91 |
|
92 |
if (isGuestMode) {
|
93 |
// Show authentication disabled notification
|
|
|
1 |
+
import { useState, useEffect, useRef } from 'react'
|
2 |
import { useNavigate } from 'react-router-dom'
|
3 |
import { useAuthStore } from '@/stores/state'
|
4 |
import { loginToServer, getAuthStatus } from '@/api/lightrag'
|
|
|
18 |
const [username, setUsername] = useState('')
|
19 |
const [password, setPassword] = useState('')
|
20 |
const [checkingAuth, setCheckingAuth] = useState(true)
|
21 |
+
const authCheckRef = useRef(false); // Prevent duplicate calls in Vite dev mode
|
22 |
|
23 |
useEffect(() => {
|
24 |
console.log('LoginPage mounted')
|
|
|
29 |
let isMounted = true; // Flag to prevent state updates after unmount
|
30 |
|
31 |
const checkAuthConfig = async () => {
|
32 |
+
// Prevent duplicate calls in Vite dev mode
|
33 |
+
if (authCheckRef.current) {
|
34 |
+
if (isMounted) setCheckingAuth(false);
|
35 |
+
return;
|
36 |
+
}
|
37 |
+
authCheckRef.current = true;
|
38 |
+
|
39 |
try {
|
40 |
// If already authenticated, redirect to home
|
41 |
if (isAuthenticated) {
|
|
|
45 |
|
46 |
// Check auth status
|
47 |
const status = await getAuthStatus()
|
48 |
+
|
49 |
+
// Set checkingAuth to false immediately after getAuthStatus
|
50 |
+
// This allows the login page to render while other processing continues
|
51 |
+
if (isMounted) {
|
52 |
+
setCheckingAuth(false);
|
53 |
+
}
|
54 |
+
|
55 |
+
// Set session flag for version check to avoid duplicate checks in App component
|
56 |
+
if (isMounted && (status.core_version || status.api_version)) {
|
57 |
+
sessionStorage.setItem('VERSION_CHECKED_FROM_LOGIN', 'true');
|
58 |
+
}
|
59 |
|
60 |
// Only proceed if component is still mounted
|
61 |
if (!isMounted) return;
|
|
|
67 |
toast.info(status.message)
|
68 |
}
|
69 |
navigate('/')
|
70 |
+
return
|
71 |
}
|
72 |
} catch (error) {
|
73 |
console.error('Failed to check auth configuration:', error)
|
74 |
+
// Also set checkingAuth to false in case of error
|
|
|
75 |
if (isMounted) {
|
76 |
+
setCheckingAuth(false);
|
77 |
}
|
78 |
}
|
79 |
+
// Removed finally block as we're setting checkingAuth earlier
|
80 |
}
|
81 |
|
82 |
// Execute immediately
|
|
|
107 |
// Check authentication mode
|
108 |
const isGuestMode = response.auth_mode === 'disabled'
|
109 |
login(response.access_token, isGuestMode, response.core_version, response.api_version)
|
110 |
+
|
111 |
+
// Set session flag for version check
|
112 |
+
if (response.core_version || response.api_version) {
|
113 |
+
sessionStorage.setItem('VERSION_CHECKED_FROM_LOGIN', 'true');
|
114 |
+
}
|
115 |
|
116 |
if (isGuestMode) {
|
117 |
// Show authentication disabled notification
|