From 7ab2ad41c463c654ff5c7a9f99c0038fdc843785 Mon Sep 17 00:00:00 2001 From: "Gan, Jimmy" Date: Sun, 1 Mar 2026 18:17:45 +0800 Subject: [PATCH] feat: add role-based sidebar link visibility control --- ...(\\\"state\\\",d.get(\\\"message\\\")))\"" | 0 dashboard/backend/auth.py | 10 +++-- dashboard/backend/rbac.py | 15 +++++-- dashboard/backend/routers/auth.py | 9 +++- dashboard/frontend/src/App.svelte | 4 +- .../frontend/src/components/Sidebar.svelte | 11 ++++- dashboard/frontend/src/routes/Settings.svelte | 42 ++++++++++++++++--- 7 files changed, 74 insertions(+), 17 deletions(-) create mode 100644 " typo, consolidate user management, compact bottom bar\\\",\\\"head\\\":\\\"dev\\\",\\\"base\\\":\\\"main\\\"}\" | python3 -c \"import sys,json; d=json.load(sys.stdin); print(\\\"PR #\\\" + str(d.get(\\\"number\\\",\\\"?\\\")) + \\\":\\\", d.get(\\\"state\\\",d.get(\\\"message\\\")))\"" diff --git "a/ typo, consolidate user management, compact bottom bar\\\",\\\"head\\\":\\\"dev\\\",\\\"base\\\":\\\"main\\\"}\" | python3 -c \"import sys,json; d=json.load(sys.stdin); print(\\\"PR #\\\" + str(d.get(\\\"number\\\",\\\"?\\\")) + \\\":\\\", d.get(\\\"state\\\",d.get(\\\"message\\\")))\"" "b/ typo, consolidate user management, compact bottom bar\\\",\\\"head\\\":\\\"dev\\\",\\\"base\\\":\\\"main\\\"}\" | python3 -c \"import sys,json; d=json.load(sys.stdin); print(\\\"PR #\\\" + str(d.get(\\\"number\\\",\\\"?\\\")) + \\\":\\\", d.get(\\\"state\\\",d.get(\\\"message\\\")))\"" new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/backend/auth.py b/dashboard/backend/auth.py index 359a5c1..7d0f3cf 100644 --- a/dashboard/backend/auth.py +++ b/dashboard/backend/auth.py @@ -31,7 +31,7 @@ def create_refresh_token(data: dict): return jwt.encode(to_encode, config.SECRET_KEY, algorithm=config.ALGORITHM) async def get_current_user(token: str = Depends(oauth2_scheme)): - from rbac import User, get_pages + from rbac import User, get_pages, get_sidebar_links credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", @@ -48,10 +48,11 @@ async def get_current_user(token: str = Depends(oauth2_scheme)): except jwt.PyJWTError: raise credentials_exception pages = get_pages(username, role) - return User(username=username, role=role, pages=pages) + sidebar_links = get_sidebar_links(username, role) + return User(username=username, role=role, pages=pages, sidebar_links=sidebar_links) async def get_current_user_ws(token: str): - from rbac import User, get_pages + from rbac import User, get_pages, get_sidebar_links credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", @@ -67,7 +68,8 @@ async def get_current_user_ws(token: str): except jwt.PyJWTError: raise credentials_exception pages = get_pages(username, role) - return User(username=username, role=role, pages=pages) + sidebar_links = get_sidebar_links(username, role) + return User(username=username, role=role, pages=pages, sidebar_links=sidebar_links) # TOTP Persistence AUTH_FILE = config.VOLUME_ROOT + "/docker/nas-dashboard/auth.json" diff --git a/dashboard/backend/rbac.py b/dashboard/backend/rbac.py index a385ee7..b4d60f5 100644 --- a/dashboard/backend/rbac.py +++ b/dashboard/backend/rbac.py @@ -16,9 +16,9 @@ ROLE_GROUP_MAP = { DEFAULT_RBAC = { "role_defaults": { - "admin": {"pages": "*"}, - "member": {"pages": ["dashboard", "docker", "files", "openclaw", "chat-digest"]}, - "viewer": {"pages": ["dashboard"]}, + "admin": {"pages": "*", "sidebar_links": "*"}, + "member": {"pages": ["dashboard", "docker", "files", "openclaw", "chat-digest"], "sidebar_links": "*"}, + "viewer": {"pages": ["dashboard"], "sidebar_links": ["dashboard"]}, }, "user_overrides": {}, } @@ -28,6 +28,7 @@ class User(BaseModel): username: str role: str pages: list | str # "*" or list of page ids + sidebar_links: list | str = "*" # "*" or list of sidebar link labels/ids def load_rbac() -> dict: @@ -61,6 +62,14 @@ def get_pages(username: str, role: str) -> list | str: return rbac.get("role_defaults", {}).get(role, {}).get("pages", []) +def get_sidebar_links(username: str, role: str) -> list | str: + rbac = load_rbac() + overrides = rbac.get("user_overrides", {}) + if username in overrides and "sidebar_links" in overrides[username]: + return overrides[username]["sidebar_links"] + return rbac.get("role_defaults", {}).get(role, {}).get("sidebar_links", "*") + + def get_sidebar_order(username: str) -> dict | None: rbac = load_rbac() overrides = rbac.get("user_overrides", {}) diff --git a/dashboard/backend/routers/auth.py b/dashboard/backend/routers/auth.py index 55aeec7..a08677c 100644 --- a/dashboard/backend/routers/auth.py +++ b/dashboard/backend/routers/auth.py @@ -156,6 +156,7 @@ async def read_users_me(current_user = Depends(auth.get_current_user)): "username": current_user.username, "role": current_user.role, "pages": current_user.pages, + "sidebar_links": current_user.sidebar_links, "has_2fa": bool(totp_secret), } @@ -411,11 +412,17 @@ async def rbac_update_role(role: str, request: Request, current_user = Depends(a from rbac import load_rbac, save_rbac body = await request.json() pages = body.get("pages") + sidebar_links = body.get("sidebar_links") if pages is None: raise HTTPException(status_code=400, detail="Missing pages field") if pages != "*" and not isinstance(pages, list): raise HTTPException(status_code=400, detail="pages must be '*' or a list") + if sidebar_links is not None and sidebar_links != "*" and not isinstance(sidebar_links, list): + raise HTTPException(status_code=400, detail="sidebar_links must be '*' or a list") rbac = load_rbac() - rbac.setdefault("role_defaults", {})[role] = {"pages": pages} + role_config = {"pages": pages} + if sidebar_links is not None: + role_config["sidebar_links"] = sidebar_links + rbac.setdefault("role_defaults", {})[role] = role_config save_rbac(rbac) return {"ok": True} diff --git a/dashboard/frontend/src/App.svelte b/dashboard/frontend/src/App.svelte index 7599134..4ac539d 100644 --- a/dashboard/frontend/src/App.svelte +++ b/dashboard/frontend/src/App.svelte @@ -20,6 +20,7 @@ let loading = $state(true); let userRole = $state(""); let allowedPages = $state([]); + let allowedSidebarLinks = $state("*"); let sidebarOrder = $state(null); let orderSaveTimer = null; @@ -29,6 +30,7 @@ setCurrentUser(data); userRole = data.role || "admin"; allowedPages = data.pages || "*"; + allowedSidebarLinks = data.sidebar_links || "*"; // Fetch sidebar preferences try { const prefs = await getPreferences(); @@ -126,7 +128,7 @@ {:else}
- +
diff --git a/dashboard/frontend/src/components/Sidebar.svelte b/dashboard/frontend/src/components/Sidebar.svelte index c033f15..b4dca40 100644 --- a/dashboard/frontend/src/components/Sidebar.svelte +++ b/dashboard/frontend/src/components/Sidebar.svelte @@ -1,5 +1,5 @@