feat(auth): implement auto guest mode and enhance token system
- 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.
This commit is contained in:
@@ -6,8 +6,10 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
|
|
||||||
class TokenPayload(BaseModel):
|
class TokenPayload(BaseModel):
|
||||||
sub: str
|
sub: str # Username
|
||||||
exp: datetime
|
exp: datetime # Expiration time
|
||||||
|
role: str = "user" # User role, default is regular user
|
||||||
|
metadata: dict = {} # Additional metadata
|
||||||
|
|
||||||
|
|
||||||
class AuthHandler:
|
class AuthHandler:
|
||||||
@@ -15,13 +17,55 @@ class AuthHandler:
|
|||||||
self.secret = os.getenv("TOKEN_SECRET", "4f85ds4f56dsf46")
|
self.secret = os.getenv("TOKEN_SECRET", "4f85ds4f56dsf46")
|
||||||
self.algorithm = "HS256"
|
self.algorithm = "HS256"
|
||||||
self.expire_hours = int(os.getenv("TOKEN_EXPIRE_HOURS", 4))
|
self.expire_hours = int(os.getenv("TOKEN_EXPIRE_HOURS", 4))
|
||||||
|
self.guest_expire_hours = int(os.getenv("GUEST_TOKEN_EXPIRE_HOURS", 2)) # Guest token default expiration time
|
||||||
|
|
||||||
|
def create_token(self, username: str, role: str = "user", custom_expire_hours: int = None, metadata: dict = None) -> str:
|
||||||
|
"""
|
||||||
|
Create JWT token
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: Username
|
||||||
|
role: User role, default is "user", guest is "guest"
|
||||||
|
custom_expire_hours: Custom expiration time (hours), if None use default value
|
||||||
|
metadata: Additional metadata
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Encoded JWT token
|
||||||
|
"""
|
||||||
|
# Choose default expiration time based on role
|
||||||
|
if custom_expire_hours is None:
|
||||||
|
if role == "guest":
|
||||||
|
expire_hours = self.guest_expire_hours
|
||||||
|
else:
|
||||||
|
expire_hours = self.expire_hours
|
||||||
|
else:
|
||||||
|
expire_hours = custom_expire_hours
|
||||||
|
|
||||||
|
expire = datetime.utcnow() + timedelta(hours=expire_hours)
|
||||||
|
|
||||||
|
# Create payload
|
||||||
|
payload = TokenPayload(
|
||||||
|
sub=username,
|
||||||
|
exp=expire,
|
||||||
|
role=role,
|
||||||
|
metadata=metadata or {}
|
||||||
|
)
|
||||||
|
|
||||||
def create_token(self, username: str) -> str:
|
|
||||||
expire = datetime.utcnow() + timedelta(hours=self.expire_hours)
|
|
||||||
payload = TokenPayload(sub=username, exp=expire)
|
|
||||||
return jwt.encode(payload.dict(), self.secret, algorithm=self.algorithm)
|
return jwt.encode(payload.dict(), self.secret, algorithm=self.algorithm)
|
||||||
|
|
||||||
def validate_token(self, token: str) -> str:
|
def validate_token(self, token: str) -> dict:
|
||||||
|
"""
|
||||||
|
Validate JWT token
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token: JWT token
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary containing user information
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If token is invalid or expired
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(token, self.secret, algorithms=[self.algorithm])
|
payload = jwt.decode(token, self.secret, algorithms=[self.algorithm])
|
||||||
expire_timestamp = payload["exp"]
|
expire_timestamp = payload["exp"]
|
||||||
@@ -31,7 +75,14 @@ class AuthHandler:
|
|||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired"
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired"
|
||||||
)
|
)
|
||||||
return payload["sub"]
|
|
||||||
|
# Return complete payload instead of just username
|
||||||
|
return {
|
||||||
|
"username": payload["sub"],
|
||||||
|
"role": payload.get("role", "user"),
|
||||||
|
"metadata": payload.get("metadata", {}),
|
||||||
|
"exp": expire_time
|
||||||
|
}
|
||||||
except jwt.PyJWTError:
|
except jwt.PyJWTError:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
|
||||||
|
@@ -341,25 +341,66 @@ def create_app(args):
|
|||||||
ollama_api = OllamaAPI(rag, top_k=args.top_k)
|
ollama_api = OllamaAPI(rag, top_k=args.top_k)
|
||||||
app.include_router(ollama_api.router, prefix="/api")
|
app.include_router(ollama_api.router, prefix="/api")
|
||||||
|
|
||||||
|
@app.get("/auth-status", dependencies=[Depends(optional_api_key)])
|
||||||
|
async def get_auth_status():
|
||||||
|
"""Get authentication status and guest token if auth is not configured"""
|
||||||
|
username = os.getenv("AUTH_USERNAME")
|
||||||
|
password = os.getenv("AUTH_PASSWORD")
|
||||||
|
|
||||||
|
if not (username and password):
|
||||||
|
# Authentication not configured, return guest token
|
||||||
|
guest_token = auth_handler.create_token(
|
||||||
|
username="guest",
|
||||||
|
role="guest",
|
||||||
|
metadata={"auth_mode": "disabled"}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"auth_configured": False,
|
||||||
|
"access_token": guest_token,
|
||||||
|
"token_type": "bearer",
|
||||||
|
"auth_mode": "disabled",
|
||||||
|
"message": "Authentication is disabled. Using guest access."
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"auth_configured": True,
|
||||||
|
"auth_mode": "enabled"
|
||||||
|
}
|
||||||
|
|
||||||
@app.post("/login", dependencies=[Depends(optional_api_key)])
|
@app.post("/login", dependencies=[Depends(optional_api_key)])
|
||||||
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||||
username = os.getenv("AUTH_USERNAME")
|
username = os.getenv("AUTH_USERNAME")
|
||||||
password = os.getenv("AUTH_PASSWORD")
|
password = os.getenv("AUTH_PASSWORD")
|
||||||
|
|
||||||
if not (username and password):
|
if not (username and password):
|
||||||
raise HTTPException(
|
# Authentication not configured, return guest token
|
||||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
guest_token = auth_handler.create_token(
|
||||||
detail="Authentication not configured",
|
username="guest",
|
||||||
|
role="guest",
|
||||||
|
metadata={"auth_mode": "disabled"}
|
||||||
)
|
)
|
||||||
|
return {
|
||||||
|
"access_token": guest_token,
|
||||||
|
"token_type": "bearer",
|
||||||
|
"auth_mode": "disabled",
|
||||||
|
"message": "Authentication is disabled. Using guest access."
|
||||||
|
}
|
||||||
|
|
||||||
if form_data.username != username or form_data.password != password:
|
if form_data.username != username or form_data.password != password:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect credentials"
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect credentials"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Regular user login
|
||||||
|
user_token = auth_handler.create_token(
|
||||||
|
username=username,
|
||||||
|
role="user",
|
||||||
|
metadata={"auth_mode": "enabled"}
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"access_token": auth_handler.create_token(username),
|
"access_token": user_token,
|
||||||
"token_type": "bearer",
|
"token_type": "bearer",
|
||||||
|
"auth_mode": "enabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.get("/health", dependencies=[Depends(optional_api_key)])
|
@app.get("/health", dependencies=[Depends(optional_api_key)])
|
||||||
|
@@ -9,7 +9,7 @@ import sys
|
|||||||
import logging
|
import logging
|
||||||
from ascii_colors import ASCIIColors
|
from ascii_colors import ASCIIColors
|
||||||
from lightrag.api import __api_version__
|
from lightrag.api import __api_version__
|
||||||
from fastapi import HTTPException, Security, Depends, Request
|
from fastapi import HTTPException, Security, Depends, Request, status
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from fastapi.security import APIKeyHeader, OAuth2PasswordBearer
|
from fastapi.security import APIKeyHeader, OAuth2PasswordBearer
|
||||||
from starlette.status import HTTP_403_FORBIDDEN
|
from starlette.status import HTTP_403_FORBIDDEN
|
||||||
@@ -35,7 +35,8 @@ ollama_server_infos = OllamaServerInfos()
|
|||||||
|
|
||||||
|
|
||||||
def get_auth_dependency():
|
def get_auth_dependency():
|
||||||
whitelist = os.getenv("WHITELIST_PATHS", "").split(",")
|
# Set default whitelist paths
|
||||||
|
whitelist = os.getenv("WHITELIST_PATHS", "/login,/health").split(",")
|
||||||
|
|
||||||
async def dependency(
|
async def dependency(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -44,10 +45,41 @@ def get_auth_dependency():
|
|||||||
if request.url.path in whitelist:
|
if request.url.path in whitelist:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not (os.getenv("AUTH_USERNAME") and os.getenv("AUTH_PASSWORD")):
|
# Check if authentication is configured
|
||||||
|
auth_configured = bool(os.getenv("AUTH_USERNAME") and os.getenv("AUTH_PASSWORD"))
|
||||||
|
|
||||||
|
# If authentication is not configured, accept any token including guest tokens
|
||||||
|
if not auth_configured:
|
||||||
|
if token: # If token is provided, still validate it
|
||||||
|
try:
|
||||||
|
# Validate token but don't raise exception
|
||||||
|
token_info = auth_handler.validate_token(token)
|
||||||
|
# Check if it's a guest token
|
||||||
|
if token_info.get("role") != "guest":
|
||||||
|
# Non-guest tokens are not valid when auth is not configured
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
# Ignore validation errors but log them
|
||||||
|
print(f"Token validation error (ignored): {str(e)}")
|
||||||
return
|
return
|
||||||
|
|
||||||
auth_handler.validate_token(token)
|
# If authentication is configured, validate the token and reject guest tokens
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="Token required"
|
||||||
|
)
|
||||||
|
|
||||||
|
token_info = auth_handler.validate_token(token)
|
||||||
|
|
||||||
|
# Reject guest tokens when authentication is configured
|
||||||
|
if token_info.get("role") == "guest":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Authentication required. Guest access not allowed when authentication is configured."
|
||||||
|
)
|
||||||
|
|
||||||
|
# At this point, we have a valid non-guest token
|
||||||
|
return
|
||||||
|
|
||||||
return dependency
|
return dependency
|
||||||
|
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
VITE_BACKEND_URL=http://localhost:9621
|
VITE_BACKEND_URL=http://localhost:9621
|
||||||
VITE_API_PROXY=true
|
VITE_API_PROXY=true
|
||||||
VITE_API_ENDPOINTS=/api,/documents,/graphs,/graph,/health,/query,/docs,/openapi.json,/login
|
VITE_API_ENDPOINTS=/api,/documents,/graphs,/graph,/health,/query,/docs,/openapi.json,/login,/auth-status
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import { HashRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
|
import { HashRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
|
||||||
import { useEffect } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useAuthStore } from '@/stores/state'
|
import { useAuthStore } from '@/stores/state'
|
||||||
|
import { getAuthStatus } from '@/api/lightrag'
|
||||||
|
import { toast } from 'sonner'
|
||||||
import { Toaster } from 'sonner'
|
import { Toaster } from 'sonner'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import LoginPage from '@/features/LoginPage'
|
import LoginPage from '@/features/LoginPage'
|
||||||
@@ -12,7 +14,58 @@ interface ProtectedRouteProps {
|
|||||||
|
|
||||||
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
||||||
const { isAuthenticated } = useAuthStore()
|
const { isAuthenticated } = useAuthStore()
|
||||||
|
const [isChecking, setIsChecking] = useState(true)
|
||||||
|
|
||||||
|
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)
|
||||||
|
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])
|
||||||
|
|
||||||
|
// Show nothing while checking auth status
|
||||||
|
if (isChecking) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// After checking, if still not authenticated, redirect to login
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return <Navigate to="/login" replace />
|
return <Navigate to="/login" replace />
|
||||||
}
|
}
|
||||||
@@ -21,13 +74,65 @@ const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AppRouter = () => {
|
const AppRouter = () => {
|
||||||
// Check login at befor startup
|
const [initializing, setInitializing] = useState(true)
|
||||||
|
const { isAuthenticated } = useAuthStore()
|
||||||
|
|
||||||
|
// Check token validity and auth configuration on app initialization
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem('LIGHTRAG-API-TOKEN');
|
let isMounted = true; // Flag to prevent state updates after unmount
|
||||||
if (!token) {
|
|
||||||
useAuthStore.getState().logout();
|
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);
|
||||||
|
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)
|
||||||
|
if (status.message) {
|
||||||
|
toast.info(status.message)
|
||||||
|
}
|
||||||
|
} else if (!token) {
|
||||||
|
// Only logout if we don't have a token
|
||||||
|
useAuthStore.getState().logout()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth initialization error:', error)
|
||||||
|
if (isMounted && !isAuthenticated) {
|
||||||
|
useAuthStore.getState().logout()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Only update state if component is still mounted
|
||||||
|
if (isMounted) {
|
||||||
|
setInitializing(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute immediately
|
||||||
|
checkAuth()
|
||||||
|
|
||||||
|
// Cleanup function to prevent state updates after unmount
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
}
|
||||||
|
}, [isAuthenticated])
|
||||||
|
|
||||||
|
// Show nothing while initializing
|
||||||
|
if (initializing) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
|
@@ -126,9 +126,19 @@ export type DocsStatusesResponse = {
|
|||||||
statuses: Record<DocStatus, DocStatusResponse[]>
|
statuses: Record<DocStatus, DocStatusResponse[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AuthStatusResponse = {
|
||||||
|
auth_configured: boolean
|
||||||
|
access_token?: string
|
||||||
|
token_type?: string
|
||||||
|
auth_mode?: 'enabled' | 'disabled'
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type LoginResponse = {
|
export type LoginResponse = {
|
||||||
access_token: string
|
access_token: string
|
||||||
token_type: string
|
token_type: string
|
||||||
|
auth_mode?: 'enabled' | 'disabled' // Authentication mode identifier
|
||||||
|
message?: string // Optional message
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InvalidApiKeyError = 'Invalid API Key'
|
export const InvalidApiKeyError = 'Invalid API Key'
|
||||||
@@ -356,6 +366,63 @@ export const clearDocuments = async (): Promise<DocActionResponse> => {
|
|||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getAuthStatus = async (): Promise<AuthStatusResponse> => {
|
||||||
|
try {
|
||||||
|
// Add a timeout to the request to prevent hanging
|
||||||
|
const response = await axiosInstance.get('/auth-status', {
|
||||||
|
timeout: 5000, // 5 second timeout
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json' // Explicitly request JSON
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if response is HTML (which indicates a redirect or wrong endpoint)
|
||||||
|
const contentType = response.headers['content-type'] || '';
|
||||||
|
if (contentType.includes('text/html')) {
|
||||||
|
console.warn('Received HTML response instead of JSON for auth-status endpoint');
|
||||||
|
return {
|
||||||
|
auth_configured: true,
|
||||||
|
auth_mode: 'enabled'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strict validation of the response data
|
||||||
|
if (response.data &&
|
||||||
|
typeof response.data === 'object' &&
|
||||||
|
'auth_configured' in response.data &&
|
||||||
|
typeof response.data.auth_configured === 'boolean') {
|
||||||
|
|
||||||
|
// For unconfigured auth, ensure we have an access token
|
||||||
|
if (!response.data.auth_configured) {
|
||||||
|
if (response.data.access_token && typeof response.data.access_token === 'string') {
|
||||||
|
return response.data;
|
||||||
|
} else {
|
||||||
|
console.warn('Auth not configured but no valid access token provided');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For configured auth, just return the data
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If response data is invalid but we got a response, log it
|
||||||
|
console.warn('Received invalid auth status response:', response.data);
|
||||||
|
|
||||||
|
// Default to auth configured if response is invalid
|
||||||
|
return {
|
||||||
|
auth_configured: true,
|
||||||
|
auth_mode: 'enabled'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// If the request fails, assume authentication is configured
|
||||||
|
console.error('Failed to get auth status:', errorMessage(error));
|
||||||
|
return {
|
||||||
|
auth_configured: true,
|
||||||
|
auth_mode: 'enabled'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const loginToServer = async (username: string, password: string): Promise<LoginResponse> => {
|
export const loginToServer = async (username: string, password: string): Promise<LoginResponse> => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('username', username);
|
formData.append('username', username);
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useAuthStore } from '@/stores/state'
|
import { useAuthStore } from '@/stores/state'
|
||||||
import { loginToServer } from '@/api/lightrag'
|
import { loginToServer, getAuthStatus } from '@/api/lightrag'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
@@ -13,11 +13,63 @@ import AppSettings from '@/components/AppSettings'
|
|||||||
|
|
||||||
const LoginPage = () => {
|
const LoginPage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { login } = useAuthStore()
|
const { login, isAuthenticated } = useAuthStore()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
|
const [checkingAuth, setCheckingAuth] = useState(true)
|
||||||
|
|
||||||
|
// Check if authentication is configured
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true; // Flag to prevent state updates after unmount
|
||||||
|
|
||||||
|
const checkAuthConfig = async () => {
|
||||||
|
try {
|
||||||
|
// If already authenticated, redirect to home
|
||||||
|
if (isAuthenticated) {
|
||||||
|
navigate('/')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check auth status
|
||||||
|
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 and redirect
|
||||||
|
login(status.access_token, true)
|
||||||
|
if (status.message) {
|
||||||
|
toast.info(status.message)
|
||||||
|
}
|
||||||
|
navigate('/')
|
||||||
|
return; // Exit early, no need to set checkingAuth to false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check auth configuration:', error)
|
||||||
|
} finally {
|
||||||
|
// Only update state if component is still mounted
|
||||||
|
if (isMounted) {
|
||||||
|
setCheckingAuth(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute immediately
|
||||||
|
checkAuthConfig()
|
||||||
|
|
||||||
|
// Cleanup function to prevent state updates after unmount
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, login, navigate])
|
||||||
|
|
||||||
|
// Don't render anything while checking auth
|
||||||
|
if (checkingAuth) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -29,9 +81,19 @@ const LoginPage = () => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const response = await loginToServer(username, password)
|
const response = await loginToServer(username, password)
|
||||||
login(response.access_token)
|
|
||||||
navigate('/')
|
// Check authentication mode
|
||||||
|
const isGuestMode = response.auth_mode === 'disabled'
|
||||||
|
login(response.access_token, isGuestMode)
|
||||||
|
|
||||||
|
if (isGuestMode) {
|
||||||
|
// Show authentication disabled notification
|
||||||
|
toast.info(response.message || t('login.authDisabled', 'Authentication is disabled. Using guest access.'))
|
||||||
|
} else {
|
||||||
toast.success(t('login.successMessage'))
|
toast.success(t('login.successMessage'))
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate('/')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login failed...', error)
|
console.error('Login failed...', error)
|
||||||
toast.error(t('login.errorInvalidCredentials'))
|
toast.error(t('login.errorInvalidCredentials'))
|
||||||
|
@@ -57,7 +57,7 @@ function TabsNavigation() {
|
|||||||
export default function SiteHeader() {
|
export default function SiteHeader() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { logout } = useAuthStore()
|
const { logout, isGuestMode } = useAuthStore()
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
logout()
|
logout()
|
||||||
@@ -74,6 +74,11 @@ export default function SiteHeader() {
|
|||||||
|
|
||||||
<div className="flex h-10 flex-1 justify-center">
|
<div className="flex h-10 flex-1 justify-center">
|
||||||
<TabsNavigation />
|
<TabsNavigation />
|
||||||
|
{isGuestMode && (
|
||||||
|
<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">
|
||||||
|
{t('login.guestMode', 'Guest Mode')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex items-center">
|
<nav className="flex items-center">
|
||||||
|
@@ -28,7 +28,9 @@
|
|||||||
"loggingIn": "Logging in...",
|
"loggingIn": "Logging in...",
|
||||||
"successMessage": "Login succeeded",
|
"successMessage": "Login succeeded",
|
||||||
"errorEmptyFields": "Please enter your username and password",
|
"errorEmptyFields": "Please enter your username and password",
|
||||||
"errorInvalidCredentials": "Login failed, please check username and password"
|
"errorInvalidCredentials": "Login failed, please check username and password",
|
||||||
|
"authDisabled": "Authentication is disabled. Using guest access.",
|
||||||
|
"guestMode": "Guest Mode"
|
||||||
},
|
},
|
||||||
"documentPanel": {
|
"documentPanel": {
|
||||||
"clearDocuments": {
|
"clearDocuments": {
|
||||||
|
@@ -28,7 +28,9 @@
|
|||||||
"loggingIn": "登录中...",
|
"loggingIn": "登录中...",
|
||||||
"successMessage": "登录成功",
|
"successMessage": "登录成功",
|
||||||
"errorEmptyFields": "请输入您的用户名和密码",
|
"errorEmptyFields": "请输入您的用户名和密码",
|
||||||
"errorInvalidCredentials": "登录失败,请检查用户名和密码"
|
"errorInvalidCredentials": "登录失败,请检查用户名和密码",
|
||||||
|
"authDisabled": "认证已禁用,使用访客访问模式。",
|
||||||
|
"guestMode": "访客模式"
|
||||||
},
|
},
|
||||||
"documentPanel": {
|
"documentPanel": {
|
||||||
"clearDocuments": {
|
"clearDocuments": {
|
||||||
|
@@ -19,7 +19,8 @@ interface BackendState {
|
|||||||
interface AuthState {
|
interface AuthState {
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
showLoginModal: boolean;
|
showLoginModal: boolean;
|
||||||
login: (token: string) => void;
|
isGuestMode: boolean; // Add guest mode flag
|
||||||
|
login: (token: string, isGuest?: boolean) => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
setShowLoginModal: (show: boolean) => void;
|
setShowLoginModal: (show: boolean) => void;
|
||||||
}
|
}
|
||||||
@@ -66,16 +67,63 @@ const useBackendState = createSelectors(useBackendStateStoreBase)
|
|||||||
|
|
||||||
export { useBackendState }
|
export { useBackendState }
|
||||||
|
|
||||||
export const useAuthStore = create<AuthState>(set => ({
|
// Helper function to check if token is a guest token
|
||||||
isAuthenticated: !!localStorage.getItem('LIGHTRAG-API-TOKEN'),
|
const isGuestToken = (token: string): boolean => {
|
||||||
|
try {
|
||||||
|
// JWT tokens are in the format: header.payload.signature
|
||||||
|
const parts = token.split('.');
|
||||||
|
if (parts.length !== 3) return false;
|
||||||
|
|
||||||
|
// Decode the payload (second part)
|
||||||
|
const payload = JSON.parse(atob(parts[1]));
|
||||||
|
|
||||||
|
// Check if the token has a role field with value "guest"
|
||||||
|
return payload.role === 'guest';
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing token:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize auth state from localStorage
|
||||||
|
const initAuthState = (): { isAuthenticated: boolean; isGuestMode: boolean } => {
|
||||||
|
const token = localStorage.getItem('LIGHTRAG-API-TOKEN');
|
||||||
|
if (!token) {
|
||||||
|
return { isAuthenticated: false, isGuestMode: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAuthenticated: true,
|
||||||
|
isGuestMode: isGuestToken(token)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthState>(set => {
|
||||||
|
// Get initial state from localStorage
|
||||||
|
const initialState = initAuthState();
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAuthenticated: initialState.isAuthenticated,
|
||||||
showLoginModal: false,
|
showLoginModal: false,
|
||||||
login: (token) => {
|
isGuestMode: initialState.isGuestMode,
|
||||||
|
|
||||||
|
login: (token, isGuest = false) => {
|
||||||
localStorage.setItem('LIGHTRAG-API-TOKEN', token);
|
localStorage.setItem('LIGHTRAG-API-TOKEN', token);
|
||||||
set({ isAuthenticated: true, showLoginModal: false });
|
set({
|
||||||
|
isAuthenticated: true,
|
||||||
|
showLoginModal: false,
|
||||||
|
isGuestMode: isGuest
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
logout: () => {
|
logout: () => {
|
||||||
localStorage.removeItem('LIGHTRAG-API-TOKEN');
|
localStorage.removeItem('LIGHTRAG-API-TOKEN');
|
||||||
set({ isAuthenticated: false });
|
set({
|
||||||
|
isAuthenticated: false,
|
||||||
|
isGuestMode: false
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setShowLoginModal: (show) => set({ showLoginModal: show })
|
setShowLoginModal: (show) => set({ showLoginModal: show })
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
|
Reference in New Issue
Block a user