choizhang commited on
Commit
11b4cc9
·
1 Parent(s): 3f238f2

Added loginPage

Browse files
lightrag/api/lightrag_server.py CHANGED
@@ -373,7 +373,7 @@ def create_app(args):
373
  ollama_api = OllamaAPI(rag, top_k=args.top_k)
374
  app.include_router(ollama_api.router, prefix="/api")
375
 
376
- @app.post("/login")
377
  async def login(form_data: OAuth2PasswordRequestForm = Depends()):
378
  username = os.getenv("AUTH_USERNAME")
379
  password = os.getenv("AUTH_PASSWORD")
 
373
  ollama_api = OllamaAPI(rag, top_k=args.top_k)
374
  app.include_router(ollama_api.router, prefix="/api")
375
 
376
+ @app.post("/login", dependencies=[Depends(optional_api_key)])
377
  async def login(form_data: OAuth2PasswordRequestForm = Depends()):
378
  username = os.getenv("AUTH_USERNAME")
379
  password = os.getenv("AUTH_PASSWORD")
lightrag_webui/bun.lock CHANGED
@@ -41,6 +41,7 @@
41
  "react-dropzone": "^14.3.6",
42
  "react-markdown": "^9.1.0",
43
  "react-number-format": "^5.4.3",
 
44
  "react-syntax-highlighter": "^15.6.1",
45
  "rehype-react": "^8.0.0",
46
  "remark-gfm": "^4.0.1",
@@ -415,6 +416,8 @@
415
 
416
  "@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.2.3" } }, "sha512-054h79ipETRfjtsCW9qJK8Ipof67Pw9bodFWmkfkaUaRiIQ1dIV2VTlheshlBx3mpKr0KeK8VqnMMCtgN9rQtw=="],
417
 
 
 
418
  "@types/debug": ["@types/[email protected]", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
419
 
420
  "@types/estree": ["@types/[email protected]", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
@@ -561,6 +564,8 @@
561
 
562
  "convert-source-map": ["[email protected]", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
563
 
 
 
564
  "cosmiconfig": ["[email protected]", "", { "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=="],
565
 
566
  "cross-spawn": ["[email protected]", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
@@ -1103,6 +1108,10 @@
1103
 
1104
  "react-remove-scroll-bar": ["[email protected]", "", { "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=="],
1105
 
 
 
 
 
1106
  "react-select": ["[email protected]", "", { "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=="],
1107
 
1108
  "react-style-singleton": ["[email protected]", "", { "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 @@
1153
 
1154
  "semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
1155
 
 
 
1156
  "set-function-length": ["[email protected]", "", { "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=="],
1157
 
1158
  "set-function-name": ["[email protected]", "", { "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 @@
1223
 
1224
  "tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1225
 
 
 
1226
  "type-check": ["[email protected]", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
1227
 
1228
  "typed-array-buffer": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
 
41
  "react-dropzone": "^14.3.6",
42
  "react-markdown": "^9.1.0",
43
  "react-number-format": "^5.4.3",
44
+ "react-router-dom": "^7.3.0",
45
  "react-syntax-highlighter": "^15.6.1",
46
  "rehype-react": "^8.0.0",
47
  "remark-gfm": "^4.0.1",
 
416
 
417
  "@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.2.3" } }, "sha512-054h79ipETRfjtsCW9qJK8Ipof67Pw9bodFWmkfkaUaRiIQ1dIV2VTlheshlBx3mpKr0KeK8VqnMMCtgN9rQtw=="],
418
 
419
+ "@types/cookie": ["@types/[email protected]", "https://registry.npmmirror.com/@types/cookie/-/cookie-0.6.0.tgz", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
420
+
421
  "@types/debug": ["@types/[email protected]", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
422
 
423
  "@types/estree": ["@types/[email protected]", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
 
564
 
565
  "convert-source-map": ["[email protected]", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
566
 
567
+ "cookie": ["[email protected]", "https://registry.npmmirror.com/cookie/-/cookie-1.0.2.tgz", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
568
+
569
  "cosmiconfig": ["[email protected]", "", { "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=="],
570
 
571
  "cross-spawn": ["[email protected]", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
 
1108
 
1109
  "react-remove-scroll-bar": ["[email protected]", "", { "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=="],
1110
 
1111
+ "react-router": ["[email protected]", "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=="],
1112
+
1113
+ "react-router-dom": ["[email protected]", "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=="],
1114
+
1115
  "react-select": ["[email protected]", "", { "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=="],
1116
 
1117
  "react-style-singleton": ["[email protected]", "", { "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=="],
 
1162
 
1163
  "semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
1164
 
1165
+ "set-cookie-parser": ["[email protected]", "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
1166
+
1167
  "set-function-length": ["[email protected]", "", { "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=="],
1168
 
1169
  "set-function-name": ["[email protected]", "", { "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=="],
 
1234
 
1235
  "tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1236
 
1237
+ "turbo-stream": ["[email protected]", "https://registry.npmmirror.com/turbo-stream/-/turbo-stream-2.4.0.tgz", {}, "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="],
1238
+
1239
  "type-check": ["[email protected]", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
1240
 
1241
  "typed-array-buffer": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
lightrag_webui/package.json CHANGED
@@ -50,6 +50,7 @@
50
  "react-dropzone": "^14.3.6",
51
  "react-markdown": "^9.1.0",
52
  "react-number-format": "^5.4.3",
 
53
  "react-syntax-highlighter": "^15.6.1",
54
  "rehype-react": "^8.0.0",
55
  "remark-gfm": "^4.0.1",
 
50
  "react-dropzone": "^14.3.6",
51
  "react-markdown": "^9.1.0",
52
  "react-number-format": "^5.4.3",
53
+ "react-router-dom": "^7.3.0",
54
  "react-syntax-highlighter": "^15.6.1",
55
  "rehype-react": "^8.0.0",
56
  "remark-gfm": "^4.0.1",
lightrag_webui/src/App.tsx CHANGED
@@ -7,7 +7,6 @@ import { healthCheckInterval } from '@/lib/constants'
7
  import { useBackendState } from '@/stores/state'
8
  import { useSettingsStore } from '@/stores/settings'
9
  import { useEffect } from 'react'
10
- import { Toaster } from 'sonner'
11
  import SiteHeader from '@/features/SiteHeader'
12
  import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
13
 
@@ -79,7 +78,6 @@ function App() {
79
  {enableHealthCheck && <StatusIndicator />}
80
  {message !== null && !apiKeyInvalid && <MessageAlert />}
81
  {apiKeyInvalid && <ApiKeyAlert />}
82
- <Toaster />
83
  </main>
84
  </ThemeProvider>
85
  )
 
7
  import { useBackendState } from '@/stores/state'
8
  import { useSettingsStore } from '@/stores/settings'
9
  import { useEffect } from 'react'
 
10
  import SiteHeader from '@/features/SiteHeader'
11
  import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
12
 
 
78
  {enableHealthCheck && <StatusIndicator />}
79
  {message !== null && !apiKeyInvalid && <MessageAlert />}
80
  {apiKeyInvalid && <ApiKeyAlert />}
 
81
  </main>
82
  </ThemeProvider>
83
  )
lightrag_webui/src/AppRouter.tsx ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
2
+ // import { useAuthStore } from '@/stores/state'
3
+ import { Toaster } from 'sonner'
4
+ import App from './App'
5
+ import LoginPage from '@/features/LoginPage'
6
+
7
+ interface ProtectedRouteProps {
8
+ children: React.ReactNode
9
+ }
10
+
11
+ const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
12
+ // const { isAuthenticated } = useAuthStore()
13
+
14
+ // if (!isAuthenticated) {
15
+ // return <Navigate to="/login" replace />
16
+ // }
17
+
18
+ return <>{children}</>
19
+ }
20
+
21
+ const AppRouter = () => {
22
+ return (
23
+ <BrowserRouter>
24
+ <Routes>
25
+ <Route path="/login" element={<LoginPage />} />
26
+ <Route
27
+ path="/*"
28
+ element={
29
+ <ProtectedRoute>
30
+ <App />
31
+ </ProtectedRoute>
32
+ }
33
+ />
34
+ </Routes>
35
+ <Toaster position="top-center" />
36
+ </BrowserRouter>
37
+ )
38
+ }
39
+
40
+ export default AppRouter
lightrag_webui/src/api/lightrag.ts CHANGED
@@ -2,6 +2,7 @@ import axios, { AxiosError } from 'axios'
2
  import { backendBaseUrl } from '@/lib/constants'
3
  import { errorMessage } from '@/lib/utils'
4
  import { useSettingsStore } from '@/stores/settings'
 
5
 
6
  // Types
7
  export type LightragNodeType = {
@@ -125,6 +126,11 @@ export type DocsStatusesResponse = {
125
  statuses: Record<DocStatus, DocStatusResponse[]>
126
  }
127
 
 
 
 
 
 
128
  export const InvalidApiKeyError = 'Invalid API Key'
129
  export const RequireApiKeError = 'API Key required'
130
 
@@ -139,9 +145,13 @@ const axiosInstance = axios.create({
139
  // Interceptor:add api key
140
  axiosInstance.interceptors.request.use((config) => {
141
  const apiKey = useSettingsStore.getState().apiKey
 
142
  if (apiKey) {
143
  config.headers['X-API-Key'] = apiKey
144
  }
 
 
 
145
  return config
146
  })
147
 
@@ -150,6 +160,21 @@ axiosInstance.interceptors.response.use(
150
  (response) => response,
151
  (error: AxiosError) => {
152
  if (error.response) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  throw new Error(
154
  `${error.response.status} ${error.response.statusText}\n${JSON.stringify(
155
  error.response.data
@@ -324,3 +349,17 @@ export const clearDocuments = async (): Promise<DocActionResponse> => {
324
  const response = await axiosInstance.delete('/documents')
325
  return response.data
326
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import { backendBaseUrl } from '@/lib/constants'
3
  import { errorMessage } from '@/lib/utils'
4
  import { useSettingsStore } from '@/stores/settings'
5
+ import { useAuthStore } from '@/stores/state'
6
 
7
  // Types
8
  export type LightragNodeType = {
 
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'
135
  export const RequireApiKeError = 'API Key required'
136
 
 
145
  // Interceptor:add api key
146
  axiosInstance.interceptors.request.use((config) => {
147
  const apiKey = useSettingsStore.getState().apiKey
148
+ const token = localStorage.getItem('LIGHTRAG-API-TOKEN');
149
  if (apiKey) {
150
  config.headers['X-API-Key'] = apiKey
151
  }
152
+ if (token) {
153
+ config.headers['Authorization'] = `Bearer ${token}`
154
+ }
155
  return config
156
  })
157
 
 
160
  (response) => response,
161
  (error: AxiosError) => {
162
  if (error.response) {
163
+ interface ErrorResponse {
164
+ detail: string;
165
+ }
166
+
167
+ if (error.response?.status === 401) {
168
+ localStorage.removeItem('LIGHTRAG-API-TOKEN');
169
+ sessionStorage.clear();
170
+ useAuthStore.getState().logout();
171
+
172
+ if (window.location.pathname !== '/login') {
173
+ window.location.href = '/login';
174
+ }
175
+
176
+ return Promise.reject(error);
177
+ }
178
  throw new Error(
179
  `${error.response.status} ${error.response.statusText}\n${JSON.stringify(
180
  error.response.data
 
349
  const response = await axiosInstance.delete('/documents')
350
  return response.data
351
  }
352
+
353
+ export const loginToServer = async (username: string, password: string): Promise<LoginResponse> => {
354
+ const formData = new FormData();
355
+ formData.append('username', username);
356
+ formData.append('password', password);
357
+
358
+ const response = await axiosInstance.post('/login', formData, {
359
+ headers: {
360
+ 'Content-Type': 'multipart/form-data'
361
+ }
362
+ });
363
+
364
+ return response.data;
365
+ }
lightrag_webui/src/features/LoginPage.tsx ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
7
+ import { Card, CardContent, CardHeader } from '@/components/ui/Card'
8
+ import Input from '@/components/ui/Input'
9
+ import Button from '@/components/ui/Button'
10
+ import { ZapIcon } from 'lucide-react'
11
+
12
+ const LoginPage = () => {
13
+ const navigate = useNavigate()
14
+ const { login } = useAuthStore()
15
+ const [loading, setLoading] = useState(false)
16
+ const [username, setUsername] = useState('')
17
+ const [password, setPassword] = useState('')
18
+
19
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
20
+ e.preventDefault()
21
+ if (!username || !password) {
22
+ toast.error('Please enter your username and password')
23
+ return
24
+ }
25
+
26
+ try {
27
+ setLoading(true)
28
+ const response = await loginToServer(username, password)
29
+ login(response.access_token)
30
+ navigate('/')
31
+ toast.success('Login succeeded')
32
+ } catch (error) {
33
+ console.error('Login failed...', error)
34
+ toast.error('Login failed, please check username and password')
35
+ } finally {
36
+ setLoading(false)
37
+ }
38
+ }
39
+
40
+ return (
41
+ <div className="flex h-screen w-screen items-center justify-center bg-gradient-to-br from-emerald-50 to-teal-100 dark:from-gray-900 dark:to-gray-800">
42
+ <Card className="w-full max-w-[480px] shadow-lg mx-4">
43
+ <CardHeader className="flex items-center justify-center space-y-2 pb-8 pt-6">
44
+ <div className="flex flex-col items-center space-y-4">
45
+ <div className="flex items-center gap-3">
46
+ <img src="/logo.png" alt="LightRAG Logo" className="h-12 w-12" />
47
+ <ZapIcon className="size-10 text-emerald-400" aria-hidden="true" />
48
+ </div>
49
+ <div className="text-center space-y-2">
50
+ <h1 className="text-3xl font-bold tracking-tight">LightRAG</h1>
51
+ <p className="text-muted-foreground text-sm">
52
+ Please enter your account and password to log in to the system
53
+ </p>
54
+ </div>
55
+ </div>
56
+ </CardHeader>
57
+ <CardContent className="px-8 pb-8">
58
+ <form onSubmit={handleSubmit} className="space-y-6">
59
+ <div className="flex items-center gap-4">
60
+ <label htmlFor="username" className="text-sm font-medium w-16 shrink-0">
61
+ username
62
+ </label>
63
+ <Input
64
+ id="username"
65
+ placeholder="Please input a username"
66
+ value={username}
67
+ onChange={(e) => setUsername(e.target.value)}
68
+ required
69
+ className="h-11 flex-1"
70
+ />
71
+ </div>
72
+ <div className="flex items-center gap-4">
73
+ <label htmlFor="password" className="text-sm font-medium w-16 shrink-0">
74
+ password
75
+ </label>
76
+ <Input
77
+ id="password"
78
+ type="password"
79
+ placeholder="Please input a password"
80
+ value={password}
81
+ onChange={(e) => setPassword(e.target.value)}
82
+ required
83
+ className="h-11 flex-1"
84
+ />
85
+ </div>
86
+ <Button
87
+ type="submit"
88
+ className="w-full h-11 text-base font-medium mt-2"
89
+ disabled={loading}
90
+ >
91
+ {loading ? 'Logging in...' : 'Login'}
92
+ </Button>
93
+ </form>
94
+ </CardContent>
95
+ </Card>
96
+ </div>
97
+ )
98
+ }
99
+
100
+ export default LoginPage
lightrag_webui/src/features/SiteHeader.tsx CHANGED
@@ -3,9 +3,11 @@ import { SiteInfo } from '@/lib/constants'
3
  import ThemeToggle from '@/components/ThemeToggle'
4
  import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
5
  import { useSettingsStore } from '@/stores/settings'
 
6
  import { cn } from '@/lib/utils'
 
7
 
8
- import { ZapIcon, GithubIcon } from 'lucide-react'
9
 
10
  interface NavigationTabProps {
11
  value: string
@@ -51,6 +53,14 @@ function TabsNavigation() {
51
  }
52
 
53
  export default function SiteHeader() {
 
 
 
 
 
 
 
 
54
  return (
55
  <header className="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 flex h-10 w-full border-b px-4 backdrop-blur">
56
  <a href="/" className="mr-6 flex items-center gap-2">
@@ -63,13 +73,22 @@ export default function SiteHeader() {
63
  <TabsNavigation />
64
  </div>
65
 
66
- <nav className="flex items-center">
67
  <Button variant="ghost" size="icon" side="bottom" tooltip="Project Repository">
68
  <a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
69
  <GithubIcon className="size-4" aria-hidden="true" />
70
  </a>
71
  </Button>
72
  <ThemeToggle />
 
 
 
 
 
 
 
 
 
73
  </nav>
74
  </header>
75
  )
 
3
  import ThemeToggle from '@/components/ThemeToggle'
4
  import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
5
  import { useSettingsStore } from '@/stores/settings'
6
+ import { useAuthStore } from '@/stores/state'
7
  import { cn } from '@/lib/utils'
8
+ import { useNavigate } from 'react-router-dom'
9
 
10
+ import { ZapIcon, GithubIcon, LogOutIcon } from 'lucide-react'
11
 
12
  interface NavigationTabProps {
13
  value: string
 
53
  }
54
 
55
  export default function SiteHeader() {
56
+ const navigate = useNavigate()
57
+ const { logout } = useAuthStore()
58
+
59
+ const handleLogout = () => {
60
+ logout()
61
+ navigate('/login')
62
+ }
63
+
64
  return (
65
  <header className="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 flex h-10 w-full border-b px-4 backdrop-blur">
66
  <a href="/" className="mr-6 flex items-center gap-2">
 
73
  <TabsNavigation />
74
  </div>
75
 
76
+ <nav className="flex items-center gap-2">
77
  <Button variant="ghost" size="icon" side="bottom" tooltip="Project Repository">
78
  <a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
79
  <GithubIcon className="size-4" aria-hidden="true" />
80
  </a>
81
  </Button>
82
  <ThemeToggle />
83
+ <Button
84
+ variant="ghost"
85
+ size="icon"
86
+ side="bottom"
87
+ tooltip="Log Out"
88
+ onClick={handleLogout}
89
+ >
90
+ <LogOutIcon className="size-4" aria-hidden="true" />
91
+ </Button>
92
  </nav>
93
  </header>
94
  )
lightrag_webui/src/main.tsx CHANGED
@@ -1,10 +1,10 @@
1
  import { StrictMode } from 'react'
2
  import { createRoot } from 'react-dom/client'
3
  import './index.css'
4
- import App from './App.tsx'
5
 
6
  createRoot(document.getElementById('root')!).render(
7
  <StrictMode>
8
- <App />
9
  </StrictMode>
10
  )
 
1
  import { StrictMode } from 'react'
2
  import { createRoot } from 'react-dom/client'
3
  import './index.css'
4
+ import AppRouter from './AppRouter'
5
 
6
  createRoot(document.getElementById('root')!).render(
7
  <StrictMode>
8
+ <AppRouter />
9
  </StrictMode>
10
  )
lightrag_webui/src/stores/state.ts CHANGED
@@ -16,6 +16,14 @@ interface BackendState {
16
  setErrorMessage: (message: string, messageTitle: string) => void
17
  }
18
 
 
 
 
 
 
 
 
 
19
  const useBackendStateStoreBase = create<BackendState>()((set) => ({
20
  health: true,
21
  message: null,
@@ -57,3 +65,17 @@ const useBackendStateStoreBase = create<BackendState>()((set) => ({
57
  const useBackendState = createSelectors(useBackendStateStoreBase)
58
 
59
  export { useBackendState }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  setErrorMessage: (message: string, messageTitle: string) => void
17
  }
18
 
19
+ interface AuthState {
20
+ isAuthenticated: boolean;
21
+ showLoginModal: boolean;
22
+ login: (token: string) => void;
23
+ logout: () => void;
24
+ setShowLoginModal: (show: boolean) => void;
25
+ }
26
+
27
  const useBackendStateStoreBase = create<BackendState>()((set) => ({
28
  health: true,
29
  message: null,
 
65
  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
+ }));