Merge pull request #1170 from lcjqyml/feat-multi-user

Feat multi user support.
This commit is contained in:
Daniel.y
2025-03-25 17:48:20 +08:00
committed by GitHub
10 changed files with 216 additions and 192 deletions

View File

@@ -157,11 +157,10 @@ QDRANT_URL=http://localhost:16333
### Redis ### Redis
REDIS_URI=redis://localhost:6379 REDIS_URI=redis://localhost:6379
### For JWTt Auth ### For JWT Auth
AUTH_USERNAME=admin # login name AUTH_ACCOUNTS='admin:admin123,user1:pass456' # username:password,username:password
AUTH_PASSWORD=admin123 # password TOKEN_SECRET=Your-Key-For-LightRAG-API-Server # JWT key
TOKEN_SECRET=your-key-for-LightRAG-API-Server # JWT key TOKEN_EXPIRE_HOURS=4 # expire duration
TOKEN_EXPIRE_HOURS=4 # expire duration
### API-Key to access LightRAG Server API ### API-Key to access LightRAG Server API
# LIGHTRAG_API_KEY=your-secure-api-key-here # LIGHTRAG_API_KEY=your-secure-api-key-here

View File

@@ -20,9 +20,14 @@ 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( self.guest_expire_hours = int(os.getenv("GUEST_TOKEN_EXPIRE_HOURS", 2))
os.getenv("GUEST_TOKEN_EXPIRE_HOURS", 2)
) # Guest token default expiration time self.accounts = {}
auth_accounts = os.getenv("AUTH_ACCOUNTS")
if auth_accounts:
for account in auth_accounts.split(","):
username, password = account.split(":", 1)
self.accounts[username] = password
def create_token( def create_token(
self, self,

View File

@@ -362,10 +362,8 @@ def create_app(args):
@app.get("/auth-status") @app.get("/auth-status")
async def get_auth_status(): async def get_auth_status():
"""Get authentication status and guest token if auth is not configured""" """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): if not auth_handler.accounts:
# Authentication not configured, return guest token # Authentication not configured, return guest token
guest_token = auth_handler.create_token( guest_token = auth_handler.create_token(
username="guest", role="guest", metadata={"auth_mode": "disabled"} username="guest", role="guest", metadata={"auth_mode": "disabled"}
@@ -389,10 +387,7 @@ def create_app(args):
@app.post("/login") @app.post("/login")
async def login(form_data: OAuth2PasswordRequestForm = Depends()): async def login(form_data: OAuth2PasswordRequestForm = Depends()):
username = os.getenv("AUTH_USERNAME") if not auth_handler.accounts:
password = os.getenv("AUTH_PASSWORD")
if not (username and password):
# Authentication not configured, return guest token # Authentication not configured, return guest token
guest_token = auth_handler.create_token( guest_token = auth_handler.create_token(
username="guest", role="guest", metadata={"auth_mode": "disabled"} username="guest", role="guest", metadata={"auth_mode": "disabled"}
@@ -405,8 +400,8 @@ def create_app(args):
"core_version": core_version, "core_version": core_version,
"api_version": __api_version__, "api_version": __api_version__,
} }
username = form_data.username
if form_data.username != username or form_data.password != password: if auth_handler.accounts.get(username) != form_data.password:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect credentials" status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect credentials"
) )

View File

@@ -38,9 +38,7 @@ for path in whitelist_paths:
whitelist_patterns.append((path, False)) # (exact_path, is_prefix_match) whitelist_patterns.append((path, False)) # (exact_path, is_prefix_match)
# Global authentication configuration # Global authentication configuration
auth_username = os.getenv("AUTH_USERNAME") auth_configured = bool(auth_handler.accounts)
auth_password = os.getenv("AUTH_PASSWORD")
auth_configured = bool(auth_username and auth_password)
class OllamaServerInfos: class OllamaServerInfos:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,17 +1,17 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" /> <meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" /> <meta http-equiv="Expires" content="0" />
<link rel="icon" type="image/svg+xml" href="logo.png" /> <link rel="icon" type="image/svg+xml" href="logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag</title> <title>Lightrag</title>
<script type="module" crossorigin src="/webui/assets/index-DUmKHl1m.js"></script> <script type="module" crossorigin src="/webui/assets/index-DJ53id6i.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-CJhG62dt.css"> <link rel="stylesheet" crossorigin href="/webui/assets/index-BwFyYQzx.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
</body>
</html> </body>

View File

@@ -55,7 +55,7 @@ function TabsNavigation() {
export default function SiteHeader() { export default function SiteHeader() {
const { t } = useTranslation() const { t } = useTranslation()
const { isGuestMode, coreVersion, apiVersion } = useAuthStore() const { isGuestMode, coreVersion, apiVersion, username } = useAuthStore()
const versionDisplay = (coreVersion && apiVersion) const versionDisplay = (coreVersion && apiVersion)
? `${coreVersion}/${apiVersion}` ? `${coreVersion}/${apiVersion}`
@@ -98,7 +98,13 @@ export default function SiteHeader() {
</Button> </Button>
<AppSettings /> <AppSettings />
{!isGuestMode && ( {!isGuestMode && (
<Button variant="ghost" size="icon" side="bottom" tooltip={t('header.logout')} onClick={handleLogout}> <Button
variant="ghost"
size="icon"
side="bottom"
tooltip={`${t('header.logout')} (${username})`}
onClick={handleLogout}
>
<LogOutIcon className="size-4" aria-hidden="true" /> <LogOutIcon className="size-4" aria-hidden="true" />
</Button> </Button>
)} )}

View File

@@ -21,6 +21,8 @@ interface AuthState {
isGuestMode: boolean; // Add guest mode flag isGuestMode: boolean; // Add guest mode flag
coreVersion: string | null; coreVersion: string | null;
apiVersion: string | null; apiVersion: string | null;
username: string | null; // login username
login: (token: string, isGuest?: boolean, coreVersion?: string | null, apiVersion?: string | null) => void; login: (token: string, isGuest?: boolean, coreVersion?: string | null, apiVersion?: string | null) => void;
logout: () => void; logout: () => void;
setVersion: (coreVersion: string | null, apiVersion: string | null) => void; setVersion: (coreVersion: string | null, apiVersion: string | null) => void;
@@ -76,36 +78,42 @@ const useBackendState = createSelectors(useBackendStateStoreBase)
export { useBackendState } export { useBackendState }
// Helper function to check if token is a guest token const parseTokenPayload = (token: string): { sub?: string; role?: string } => {
const isGuestToken = (token: string): boolean => {
try { try {
// JWT tokens are in the format: header.payload.signature // JWT tokens are in the format: header.payload.signature
const parts = token.split('.'); const parts = token.split('.');
if (parts.length !== 3) return false; if (parts.length !== 3) return {};
// Decode the payload (second part)
const payload = JSON.parse(atob(parts[1])); const payload = JSON.parse(atob(parts[1]));
return payload;
// Check if the token has a role field with value "guest"
return payload.role === 'guest';
} catch (e) { } catch (e) {
console.error('Error parsing token:', e); console.error('Error parsing token payload:', e);
return false; return {};
} }
}; };
// Initialize auth state from localStorage const getUsernameFromToken = (token: string): string | null => {
const initAuthState = (): { isAuthenticated: boolean; isGuestMode: boolean; coreVersion: string | null; apiVersion: string | null } => { const payload = parseTokenPayload(token);
return payload.sub || null;
};
const isGuestToken = (token: string): boolean => {
const payload = parseTokenPayload(token);
return payload.role === 'guest';
};
const initAuthState = (): { isAuthenticated: boolean; isGuestMode: boolean; coreVersion: string | null; apiVersion: string | null; username: string | null } => {
const token = localStorage.getItem('LIGHTRAG-API-TOKEN'); const token = localStorage.getItem('LIGHTRAG-API-TOKEN');
const coreVersion = localStorage.getItem('LIGHTRAG-CORE-VERSION'); const coreVersion = localStorage.getItem('LIGHTRAG-CORE-VERSION');
const apiVersion = localStorage.getItem('LIGHTRAG-API-VERSION'); const apiVersion = localStorage.getItem('LIGHTRAG-API-VERSION');
const username = token ? getUsernameFromToken(token) : null;
if (!token) { if (!token) {
return { return {
isAuthenticated: false, isAuthenticated: false,
isGuestMode: false, isGuestMode: false,
coreVersion: coreVersion, coreVersion: coreVersion,
apiVersion: apiVersion apiVersion: apiVersion,
username: null,
}; };
} }
@@ -113,7 +121,8 @@ const initAuthState = (): { isAuthenticated: boolean; isGuestMode: boolean; core
isAuthenticated: true, isAuthenticated: true,
isGuestMode: isGuestToken(token), isGuestMode: isGuestToken(token),
coreVersion: coreVersion, coreVersion: coreVersion,
apiVersion: apiVersion apiVersion: apiVersion,
username: username,
}; };
}; };
@@ -126,6 +135,7 @@ export const useAuthStore = create<AuthState>(set => {
isGuestMode: initialState.isGuestMode, isGuestMode: initialState.isGuestMode,
coreVersion: initialState.coreVersion, coreVersion: initialState.coreVersion,
apiVersion: initialState.apiVersion, apiVersion: initialState.apiVersion,
username: initialState.username,
login: (token, isGuest = false, coreVersion = null, apiVersion = null) => { login: (token, isGuest = false, coreVersion = null, apiVersion = null) => {
localStorage.setItem('LIGHTRAG-API-TOKEN', token); localStorage.setItem('LIGHTRAG-API-TOKEN', token);
@@ -137,11 +147,13 @@ export const useAuthStore = create<AuthState>(set => {
localStorage.setItem('LIGHTRAG-API-VERSION', apiVersion); localStorage.setItem('LIGHTRAG-API-VERSION', apiVersion);
} }
const username = getUsernameFromToken(token);
set({ set({
isAuthenticated: true, isAuthenticated: true,
isGuestMode: isGuest, isGuestMode: isGuest,
username: username,
coreVersion: coreVersion, coreVersion: coreVersion,
apiVersion: apiVersion apiVersion: apiVersion,
}); });
}, },
@@ -154,8 +166,9 @@ export const useAuthStore = create<AuthState>(set => {
set({ set({
isAuthenticated: false, isAuthenticated: false,
isGuestMode: false, isGuestMode: false,
username: null,
coreVersion: coreVersion, coreVersion: coreVersion,
apiVersion: apiVersion apiVersion: apiVersion,
}); });
}, },