From 6b22e8065bdf3dafb1b1765e765f3b51131ee6e7 Mon Sep 17 00:00:00 2001 From: choizhang Date: Tue, 11 Mar 2025 14:48:19 +0800 Subject: [PATCH] Added loginPage --- lightrag/api/lightrag_server.py | 2 +- lightrag_webui/bun.lock | 13 +++ lightrag_webui/package.json | 1 + lightrag_webui/src/App.tsx | 2 - lightrag_webui/src/AppRouter.tsx | 40 +++++++++ lightrag_webui/src/api/lightrag.ts | 39 ++++++++ lightrag_webui/src/features/LoginPage.tsx | 100 +++++++++++++++++++++ lightrag_webui/src/features/SiteHeader.tsx | 23 ++++- lightrag_webui/src/main.tsx | 4 +- lightrag_webui/src/stores/state.ts | 22 +++++ 10 files changed, 239 insertions(+), 7 deletions(-) create mode 100644 lightrag_webui/src/AppRouter.tsx create mode 100644 lightrag_webui/src/features/LoginPage.tsx diff --git a/lightrag/api/lightrag_server.py b/lightrag/api/lightrag_server.py index fd09a691..5d223759 100644 --- a/lightrag/api/lightrag_server.py +++ b/lightrag/api/lightrag_server.py @@ -373,7 +373,7 @@ def create_app(args): ollama_api = OllamaAPI(rag, top_k=args.top_k) app.include_router(ollama_api.router, prefix="/api") - @app.post("/login") + @app.post("/login", dependencies=[Depends(optional_api_key)]) async def login(form_data: OAuth2PasswordRequestForm = Depends()): username = os.getenv("AUTH_USERNAME") password = os.getenv("AUTH_PASSWORD") diff --git a/lightrag_webui/bun.lock b/lightrag_webui/bun.lock index 6157e38c..7435e125 100644 --- a/lightrag_webui/bun.lock +++ b/lightrag_webui/bun.lock @@ -41,6 +41,7 @@ "react-dropzone": "^14.3.6", "react-markdown": "^9.1.0", "react-number-format": "^5.4.3", + "react-router-dom": "^7.3.0", "react-syntax-highlighter": "^15.6.1", "rehype-react": "^8.0.0", "remark-gfm": "^4.0.1", @@ -415,6 +416,8 @@ "@types/bun": ["@types/bun@1.2.3", "", { "dependencies": { "bun-types": "1.2.3" } }, "sha512-054h79ipETRfjtsCW9qJK8Ipof67Pw9bodFWmkfkaUaRiIQ1dIV2VTlheshlBx3mpKr0KeK8VqnMMCtgN9rQtw=="], + "@types/cookie": ["@types/cookie@0.6.0", "https://registry.npmmirror.com/@types/cookie/-/cookie-0.6.0.tgz", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], @@ -561,6 +564,8 @@ "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + "cookie": ["cookie@1.0.2", "https://registry.npmmirror.com/cookie/-/cookie-1.0.2.tgz", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + "cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -1103,6 +1108,10 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + "react-router": ["react-router@7.3.0", "https://registry.npmmirror.com/react-router/-/react-router-7.3.0.tgz", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-466f2W7HIWaNXTKM5nHTqNxLrHTyXybm7R0eBlVSt0k/u55tTCDO194OIx/NrYD4TS5SXKTNekXfT37kMKUjgw=="], + + "react-router-dom": ["react-router-dom@7.3.0", "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.3.0.tgz", { "dependencies": { "react-router": "7.3.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-z7Q5FTiHGgQfEurX/FBinkOXhWREJIAB2RiU24lvcBa82PxUpwqvs/PAXb9lJyPjTs2jrl6UkLvCZVGJPeNuuQ=="], + "react-select": ["react-select@5.10.0", "", { "dependencies": { "@babel/runtime": "^7.12.0", "@emotion/cache": "^11.4.0", "@emotion/react": "^11.8.1", "@floating-ui/dom": "^1.0.1", "@types/react-transition-group": "^4.4.0", "memoize-one": "^6.0.0", "prop-types": "^15.6.0", "react-transition-group": "^4.3.0", "use-isomorphic-layout-effect": "^1.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-k96gw+i6N3ExgDwPIg0lUPmexl1ygPe6u5BdQFNBhkpbwroIgCNXdubtIzHfThYXYYTubwOBafoMnn7ruEP1xA=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], @@ -1153,6 +1162,8 @@ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "set-cookie-parser": ["set-cookie-parser@2.7.1", "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], @@ -1223,6 +1234,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "turbo-stream": ["turbo-stream@2.4.0", "https://registry.npmmirror.com/turbo-stream/-/turbo-stream-2.4.0.tgz", {}, "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], diff --git a/lightrag_webui/package.json b/lightrag_webui/package.json index 578ee36f..9b85f01f 100644 --- a/lightrag_webui/package.json +++ b/lightrag_webui/package.json @@ -50,6 +50,7 @@ "react-dropzone": "^14.3.6", "react-markdown": "^9.1.0", "react-number-format": "^5.4.3", + "react-router-dom": "^7.3.0", "react-syntax-highlighter": "^15.6.1", "rehype-react": "^8.0.0", "remark-gfm": "^4.0.1", diff --git a/lightrag_webui/src/App.tsx b/lightrag_webui/src/App.tsx index 1cf8c5e3..0a2ec50f 100644 --- a/lightrag_webui/src/App.tsx +++ b/lightrag_webui/src/App.tsx @@ -7,7 +7,6 @@ import { healthCheckInterval } from '@/lib/constants' import { useBackendState } from '@/stores/state' import { useSettingsStore } from '@/stores/settings' import { useEffect } from 'react' -import { Toaster } from 'sonner' import SiteHeader from '@/features/SiteHeader' import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag' @@ -79,7 +78,6 @@ function App() { {enableHealthCheck && } {message !== null && !apiKeyInvalid && } {apiKeyInvalid && } - ) diff --git a/lightrag_webui/src/AppRouter.tsx b/lightrag_webui/src/AppRouter.tsx new file mode 100644 index 00000000..a28b57c9 --- /dev/null +++ b/lightrag_webui/src/AppRouter.tsx @@ -0,0 +1,40 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +// import { useAuthStore } from '@/stores/state' +import { Toaster } from 'sonner' +import App from './App' +import LoginPage from '@/features/LoginPage' + +interface ProtectedRouteProps { + children: React.ReactNode +} + +const ProtectedRoute = ({ children }: ProtectedRouteProps) => { + // const { isAuthenticated } = useAuthStore() + + // if (!isAuthenticated) { + // return + // } + + return <>{children} +} + +const AppRouter = () => { + return ( + + + } /> + + + + } + /> + + + + ) +} + +export default AppRouter diff --git a/lightrag_webui/src/api/lightrag.ts b/lightrag_webui/src/api/lightrag.ts index cba9c964..a4cd86fb 100644 --- a/lightrag_webui/src/api/lightrag.ts +++ b/lightrag_webui/src/api/lightrag.ts @@ -2,6 +2,7 @@ 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' // Types export type LightragNodeType = { @@ -125,6 +126,11 @@ export type DocsStatusesResponse = { statuses: Record } +export type LoginResponse = { + access_token: string + token_type: string +} + export const InvalidApiKeyError = 'Invalid API Key' export const RequireApiKeError = 'API Key required' @@ -139,9 +145,13 @@ const axiosInstance = axios.create({ // Interceptor:add api key axiosInstance.interceptors.request.use((config) => { const apiKey = useSettingsStore.getState().apiKey + const token = localStorage.getItem('LIGHTRAG-API-TOKEN'); if (apiKey) { config.headers['X-API-Key'] = apiKey } + if (token) { + config.headers['Authorization'] = `Bearer ${token}` + } return config }) @@ -150,6 +160,21 @@ axiosInstance.interceptors.response.use( (response) => response, (error: AxiosError) => { if (error.response) { + interface ErrorResponse { + detail: string; + } + + if (error.response?.status === 401) { + localStorage.removeItem('LIGHTRAG-API-TOKEN'); + sessionStorage.clear(); + useAuthStore.getState().logout(); + + if (window.location.pathname !== '/login') { + window.location.href = '/login'; + } + + return Promise.reject(error); + } throw new Error( `${error.response.status} ${error.response.statusText}\n${JSON.stringify( error.response.data @@ -324,3 +349,17 @@ export const clearDocuments = async (): Promise => { const response = await axiosInstance.delete('/documents') return response.data } + +export const loginToServer = async (username: string, password: string): Promise => { + const formData = new FormData(); + formData.append('username', username); + formData.append('password', password); + + const response = await axiosInstance.post('/login', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }); + + return response.data; +} diff --git a/lightrag_webui/src/features/LoginPage.tsx b/lightrag_webui/src/features/LoginPage.tsx new file mode 100644 index 00000000..ad0e6227 --- /dev/null +++ b/lightrag_webui/src/features/LoginPage.tsx @@ -0,0 +1,100 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { useAuthStore } from '@/stores/state' +import { loginToServer } from '@/api/lightrag' +import { toast } from 'sonner' + +import { Card, CardContent, CardHeader } from '@/components/ui/Card' +import Input from '@/components/ui/Input' +import Button from '@/components/ui/Button' +import { ZapIcon } from 'lucide-react' + +const LoginPage = () => { + const navigate = useNavigate() + const { login } = useAuthStore() + const [loading, setLoading] = useState(false) + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!username || !password) { + toast.error('Please enter your username and password') + return + } + + try { + setLoading(true) + const response = await loginToServer(username, password) + login(response.access_token) + navigate('/') + toast.success('Login succeeded') + } catch (error) { + console.error('Login failed...', error) + toast.error('Login failed, please check username and password') + } finally { + setLoading(false) + } + } + + return ( +
+ + +
+
+ LightRAG Logo +
+
+

LightRAG

+

+ Please enter your account and password to log in to the system +

+
+
+
+ +
+
+ + setUsername(e.target.value)} + required + className="h-11 flex-1" + /> +
+
+ + setPassword(e.target.value)} + required + className="h-11 flex-1" + /> +
+ +
+
+
+
+ ) +} + +export default LoginPage diff --git a/lightrag_webui/src/features/SiteHeader.tsx b/lightrag_webui/src/features/SiteHeader.tsx index c09ce089..b92e260e 100644 --- a/lightrag_webui/src/features/SiteHeader.tsx +++ b/lightrag_webui/src/features/SiteHeader.tsx @@ -3,9 +3,11 @@ import { SiteInfo } from '@/lib/constants' import ThemeToggle from '@/components/ThemeToggle' import { TabsList, TabsTrigger } from '@/components/ui/Tabs' import { useSettingsStore } from '@/stores/settings' +import { useAuthStore } from '@/stores/state' import { cn } from '@/lib/utils' +import { useNavigate } from 'react-router-dom' -import { ZapIcon, GithubIcon } from 'lucide-react' +import { ZapIcon, GithubIcon, LogOutIcon } from 'lucide-react' interface NavigationTabProps { value: string @@ -51,6 +53,14 @@ function TabsNavigation() { } export default function SiteHeader() { + const navigate = useNavigate() + const { logout } = useAuthStore() + + const handleLogout = () => { + logout() + navigate('/login') + } + return (
@@ -63,13 +73,22 @@ export default function SiteHeader() { -
) diff --git a/lightrag_webui/src/main.tsx b/lightrag_webui/src/main.tsx index 2caec890..215e0118 100644 --- a/lightrag_webui/src/main.tsx +++ b/lightrag_webui/src/main.tsx @@ -1,10 +1,10 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' -import App from './App.tsx' +import AppRouter from './AppRouter' createRoot(document.getElementById('root')!).render( - + ) diff --git a/lightrag_webui/src/stores/state.ts b/lightrag_webui/src/stores/state.ts index 0e104e6d..0ccf4297 100644 --- a/lightrag_webui/src/stores/state.ts +++ b/lightrag_webui/src/stores/state.ts @@ -16,6 +16,14 @@ interface BackendState { setErrorMessage: (message: string, messageTitle: string) => void } +interface AuthState { + isAuthenticated: boolean; + showLoginModal: boolean; + login: (token: string) => void; + logout: () => void; + setShowLoginModal: (show: boolean) => void; +} + const useBackendStateStoreBase = create()((set) => ({ health: true, message: null, @@ -57,3 +65,17 @@ const useBackendStateStoreBase = create()((set) => ({ const useBackendState = createSelectors(useBackendStateStoreBase) export { useBackendState } + +export const useAuthStore = create(set => ({ + isAuthenticated: !!localStorage.getItem('LIGHTRAG-API-TOKEN'), + showLoginModal: false, + login: (token) => { + localStorage.setItem('LIGHTRAG-API-TOKEN', token); + set({ isAuthenticated: true, showLoginModal: false }); + }, + logout: () => { + localStorage.removeItem('LIGHTRAG-API-TOKEN'); + set({ isAuthenticated: false }); + }, + setShowLoginModal: (show) => set({ showLoginModal: show }) +}));