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 @@