yangdx commited on
Commit
270cb30
·
1 Parent(s): 529287b

feat(auth): implement auto guest mode and enhance token system

Browse files

- Add role-based token system with metadata support
- Implement automatic guest mode for unconfigured authentication
- Create new /auth-status endpoint for authentication status checking
- Modify frontend to auto-detect auth status and bypass login when appropriate
- Add guest mode indicator in site header for better UX

This change allows users to automatically access the system without manual
login when authentication is not configured, while maintaining secure
authentication when credentials are properly set up.

lightrag/api/auth.py CHANGED
@@ -6,8 +6,10 @@ from pydantic import BaseModel
6
 
7
 
8
  class TokenPayload(BaseModel):
9
- sub: str
10
- exp: datetime
 
 
11
 
12
 
13
  class AuthHandler:
@@ -15,13 +17,55 @@ class AuthHandler:
15
  self.secret = os.getenv("TOKEN_SECRET", "4f85ds4f56dsf46")
16
  self.algorithm = "HS256"
17
  self.expire_hours = int(os.getenv("TOKEN_EXPIRE_HOURS", 4))
 
18
 
19
- def create_token(self, username: str) -> str:
20
- expire = datetime.utcnow() + timedelta(hours=self.expire_hours)
21
- payload = TokenPayload(sub=username, exp=expire)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  return jwt.encode(payload.dict(), self.secret, algorithm=self.algorithm)
23
 
24
- def validate_token(self, token: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
25
  try:
26
  payload = jwt.decode(token, self.secret, algorithms=[self.algorithm])
27
  expire_timestamp = payload["exp"]
@@ -31,7 +75,14 @@ class AuthHandler:
31
  raise HTTPException(
32
  status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired"
33
  )
34
- return payload["sub"]
 
 
 
 
 
 
 
35
  except jwt.PyJWTError:
36
  raise HTTPException(
37
  status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
 
6
 
7
 
8
  class TokenPayload(BaseModel):
9
+ sub: str # Username
10
+ exp: datetime # Expiration time
11
+ role: str = "user" # User role, default is regular user
12
+ metadata: dict = {} # Additional metadata
13
 
14
 
15
  class AuthHandler:
 
17
  self.secret = os.getenv("TOKEN_SECRET", "4f85ds4f56dsf46")
18
  self.algorithm = "HS256"
19
  self.expire_hours = int(os.getenv("TOKEN_EXPIRE_HOURS", 4))
20
+ self.guest_expire_hours = int(os.getenv("GUEST_TOKEN_EXPIRE_HOURS", 2)) # Guest token default expiration time
21
 
22
+ def create_token(self, username: str, role: str = "user", custom_expire_hours: int = None, metadata: dict = None) -> str:
23
+ """
24
+ Create JWT token
25
+
26
+ Args:
27
+ username: Username
28
+ role: User role, default is "user", guest is "guest"
29
+ custom_expire_hours: Custom expiration time (hours), if None use default value
30
+ metadata: Additional metadata
31
+
32
+ Returns:
33
+ str: Encoded JWT token
34
+ """
35
+ # Choose default expiration time based on role
36
+ if custom_expire_hours is None:
37
+ if role == "guest":
38
+ expire_hours = self.guest_expire_hours
39
+ else:
40
+ expire_hours = self.expire_hours
41
+ else:
42
+ expire_hours = custom_expire_hours
43
+
44
+ expire = datetime.utcnow() + timedelta(hours=expire_hours)
45
+
46
+ # Create payload
47
+ payload = TokenPayload(
48
+ sub=username,
49
+ exp=expire,
50
+ role=role,
51
+ metadata=metadata or {}
52
+ )
53
+
54
  return jwt.encode(payload.dict(), self.secret, algorithm=self.algorithm)
55
 
56
+ def validate_token(self, token: str) -> dict:
57
+ """
58
+ Validate JWT token
59
+
60
+ Args:
61
+ token: JWT token
62
+
63
+ Returns:
64
+ dict: Dictionary containing user information
65
+
66
+ Raises:
67
+ HTTPException: If token is invalid or expired
68
+ """
69
  try:
70
  payload = jwt.decode(token, self.secret, algorithms=[self.algorithm])
71
  expire_timestamp = payload["exp"]
 
75
  raise HTTPException(
76
  status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired"
77
  )
78
+
79
+ # Return complete payload instead of just username
80
+ return {
81
+ "username": payload["sub"],
82
+ "role": payload.get("role", "user"),
83
+ "metadata": payload.get("metadata", {}),
84
+ "exp": expire_time
85
+ }
86
  except jwt.PyJWTError:
87
  raise HTTPException(
88
  status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
lightrag/api/lightrag_server.py CHANGED
@@ -341,25 +341,66 @@ def create_app(args):
341
  ollama_api = OllamaAPI(rag, top_k=args.top_k)
342
  app.include_router(ollama_api.router, prefix="/api")
343
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  @app.post("/login", dependencies=[Depends(optional_api_key)])
345
  async def login(form_data: OAuth2PasswordRequestForm = Depends()):
346
  username = os.getenv("AUTH_USERNAME")
347
  password = os.getenv("AUTH_PASSWORD")
348
 
349
  if not (username and password):
350
- raise HTTPException(
351
- status_code=status.HTTP_501_NOT_IMPLEMENTED,
352
- detail="Authentication not configured",
 
 
353
  )
 
 
 
 
 
 
354
 
355
  if form_data.username != username or form_data.password != password:
356
  raise HTTPException(
357
  status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect credentials"
358
  )
359
 
 
 
 
 
 
 
360
  return {
361
- "access_token": auth_handler.create_token(username),
362
  "token_type": "bearer",
 
363
  }
364
 
365
  @app.get("/health", dependencies=[Depends(optional_api_key)])
 
341
  ollama_api = OllamaAPI(rag, top_k=args.top_k)
342
  app.include_router(ollama_api.router, prefix="/api")
343
 
344
+ @app.get("/auth-status", dependencies=[Depends(optional_api_key)])
345
+ async def get_auth_status():
346
+ """Get authentication status and guest token if auth is not configured"""
347
+ username = os.getenv("AUTH_USERNAME")
348
+ password = os.getenv("AUTH_PASSWORD")
349
+
350
+ if not (username and password):
351
+ # Authentication not configured, return guest token
352
+ guest_token = auth_handler.create_token(
353
+ username="guest",
354
+ role="guest",
355
+ metadata={"auth_mode": "disabled"}
356
+ )
357
+ return {
358
+ "auth_configured": False,
359
+ "access_token": guest_token,
360
+ "token_type": "bearer",
361
+ "auth_mode": "disabled",
362
+ "message": "Authentication is disabled. Using guest access."
363
+ }
364
+
365
+ return {
366
+ "auth_configured": True,
367
+ "auth_mode": "enabled"
368
+ }
369
+
370
  @app.post("/login", dependencies=[Depends(optional_api_key)])
371
  async def login(form_data: OAuth2PasswordRequestForm = Depends()):
372
  username = os.getenv("AUTH_USERNAME")
373
  password = os.getenv("AUTH_PASSWORD")
374
 
375
  if not (username and password):
376
+ # Authentication not configured, return guest token
377
+ guest_token = auth_handler.create_token(
378
+ username="guest",
379
+ role="guest",
380
+ metadata={"auth_mode": "disabled"}
381
  )
382
+ return {
383
+ "access_token": guest_token,
384
+ "token_type": "bearer",
385
+ "auth_mode": "disabled",
386
+ "message": "Authentication is disabled. Using guest access."
387
+ }
388
 
389
  if form_data.username != username or form_data.password != password:
390
  raise HTTPException(
391
  status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect credentials"
392
  )
393
 
394
+ # Regular user login
395
+ user_token = auth_handler.create_token(
396
+ username=username,
397
+ role="user",
398
+ metadata={"auth_mode": "enabled"}
399
+ )
400
  return {
401
+ "access_token": user_token,
402
  "token_type": "bearer",
403
+ "auth_mode": "enabled"
404
  }
405
 
406
  @app.get("/health", dependencies=[Depends(optional_api_key)])
lightrag/api/utils_api.py CHANGED
@@ -9,7 +9,7 @@ import sys
9
  import logging
10
  from ascii_colors import ASCIIColors
11
  from lightrag.api import __api_version__
12
- from fastapi import HTTPException, Security, Depends, Request
13
  from dotenv import load_dotenv
14
  from fastapi.security import APIKeyHeader, OAuth2PasswordBearer
15
  from starlette.status import HTTP_403_FORBIDDEN
@@ -35,7 +35,8 @@ ollama_server_infos = OllamaServerInfos()
35
 
36
 
37
  def get_auth_dependency():
38
- whitelist = os.getenv("WHITELIST_PATHS", "").split(",")
 
39
 
40
  async def dependency(
41
  request: Request,
@@ -44,10 +45,41 @@ def get_auth_dependency():
44
  if request.url.path in whitelist:
45
  return
46
 
47
- if not (os.getenv("AUTH_USERNAME") and os.getenv("AUTH_PASSWORD")):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  return
49
-
50
- auth_handler.validate_token(token)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  return dependency
53
 
 
9
  import logging
10
  from ascii_colors import ASCIIColors
11
  from lightrag.api import __api_version__
12
+ from fastapi import HTTPException, Security, Depends, Request, status
13
  from dotenv import load_dotenv
14
  from fastapi.security import APIKeyHeader, OAuth2PasswordBearer
15
  from starlette.status import HTTP_403_FORBIDDEN
 
35
 
36
 
37
  def get_auth_dependency():
38
+ # Set default whitelist paths
39
+ whitelist = os.getenv("WHITELIST_PATHS", "/login,/health").split(",")
40
 
41
  async def dependency(
42
  request: Request,
 
45
  if request.url.path in whitelist:
46
  return
47
 
48
+ # Check if authentication is configured
49
+ auth_configured = bool(os.getenv("AUTH_USERNAME") and os.getenv("AUTH_PASSWORD"))
50
+
51
+ # If authentication is not configured, accept any token including guest tokens
52
+ if not auth_configured:
53
+ if token: # If token is provided, still validate it
54
+ try:
55
+ # Validate token but don't raise exception
56
+ token_info = auth_handler.validate_token(token)
57
+ # Check if it's a guest token
58
+ if token_info.get("role") != "guest":
59
+ # Non-guest tokens are not valid when auth is not configured
60
+ pass
61
+ except Exception as e:
62
+ # Ignore validation errors but log them
63
+ print(f"Token validation error (ignored): {str(e)}")
64
  return
65
+
66
+ # If authentication is configured, validate the token and reject guest tokens
67
+ if not token:
68
+ raise HTTPException(
69
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Token required"
70
+ )
71
+
72
+ token_info = auth_handler.validate_token(token)
73
+
74
+ # Reject guest tokens when authentication is configured
75
+ if token_info.get("role") == "guest":
76
+ raise HTTPException(
77
+ status_code=status.HTTP_401_UNAUTHORIZED,
78
+ detail="Authentication required. Guest access not allowed when authentication is configured."
79
+ )
80
+
81
+ # At this point, we have a valid non-guest token
82
+ return
83
 
84
  return dependency
85
 
lightrag_webui/env.local.sample CHANGED
@@ -1,3 +1,3 @@
1
  VITE_BACKEND_URL=http://localhost:9621
2
  VITE_API_PROXY=true
3
- VITE_API_ENDPOINTS=/api,/documents,/graphs,/graph,/health,/query,/docs,/openapi.json,/login
 
1
  VITE_BACKEND_URL=http://localhost:9621
2
  VITE_API_PROXY=true
3
+ VITE_API_ENDPOINTS=/api,/documents,/graphs,/graph,/health,/query,/docs,/openapi.json,/login,/auth-status
lightrag_webui/src/AppRouter.tsx CHANGED
@@ -1,6 +1,8 @@
1
  import { HashRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
2
- import { useEffect } from 'react'
3
  import { useAuthStore } from '@/stores/state'
 
 
4
  import { Toaster } from 'sonner'
5
  import App from './App'
6
  import LoginPage from '@/features/LoginPage'
@@ -12,7 +14,58 @@ interface ProtectedRouteProps {
12
 
13
  const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
14
  const { isAuthenticated } = useAuthStore()
 
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  if (!isAuthenticated) {
17
  return <Navigate to="/login" replace />
18
  }
@@ -21,13 +74,65 @@ const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
21
  }
22
 
23
  const AppRouter = () => {
24
- // Check login at befor startup
 
 
 
25
  useEffect(() => {
26
- const token = localStorage.getItem('LIGHTRAG-API-TOKEN');
27
- if (!token) {
28
- useAuthStore.getState().logout();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  }
30
- }, []);
 
 
 
 
 
31
 
32
  return (
33
  <ThemeProvider>
 
1
  import { HashRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
2
+ import { useEffect, useState } from 'react'
3
  import { useAuthStore } from '@/stores/state'
4
+ import { getAuthStatus } from '@/api/lightrag'
5
+ import { toast } from 'sonner'
6
  import { Toaster } from 'sonner'
7
  import App from './App'
8
  import LoginPage from '@/features/LoginPage'
 
14
 
15
  const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
16
  const { isAuthenticated } = useAuthStore()
17
+ const [isChecking, setIsChecking] = useState(true)
18
 
19
+ useEffect(() => {
20
+ let isMounted = true; // Flag to prevent state updates after unmount
21
+
22
+ // This effect will run when the component mounts
23
+ // and will check if authentication is required
24
+ const checkAuthStatus = async () => {
25
+ try {
26
+ // Skip check if already authenticated
27
+ if (isAuthenticated) {
28
+ if (isMounted) setIsChecking(false);
29
+ return;
30
+ }
31
+
32
+ const status = await getAuthStatus()
33
+
34
+ // Only proceed if component is still mounted
35
+ if (!isMounted) return;
36
+
37
+ if (!status.auth_configured && status.access_token) {
38
+ // If auth is not configured, use the guest token
39
+ useAuthStore.getState().login(status.access_token, true)
40
+ if (status.message) {
41
+ toast.info(status.message)
42
+ }
43
+ }
44
+ } catch (error) {
45
+ console.error('Failed to check auth status:', error)
46
+ } finally {
47
+ // Only update state if component is still mounted
48
+ if (isMounted) {
49
+ setIsChecking(false)
50
+ }
51
+ }
52
+ }
53
+
54
+ // Execute immediately
55
+ checkAuthStatus()
56
+
57
+ // Cleanup function to prevent state updates after unmount
58
+ return () => {
59
+ isMounted = false;
60
+ }
61
+ }, [isAuthenticated])
62
+
63
+ // Show nothing while checking auth status
64
+ if (isChecking) {
65
+ return null
66
+ }
67
+
68
+ // After checking, if still not authenticated, redirect to login
69
  if (!isAuthenticated) {
70
  return <Navigate to="/login" replace />
71
  }
 
74
  }
75
 
76
  const AppRouter = () => {
77
+ const [initializing, setInitializing] = useState(true)
78
+ const { isAuthenticated } = useAuthStore()
79
+
80
+ // Check token validity and auth configuration on app initialization
81
  useEffect(() => {
82
+ let isMounted = true; // Flag to prevent state updates after unmount
83
+
84
+ const checkAuth = async () => {
85
+ try {
86
+ const token = localStorage.getItem('LIGHTRAG-API-TOKEN')
87
+
88
+ // If we have a token, we're already authenticated
89
+ if (token && isAuthenticated) {
90
+ if (isMounted) setInitializing(false);
91
+ return;
92
+ }
93
+
94
+ // If no token or not authenticated, check if auth is configured
95
+ const status = await getAuthStatus()
96
+
97
+ // Only proceed if component is still mounted
98
+ if (!isMounted) return;
99
+
100
+ if (!status.auth_configured && status.access_token) {
101
+ // If auth is not configured, use the guest token
102
+ useAuthStore.getState().login(status.access_token, true)
103
+ if (status.message) {
104
+ toast.info(status.message)
105
+ }
106
+ } else if (!token) {
107
+ // Only logout if we don't have a token
108
+ useAuthStore.getState().logout()
109
+ }
110
+ } catch (error) {
111
+ console.error('Auth initialization error:', error)
112
+ if (isMounted && !isAuthenticated) {
113
+ useAuthStore.getState().logout()
114
+ }
115
+ } finally {
116
+ // Only update state if component is still mounted
117
+ if (isMounted) {
118
+ setInitializing(false)
119
+ }
120
+ }
121
+ }
122
+
123
+ // Execute immediately
124
+ checkAuth()
125
+
126
+ // Cleanup function to prevent state updates after unmount
127
+ return () => {
128
+ isMounted = false;
129
  }
130
+ }, [isAuthenticated])
131
+
132
+ // Show nothing while initializing
133
+ if (initializing) {
134
+ return null
135
+ }
136
 
137
  return (
138
  <ThemeProvider>
lightrag_webui/src/api/lightrag.ts CHANGED
@@ -126,9 +126,19 @@ export type DocsStatusesResponse = {
126
  statuses: Record<DocStatus, DocStatusResponse[]>
127
  }
128
 
 
 
 
 
 
 
 
 
129
  export type LoginResponse = {
130
  access_token: string
131
  token_type: string
 
 
132
  }
133
 
134
  export const InvalidApiKeyError = 'Invalid API Key'
@@ -356,6 +366,63 @@ export const clearDocuments = async (): Promise<DocActionResponse> => {
356
  return response.data
357
  }
358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  export const loginToServer = async (username: string, password: string): Promise<LoginResponse> => {
360
  const formData = new FormData();
361
  formData.append('username', username);
 
126
  statuses: Record<DocStatus, DocStatusResponse[]>
127
  }
128
 
129
+ export type AuthStatusResponse = {
130
+ auth_configured: boolean
131
+ access_token?: string
132
+ token_type?: string
133
+ auth_mode?: 'enabled' | 'disabled'
134
+ message?: string
135
+ }
136
+
137
  export type LoginResponse = {
138
  access_token: string
139
  token_type: string
140
+ auth_mode?: 'enabled' | 'disabled' // Authentication mode identifier
141
+ message?: string // Optional message
142
  }
143
 
144
  export const InvalidApiKeyError = 'Invalid API Key'
 
366
  return response.data
367
  }
368
 
369
+ export const getAuthStatus = async (): Promise<AuthStatusResponse> => {
370
+ try {
371
+ // Add a timeout to the request to prevent hanging
372
+ const response = await axiosInstance.get('/auth-status', {
373
+ timeout: 5000, // 5 second timeout
374
+ headers: {
375
+ 'Accept': 'application/json' // Explicitly request JSON
376
+ }
377
+ });
378
+
379
+ // Check if response is HTML (which indicates a redirect or wrong endpoint)
380
+ const contentType = response.headers['content-type'] || '';
381
+ if (contentType.includes('text/html')) {
382
+ console.warn('Received HTML response instead of JSON for auth-status endpoint');
383
+ return {
384
+ auth_configured: true,
385
+ auth_mode: 'enabled'
386
+ };
387
+ }
388
+
389
+ // Strict validation of the response data
390
+ if (response.data &&
391
+ typeof response.data === 'object' &&
392
+ 'auth_configured' in response.data &&
393
+ typeof response.data.auth_configured === 'boolean') {
394
+
395
+ // For unconfigured auth, ensure we have an access token
396
+ if (!response.data.auth_configured) {
397
+ if (response.data.access_token && typeof response.data.access_token === 'string') {
398
+ return response.data;
399
+ } else {
400
+ console.warn('Auth not configured but no valid access token provided');
401
+ }
402
+ } else {
403
+ // For configured auth, just return the data
404
+ return response.data;
405
+ }
406
+ }
407
+
408
+ // If response data is invalid but we got a response, log it
409
+ console.warn('Received invalid auth status response:', response.data);
410
+
411
+ // Default to auth configured if response is invalid
412
+ return {
413
+ auth_configured: true,
414
+ auth_mode: 'enabled'
415
+ };
416
+ } catch (error) {
417
+ // If the request fails, assume authentication is configured
418
+ console.error('Failed to get auth status:', errorMessage(error));
419
+ return {
420
+ auth_configured: true,
421
+ auth_mode: 'enabled'
422
+ };
423
+ }
424
+ }
425
+
426
  export const loginToServer = async (username: string, password: string): Promise<LoginResponse> => {
427
  const formData = new FormData();
428
  formData.append('username', username);
lightrag_webui/src/features/LoginPage.tsx CHANGED
@@ -1,7 +1,7 @@
1
- import { useState } from 'react'
2
  import { useNavigate } from 'react-router-dom'
3
  import { useAuthStore } from '@/stores/state'
4
- import { loginToServer } from '@/api/lightrag'
5
  import { toast } from 'sonner'
6
  import { useTranslation } from 'react-i18next'
7
 
@@ -13,11 +13,63 @@ import AppSettings from '@/components/AppSettings'
13
 
14
  const LoginPage = () => {
15
  const navigate = useNavigate()
16
- const { login } = useAuthStore()
17
  const { t } = useTranslation()
18
  const [loading, setLoading] = useState(false)
19
  const [username, setUsername] = useState('')
20
  const [password, setPassword] = useState('')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
23
  e.preventDefault()
@@ -29,9 +81,19 @@ const LoginPage = () => {
29
  try {
30
  setLoading(true)
31
  const response = await loginToServer(username, password)
32
- login(response.access_token)
 
 
 
 
 
 
 
 
 
 
 
33
  navigate('/')
34
- toast.success(t('login.successMessage'))
35
  } catch (error) {
36
  console.error('Login failed...', error)
37
  toast.error(t('login.errorInvalidCredentials'))
 
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'
5
  import { toast } from 'sonner'
6
  import { useTranslation } from 'react-i18next'
7
 
 
13
 
14
  const LoginPage = () => {
15
  const navigate = useNavigate()
16
+ const { login, isAuthenticated } = useAuthStore()
17
  const { t } = useTranslation()
18
  const [loading, setLoading] = useState(false)
19
  const [username, setUsername] = useState('')
20
  const [password, setPassword] = useState('')
21
+ const [checkingAuth, setCheckingAuth] = useState(true)
22
+
23
+ // Check if authentication is configured
24
+ useEffect(() => {
25
+ let isMounted = true; // Flag to prevent state updates after unmount
26
+
27
+ const checkAuthConfig = async () => {
28
+ try {
29
+ // If already authenticated, redirect to home
30
+ if (isAuthenticated) {
31
+ navigate('/')
32
+ return
33
+ }
34
+
35
+ // Check auth status
36
+ const status = await getAuthStatus()
37
+
38
+ // Only proceed if component is still mounted
39
+ if (!isMounted) return;
40
+
41
+ if (!status.auth_configured && status.access_token) {
42
+ // If auth is not configured, use the guest token and redirect
43
+ login(status.access_token, true)
44
+ if (status.message) {
45
+ toast.info(status.message)
46
+ }
47
+ navigate('/')
48
+ return; // Exit early, no need to set checkingAuth to false
49
+ }
50
+ } catch (error) {
51
+ console.error('Failed to check auth configuration:', error)
52
+ } finally {
53
+ // Only update state if component is still mounted
54
+ if (isMounted) {
55
+ setCheckingAuth(false)
56
+ }
57
+ }
58
+ }
59
+
60
+ // Execute immediately
61
+ checkAuthConfig()
62
+
63
+ // Cleanup function to prevent state updates after unmount
64
+ return () => {
65
+ isMounted = false;
66
+ }
67
+ }, [isAuthenticated, login, navigate])
68
+
69
+ // Don't render anything while checking auth
70
+ if (checkingAuth) {
71
+ return null
72
+ }
73
 
74
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
75
  e.preventDefault()
 
81
  try {
82
  setLoading(true)
83
  const response = await loginToServer(username, password)
84
+
85
+ // Check authentication mode
86
+ const isGuestMode = response.auth_mode === 'disabled'
87
+ login(response.access_token, isGuestMode)
88
+
89
+ if (isGuestMode) {
90
+ // Show authentication disabled notification
91
+ toast.info(response.message || t('login.authDisabled', 'Authentication is disabled. Using guest access.'))
92
+ } else {
93
+ toast.success(t('login.successMessage'))
94
+ }
95
+
96
  navigate('/')
 
97
  } catch (error) {
98
  console.error('Login failed...', error)
99
  toast.error(t('login.errorInvalidCredentials'))
lightrag_webui/src/features/SiteHeader.tsx CHANGED
@@ -57,7 +57,7 @@ function TabsNavigation() {
57
  export default function SiteHeader() {
58
  const { t } = useTranslation()
59
  const navigate = useNavigate()
60
- const { logout } = useAuthStore()
61
 
62
  const handleLogout = () => {
63
  logout()
@@ -74,6 +74,11 @@ export default function SiteHeader() {
74
 
75
  <div className="flex h-10 flex-1 justify-center">
76
  <TabsNavigation />
 
 
 
 
 
77
  </div>
78
 
79
  <nav className="flex items-center">
 
57
  export default function SiteHeader() {
58
  const { t } = useTranslation()
59
  const navigate = useNavigate()
60
+ const { logout, isGuestMode } = useAuthStore()
61
 
62
  const handleLogout = () => {
63
  logout()
 
74
 
75
  <div className="flex h-10 flex-1 justify-center">
76
  <TabsNavigation />
77
+ {isGuestMode && (
78
+ <div className="ml-2 self-center px-2 py-1 text-xs bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200 rounded-md">
79
+ {t('login.guestMode', 'Guest Mode')}
80
+ </div>
81
+ )}
82
  </div>
83
 
84
  <nav className="flex items-center">
lightrag_webui/src/locales/en.json CHANGED
@@ -28,7 +28,9 @@
28
  "loggingIn": "Logging in...",
29
  "successMessage": "Login succeeded",
30
  "errorEmptyFields": "Please enter your username and password",
31
- "errorInvalidCredentials": "Login failed, please check username and password"
 
 
32
  },
33
  "documentPanel": {
34
  "clearDocuments": {
 
28
  "loggingIn": "Logging in...",
29
  "successMessage": "Login succeeded",
30
  "errorEmptyFields": "Please enter your username and password",
31
+ "errorInvalidCredentials": "Login failed, please check username and password",
32
+ "authDisabled": "Authentication is disabled. Using guest access.",
33
+ "guestMode": "Guest Mode"
34
  },
35
  "documentPanel": {
36
  "clearDocuments": {
lightrag_webui/src/locales/zh.json CHANGED
@@ -28,7 +28,9 @@
28
  "loggingIn": "登录中...",
29
  "successMessage": "登录成功",
30
  "errorEmptyFields": "请输入您的用户名和密码",
31
- "errorInvalidCredentials": "登录失败,请检查用户名和密码"
 
 
32
  },
33
  "documentPanel": {
34
  "clearDocuments": {
 
28
  "loggingIn": "登录中...",
29
  "successMessage": "登录成功",
30
  "errorEmptyFields": "请输入您的用户名和密码",
31
+ "errorInvalidCredentials": "登录失败,请检查用户名和密码",
32
+ "authDisabled": "认证已禁用,使用访客访问模式。",
33
+ "guestMode": "访客模式"
34
  },
35
  "documentPanel": {
36
  "clearDocuments": {
lightrag_webui/src/stores/state.ts CHANGED
@@ -19,7 +19,8 @@ interface BackendState {
19
  interface AuthState {
20
  isAuthenticated: boolean;
21
  showLoginModal: boolean;
22
- login: (token: string) => void;
 
23
  logout: () => void;
24
  setShowLoginModal: (show: boolean) => void;
25
  }
@@ -66,16 +67,63 @@ const useBackendState = createSelectors(useBackendStateStoreBase)
66
 
67
  export { useBackendState }
68
 
69
- export const useAuthStore = create<AuthState>(set => ({
70
- isAuthenticated: !!localStorage.getItem('LIGHTRAG-API-TOKEN'),
71
- showLoginModal: false,
72
- login: (token) => {
73
- localStorage.setItem('LIGHTRAG-API-TOKEN', token);
74
- set({ isAuthenticated: true, showLoginModal: false });
75
- },
76
- logout: () => {
77
- localStorage.removeItem('LIGHTRAG-API-TOKEN');
78
- set({ isAuthenticated: false });
79
- },
80
- setShowLoginModal: (show) => set({ showLoginModal: show })
81
- }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  interface AuthState {
20
  isAuthenticated: boolean;
21
  showLoginModal: boolean;
22
+ isGuestMode: boolean; // Add guest mode flag
23
+ login: (token: string, isGuest?: boolean) => void;
24
  logout: () => void;
25
  setShowLoginModal: (show: boolean) => void;
26
  }
 
67
 
68
  export { useBackendState }
69
 
70
+ // Helper function to check if token is a guest token
71
+ const isGuestToken = (token: string): boolean => {
72
+ try {
73
+ // JWT tokens are in the format: header.payload.signature
74
+ const parts = token.split('.');
75
+ if (parts.length !== 3) return false;
76
+
77
+ // Decode the payload (second part)
78
+ const payload = JSON.parse(atob(parts[1]));
79
+
80
+ // Check if the token has a role field with value "guest"
81
+ return payload.role === 'guest';
82
+ } catch (e) {
83
+ console.error('Error parsing token:', e);
84
+ return false;
85
+ }
86
+ };
87
+
88
+ // Initialize auth state from localStorage
89
+ const initAuthState = (): { isAuthenticated: boolean; isGuestMode: boolean } => {
90
+ const token = localStorage.getItem('LIGHTRAG-API-TOKEN');
91
+ if (!token) {
92
+ return { isAuthenticated: false, isGuestMode: false };
93
+ }
94
+
95
+ return {
96
+ isAuthenticated: true,
97
+ isGuestMode: isGuestToken(token)
98
+ };
99
+ };
100
+
101
+ export const useAuthStore = create<AuthState>(set => {
102
+ // Get initial state from localStorage
103
+ const initialState = initAuthState();
104
+
105
+ return {
106
+ isAuthenticated: initialState.isAuthenticated,
107
+ showLoginModal: false,
108
+ isGuestMode: initialState.isGuestMode,
109
+
110
+ login: (token, isGuest = false) => {
111
+ localStorage.setItem('LIGHTRAG-API-TOKEN', token);
112
+ set({
113
+ isAuthenticated: true,
114
+ showLoginModal: false,
115
+ isGuestMode: isGuest
116
+ });
117
+ },
118
+
119
+ logout: () => {
120
+ localStorage.removeItem('LIGHTRAG-API-TOKEN');
121
+ set({
122
+ isAuthenticated: false,
123
+ isGuestMode: false
124
+ });
125
+ },
126
+
127
+ setShowLoginModal: (show) => set({ showLoginModal: show })
128
+ };
129
+ });