Merge pull request #1124 from danielaskdd/improve-login-flow

Improve login flow
This commit is contained in:
Daniel.y
2025-03-19 21:42:52 +08:00
committed by GitHub
17 changed files with 307 additions and 269 deletions

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-CSrxfS-k.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-mPRIIErN.css">
<script type="module" crossorigin src="/webui/assets/index-C56SCRGK.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-BE_O4IWQ.css">
</head>
<body>
<div id="root"></div>

View File

@@ -5,11 +5,9 @@ import MessageAlert from '@/components/MessageAlert'
import ApiKeyAlert from '@/components/ApiKeyAlert'
import StatusIndicator from '@/components/graph/StatusIndicator'
import { healthCheckInterval } from '@/lib/constants'
import { useBackendState, useAuthStore } from '@/stores/state'
import { useBackendState } from '@/stores/state'
import { useSettingsStore } from '@/stores/settings'
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { navigationService } from '@/services/navigation'
import SiteHeader from '@/features/SiteHeader'
import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
@@ -21,22 +19,13 @@ import ApiSite from '@/features/ApiSite'
import { Tabs, TabsContent } from '@/components/ui/Tabs'
function App() {
const navigate = useNavigate();
const message = useBackendState.use.message()
// Initialize navigation service
useEffect(() => {
navigationService.setNavigate(navigate);
}, [navigate]);
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
const currentTab = useSettingsStore.use.currentTab()
const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
// Health check
useEffect(() => {
const { isAuthenticated } = useAuthStore.getState();
if (!enableHealthCheck || !isAuthenticated) return
// Check immediately
useBackendState.getState().check()

View File

@@ -1,6 +1,7 @@
import { HashRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
import { HashRouter as Router, Routes, Route, useNavigate } from 'react-router-dom'
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'
@@ -15,6 +16,12 @@ interface ProtectedRouteProps {
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
@@ -65,17 +72,38 @@ const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
return null
}
// After checking, if still not authenticated, redirect to login
// After checking, if still not authenticated
if (!isAuthenticated) {
return <Navigate to="/login" replace />
// Get current path and check if it's a direct access
const currentPath = window.location.hash.slice(1); // Remove the '#' from hash
const isLoginPage = currentPath === '/login';
// Skip redirect if already on login page
if (isLoginPage) {
return null;
}
// For non-login pages, handle state reset and navigation
if (!isLoginPage) {
// Use navigation service for redirection
console.log('Not authenticated, redirecting to login');
navigationService.navigateToLogin();
return null;
}
}
return <>{children}</>
}
const AppRouter = () => {
const AppContent = () => {
const [initializing, setInitializing] = useState(true)
const { isAuthenticated } = useAuthStore()
const navigate = useNavigate()
// Set navigate function for navigation service
useEffect(() => {
navigationService.setNavigate(navigate)
}, [navigate])
// Check token validity and auth configuration on app initialization
useEffect(() => {
@@ -134,21 +162,27 @@ const AppRouter = () => {
return null
}
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/*"
element={
<ProtectedRoute>
<App />
</ProtectedRoute>
}
/>
</Routes>
)
}
const AppRouter = () => {
return (
<ThemeProvider>
<Router>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/*"
element={
<ProtectedRoute>
<App />
</ProtectedRoute>
}
/>
</Routes>
<Toaster position="top-center" />
<AppContent />
<Toaster position="bottom-center" />
</Router>
</ThemeProvider>
)

View File

@@ -2,7 +2,6 @@ import axios, { AxiosError } from 'axios'
import { backendBaseUrl } from '@/lib/constants'
import { errorMessage } from '@/lib/utils'
import { useSettingsStore } from '@/stores/settings'
import { useAuthStore } from '@/stores/state'
import { navigationService } from '@/services/navigation'
// Types
@@ -174,13 +173,12 @@ axiosInstance.interceptors.response.use(
(error: AxiosError) => {
if (error.response) {
if (error.response?.status === 401) {
localStorage.removeItem('LIGHTRAG-API-TOKEN');
sessionStorage.clear();
useAuthStore.getState().logout();
// Use navigation service to handle redirection
// For login API, throw error directly
if (error.config?.url?.includes('/login')) {
throw error;
}
// For other APIs, navigate to login page
navigationService.navigateToLogin();
// Return a never-resolving promise to prevent further execution
return new Promise(() => {});
}

View File

@@ -32,7 +32,7 @@ export default function AppSettings({ className }: AppSettingsProps) {
return (
<Popover open={opened} onOpenChange={setOpened}>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className={cn("h-9 w-9", className)}>
<Button variant="ghost" size="icon" className={cn('h-9 w-9', className)}>
<PaletteIcon className="h-5 w-5" />
</Button>
</PopoverTrigger>

View File

@@ -29,8 +29,6 @@ const GraphLabels = () => {
// Set global flag to indicate we've attempted to fetch in this session
useGraphStore.getState().setLabelsFetchAttempted(true)
console.log('Fetching graph labels (once per session)...')
useGraphStore.getState().fetchAllDatabaseLabels()
.then(() => {
labelsLoadedRef.current = true

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { useState, useEffect, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useTabVisibility } from '@/contexts/useTabVisibility'
import { useSettingsStore } from '@/stores/settings'
import Button from '@/components/ui/Button'
import {
Table,
@@ -27,9 +27,7 @@ export default function DocumentManager() {
const { t } = useTranslation()
const health = useBackendState.use.health()
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
const { isTabVisible } = useTabVisibility()
const isDocumentsTabVisible = isTabVisible('documents')
const initialLoadRef = useRef(false)
const currentTab = useSettingsStore.use.currentTab()
const fetchDocuments = useCallback(async () => {
try {
@@ -53,13 +51,12 @@ export default function DocumentManager() {
}
}, [setDocs, t])
// Only fetch documents when the tab becomes visible for the first time
// Fetch documents when the tab becomes visible
useEffect(() => {
if (isDocumentsTabVisible && !initialLoadRef.current) {
if (currentTab === 'documents') {
fetchDocuments()
initialLoadRef.current = true
}
}, [isDocumentsTabVisible, fetchDocuments])
}, [currentTab, fetchDocuments])
const scanDocuments = useCallback(async () => {
try {
@@ -70,9 +67,9 @@ export default function DocumentManager() {
}
}, [t])
// Only set up polling when the tab is visible and health is good
// Set up polling when the documents tab is active and health is good
useEffect(() => {
if (!isDocumentsTabVisible || !health) {
if (currentTab !== 'documents' || !health) {
return
}
@@ -85,7 +82,7 @@ export default function DocumentManager() {
}, 5000)
return () => clearInterval(interval)
}, [health, fetchDocuments, t, isDocumentsTabVisible])
}, [health, fetchDocuments, t, currentTab])
return (
<Card className="!size-full !rounded-none !border-none">

View File

@@ -1,5 +1,4 @@
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
import { useTabVisibility } from '@/contexts/useTabVisibility'
// import { MiniMap } from '@react-sigma/minimap'
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
import { Settings as SigmaSettings } from 'sigma/settings'
@@ -108,52 +107,39 @@ const GraphEvents = () => {
const GraphViewer = () => {
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
const sigmaRef = useRef<any>(null)
const initAttemptedRef = useRef(false)
const selectedNode = useGraphStore.use.selectedNode()
const focusedNode = useGraphStore.use.focusedNode()
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
const isFetching = useGraphStore.use.isFetching()
const shouldRender = useGraphStore.use.shouldRender() // Rendering control state
// Get tab visibility
const { isTabVisible } = useTabVisibility()
const isGraphTabVisible = isTabVisible('knowledge-graph')
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
// Handle component mount/unmount and tab visibility
useEffect(() => {
// When component mounts or tab becomes visible
if (isGraphTabVisible && !shouldRender && !isFetching && !initAttemptedRef.current) {
// If tab is visible but graph is not rendering, try to enable rendering
useGraphStore.getState().setShouldRender(true)
initAttemptedRef.current = true
console.log('Graph viewer initialized')
}
// Cleanup function when component unmounts
return () => {
// Only log cleanup, don't actually clean up the WebGL context
// This allows the WebGL context to persist across tab switches
console.log('Graph viewer cleanup')
}
}, [isGraphTabVisible, shouldRender, isFetching])
// Initialize sigma settings once on component mount
// All dynamic settings will be updated in GraphControl using useSetSettings
useEffect(() => {
setSigmaSettings(defaultSigmaSettings)
console.log('Initialized sigma settings')
}, [])
// Clean up sigma instance when component unmounts
useEffect(() => {
return () => {
// Clear the sigma instance when component unmounts
useGraphStore.getState().setSigmaInstance(null);
console.log('Cleared sigma instance on unmount');
// TAB is mount twice in vite dev mode, this is a workaround
const sigma = useGraphStore.getState().sigmaInstance;
if (sigma) {
try {
// Destroy sigmaand clear WebGL context
sigma.kill();
useGraphStore.getState().setSigmaInstance(null);
console.log('Cleared sigma instance on Graphviewer unmount');
} catch (error) {
console.error('Error cleaning up sigma instance:', error);
}
}
};
}, []);

View File

@@ -4,7 +4,6 @@ import { useAuthStore } from '@/stores/state'
import { loginToServer, getAuthStatus } from '@/api/lightrag'
import { toast } from 'sonner'
import { useTranslation } from 'react-i18next'
import { Card, CardContent, CardHeader } from '@/components/ui/Card'
import Input from '@/components/ui/Input'
import Button from '@/components/ui/Button'
@@ -20,7 +19,11 @@ const LoginPage = () => {
const [password, setPassword] = useState('')
const [checkingAuth, setCheckingAuth] = useState(true)
// Check if authentication is configured
useEffect(() => {
console.log('LoginPage mounted')
}, []);
// Check if authentication is configured, skip login if not
useEffect(() => {
let isMounted = true; // Flag to prevent state updates after unmount
@@ -45,7 +48,7 @@ const LoginPage = () => {
toast.info(status.message)
}
navigate('/')
return; // Exit early, no need to set checkingAuth to false
return // Exit early, no need to set checkingAuth to false
}
} catch (error) {
console.error('Failed to check auth configuration:', error)
@@ -93,10 +96,16 @@ const LoginPage = () => {
toast.success(t('login.successMessage'))
}
// Navigate to home page after successful login
navigate('/')
} catch (error) {
console.error('Login failed...', error)
toast.error(t('login.errorInvalidCredentials'))
// Clear any existing auth state
useAuthStore.getState().logout()
// Clear local storage
localStorage.removeItem('LIGHTRAG-API-TOKEN')
} finally {
setLoading(false)
}

View File

@@ -6,8 +6,7 @@ import { useSettingsStore } from '@/stores/settings'
import { useAuthStore } from '@/stores/state'
import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { navigationService } from '@/services/navigation'
import { ZapIcon, GithubIcon, LogOutIcon } from 'lucide-react'
interface NavigationTabProps {
@@ -56,12 +55,10 @@ function TabsNavigation() {
export default function SiteHeader() {
const { t } = useTranslation()
const navigate = useNavigate()
const { logout, isGuestMode } = useAuthStore()
const { isGuestMode } = useAuthStore()
const handleLogout = () => {
logout()
navigate('/login')
navigationService.navigateToLogin();
}
return (

View File

@@ -8,7 +8,6 @@ import { toast } from 'sonner'
import { queryGraphs } from '@/api/lightrag'
import { useBackendState } from '@/stores/state'
import { useSettingsStore } from '@/stores/settings'
import { useTabVisibility } from '@/contexts/useTabVisibility'
import seedrandom from 'seedrandom'
@@ -190,10 +189,6 @@ const useLightrangeGraph = () => {
const nodeToExpand = useGraphStore.use.nodeToExpand()
const nodeToPrune = useGraphStore.use.nodeToPrune()
// Get tab visibility
const { isTabVisible } = useTabVisibility()
const isGraphTabVisible = isTabVisible('knowledge-graph')
// Track previous parameters to detect actual changes
const prevParamsRef = useRef({ queryLabel, maxQueryDepth, minDegree })
@@ -248,19 +243,12 @@ const useLightrangeGraph = () => {
if (!isFetching && !fetchInProgressRef.current &&
(paramsChanged || !useGraphStore.getState().graphDataFetchAttempted)) {
// Only fetch data if the Graph tab is visible and we haven't attempted a fetch yet
if (!isGraphTabVisible) {
console.log('Graph tab not visible, skipping data fetch');
return;
}
// Set flags
fetchInProgressRef.current = true
useGraphStore.getState().setGraphDataFetchAttempted(true)
const state = useGraphStore.getState()
state.setIsFetching(true)
state.setShouldRender(false) // Disable rendering during data loading
// Clear selection and highlighted nodes before fetching new graph
state.clearSelection()
@@ -305,8 +293,6 @@ const useLightrangeGraph = () => {
// Reset camera view
state.setMoveToSelectedNode(true)
// Enable rendering if the tab is visible
state.setShouldRender(isGraphTabVisible)
state.setIsFetching(false)
}).catch((error) => {
console.error('Error fetching graph data:', error)
@@ -314,29 +300,12 @@ const useLightrangeGraph = () => {
// Reset state on error
const state = useGraphStore.getState()
state.setIsFetching(false)
state.setShouldRender(isGraphTabVisible)
dataLoadedRef.current = false
fetchInProgressRef.current = false
state.setGraphDataFetchAttempted(false)
})
}
}, [queryLabel, maxQueryDepth, minDegree, isFetching, paramsChanged, isGraphTabVisible, rawGraph, sigmaGraph])
// Update rendering state and handle tab visibility changes
useEffect(() => {
// When tab becomes visible
if (isGraphTabVisible) {
// If we have data, enable rendering
if (rawGraph) {
useGraphStore.getState().setShouldRender(true)
}
// We no longer reset the fetch attempted flag here to prevent continuous API calls
} else {
// When tab becomes invisible, disable rendering
useGraphStore.getState().setShouldRender(false)
}
}, [isGraphTabVisible, rawGraph])
}, [queryLabel, maxQueryDepth, minDegree, isFetching, paramsChanged, rawGraph, sigmaGraph])
// Handle node expansion
useEffect(() => {

View File

@@ -1,4 +1,7 @@
import { NavigateFunction } from 'react-router-dom';
import { useAuthStore, useBackendState } from '@/stores/state';
import { useGraphStore } from '@/stores/graph';
import { useSettingsStore } from '@/stores/settings';
class NavigationService {
private navigate: NavigateFunction | null = null;
@@ -7,11 +10,80 @@ class NavigationService {
this.navigate = navigate;
}
navigateToLogin() {
if (this.navigate) {
this.navigate('/login');
/**
* Reset all application state to ensure a clean environment.
* This function should be called when:
* 1. User logs out
* 2. Authentication token expires
* 3. Direct access to login page
*/
resetAllApplicationState() {
console.log('Resetting all application state...');
// Reset graph state
const graphStore = useGraphStore.getState();
const sigma = graphStore.sigmaInstance;
graphStore.reset();
graphStore.setGraphDataFetchAttempted(false);
graphStore.setLabelsFetchAttempted(false);
graphStore.setSigmaInstance(null);
// Reset backend state
useBackendState.getState().clear();
// Reset retrieval history while preserving other user preferences
useSettingsStore.getState().setRetrievalHistory([]);
// Clear authentication state
sessionStorage.clear();
if (sigma) {
sigma.getGraph().clear();
sigma.kill();
useGraphStore.getState().setSigmaInstance(null);
}
}
/**
* Handle direct access to login page
* @returns true if it's a direct access, false if navigated from another page
*/
handleDirectLoginAccess() {
const isDirectAccess = !document.referrer;
if (isDirectAccess) {
this.resetAllApplicationState();
}
return isDirectAccess;
}
/**
* Navigate to login page and reset application state
* @param skipReset whether to skip state reset (used for direct access scenario where reset is already handled)
*/
navigateToLogin() {
if (!this.navigate) {
console.error('Navigation function not set');
return;
}
// First navigate to login page
this.navigate('/login');
// Then reset state after navigation
setTimeout(() => {
this.resetAllApplicationState();
useAuthStore.getState().logout();
}, 0);
}
navigateToHome() {
if (!this.navigate) {
console.error('Navigation function not set');
return;
}
this.navigate('/');
}
}
export const navigationService = new NavigationService();

View File

@@ -74,7 +74,6 @@ interface GraphState {
moveToSelectedNode: boolean
isFetching: boolean
shouldRender: boolean
// Global flags to track data fetching attempts
graphDataFetchAttempted: boolean
@@ -95,7 +94,6 @@ interface GraphState {
setAllDatabaseLabels: (labels: string[]) => void
fetchAllDatabaseLabels: () => Promise<void>
setIsFetching: (isFetching: boolean) => void
setShouldRender: (shouldRender: boolean) => void
// 搜索引擎方法
setSearchEngine: (engine: MiniSearch | null) => void
@@ -122,7 +120,6 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
moveToSelectedNode: false,
isFetching: false,
shouldRender: false,
// Initialize global flags
graphDataFetchAttempted: false,
@@ -137,7 +134,6 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
setIsFetching: (isFetching: boolean) => set({ isFetching }),
setShouldRender: (shouldRender: boolean) => set({ shouldRender }),
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
set({ selectedNode: nodeId, moveToSelectedNode }),
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
@@ -159,8 +155,7 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
rawGraph: null,
sigmaGraph: null, // to avoid other components from acccessing graph objects
searchEngine: null,
moveToSelectedNode: false,
shouldRender: false
moveToSelectedNode: false
});
},

View File

@@ -18,11 +18,9 @@ interface BackendState {
interface AuthState {
isAuthenticated: boolean;
showLoginModal: boolean;
isGuestMode: boolean; // Add guest mode flag
login: (token: string, isGuest?: boolean) => void;
logout: () => void;
setShowLoginModal: (show: boolean) => void;
}
const useBackendStateStoreBase = create<BackendState>()((set) => ({
@@ -104,14 +102,12 @@ export const useAuthStore = create<AuthState>(set => {
return {
isAuthenticated: initialState.isAuthenticated,
showLoginModal: false,
isGuestMode: initialState.isGuestMode,
login: (token, isGuest = false) => {
localStorage.setItem('LIGHTRAG-API-TOKEN', token);
set({
isAuthenticated: true,
showLoginModal: false,
isGuestMode: isGuest
});
},
@@ -122,8 +118,6 @@ export const useAuthStore = create<AuthState>(set => {
isAuthenticated: false,
isGuestMode: false
});
},
setShowLoginModal: (show) => set({ showLoginModal: show })
}
};
});