diff --git a/dashboard/backend/rbac.py b/dashboard/backend/rbac.py
index 20da62e..a385ee7 100644
--- a/dashboard/backend/rbac.py
+++ b/dashboard/backend/rbac.py
@@ -61,6 +61,18 @@ def get_pages(username: str, role: str) -> list | str:
return rbac.get("role_defaults", {}).get(role, {}).get("pages", [])
+def get_sidebar_order(username: str) -> dict | None:
+ rbac = load_rbac()
+ overrides = rbac.get("user_overrides", {})
+ return overrides.get(username, {}).get("sidebar_order")
+
+
+def save_sidebar_order(username: str, order: dict):
+ rbac = load_rbac()
+ rbac.setdefault("user_overrides", {}).setdefault(username, {})["sidebar_order"] = order
+ save_rbac(rbac)
+
+
def has_page_access(user: User, page: str) -> bool:
if user.pages == "*":
return True
diff --git a/dashboard/backend/routers/auth.py b/dashboard/backend/routers/auth.py
index 9246002..0e3775f 100644
--- a/dashboard/backend/routers/auth.py
+++ b/dashboard/backend/routers/auth.py
@@ -364,10 +364,28 @@ async def rbac_set_override(username: str, request: Request, current_user = Depe
if pages is None:
raise HTTPException(status_code=400, detail="Missing pages field")
rbac = load_rbac()
- rbac.setdefault("user_overrides", {})[username] = {"pages": pages}
+ existing = rbac.setdefault("user_overrides", {}).get(username, {})
+ existing["pages"] = pages
+ rbac["user_overrides"][username] = existing
save_rbac(rbac)
return {"ok": True}
+@router.get("/preferences")
+async def get_preferences(current_user = Depends(auth.get_current_user)):
+ from rbac import get_sidebar_order
+ order = get_sidebar_order(current_user.username)
+ return {"sidebar_order": order}
+
+@router.put("/preferences")
+async def save_preferences(request: Request, current_user = Depends(auth.get_current_user)):
+ from rbac import save_sidebar_order
+ body = await request.json()
+ order = body.get("sidebar_order")
+ if order is None or not isinstance(order, dict):
+ raise HTTPException(status_code=400, detail="Missing sidebar_order dict")
+ save_sidebar_order(current_user.username, order)
+ return {"ok": True}
+
@router.delete("/rbac/overrides/{username}")
async def rbac_delete_override(username: str, current_user = Depends(auth.get_current_user)):
if current_user.role != "admin":
diff --git a/dashboard/frontend/src/App.svelte b/dashboard/frontend/src/App.svelte
index eeb0f2b..7262c2e 100644
--- a/dashboard/frontend/src/App.svelte
+++ b/dashboard/frontend/src/App.svelte
@@ -11,7 +11,7 @@
import Security from "./routes/Security.svelte";
import Login from "./routes/Login.svelte";
import { onMount } from "svelte";
- import { getToken, checkAuth, setToken, setRefreshToken, currentUser, setCurrentUser } from "./lib/api.js";
+ import { getToken, checkAuth, setToken, setRefreshToken, currentUser, setCurrentUser, getPreferences, savePreferences } from "./lib/api.js";
let page = $state("dashboard");
let dark = $state(false);
@@ -20,6 +20,8 @@
let loading = $state(true);
let userRole = $state("");
let allowedPages = $state([]);
+ let sidebarOrder = $state(null);
+ let orderSaveTimer = null;
async function fetchUserInfo() {
try {
@@ -27,11 +29,24 @@
setCurrentUser(data);
userRole = data.role || "admin";
allowedPages = data.pages || "*";
+ // Fetch sidebar preferences
+ try {
+ const prefs = await getPreferences();
+ sidebarOrder = prefs.sidebar_order || null;
+ } catch {}
} catch (e) {
console.error("Failed to fetch user info:", e);
}
}
+ function handleOrderChange(newOrder) {
+ sidebarOrder = newOrder;
+ clearTimeout(orderSaveTimer);
+ orderSaveTimer = setTimeout(() => {
+ savePreferences({ sidebar_order: newOrder }).catch(() => {});
+ }, 500);
+ }
+
function hasPageAccess(pageId) {
if (allowedPages === "*") return true;
return Array.isArray(allowedPages) && allowedPages.includes(pageId);
@@ -109,7 +124,7 @@
{:else}
-
+
diff --git a/dashboard/frontend/src/components/Sidebar.svelte b/dashboard/frontend/src/components/Sidebar.svelte
index 2a4d968..95cc5ea 100644
--- a/dashboard/frontend/src/components/Sidebar.svelte
+++ b/dashboard/frontend/src/components/Sidebar.svelte
@@ -1,13 +1,13 @@
@@ -80,16 +151,22 @@
+ {#if currentUser.role === "admin" && rbacConfig}
+
+
+
Access Control
+
+ {rbacLoading ? "Loading..." : "Refresh"}
+
+
+
+ {#if rbacMsg}
+
{rbacMsg}
+ {/if}
+ {#if rbacErr}
+
{rbacErr}
+ {/if}
+
+
+
+
Role Defaults
+
+ {#each Object.entries(rbacConfig.role_defaults || {}) as [role, cfg]}
+
+ {role}
+ {cfg.pages === "*" ? "All pages" : (cfg.pages || []).join(", ")}
+
+ {/each}
+
+
+
+
+
+
+
User Overrides
+ {#if editingUser !== "__new__"}
+
Add Override
+ {/if}
+
+
+ {#if Object.keys(rbacConfig.user_overrides || {}).length > 0}
+
+ {#each Object.entries(rbacConfig.user_overrides) as [username, cfg]}
+ {#if editingUser === username}
+
+
{username}
+
+ {#each allPages as p}
+ togglePage(p)} class="px-2 py-0.5 text-xs rounded-md transition-colors {editPages.includes(p) ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">{p}
+ {/each}
+
+
+ Save
+ Cancel
+
+
+ {:else}
+
+
+ {username}
+ {(cfg.pages || []).join(", ")}
+
+
+ startEdit(username, cfg.pages || [])} class="text-xs text-primary-500 hover:text-primary-700">Edit
+ deleteOverride(username)} class="text-xs text-rose-500 hover:text-rose-700">Delete
+
+
+ {/if}
+ {/each}
+
+ {:else if editingUser !== "__new__"}
+
No user overrides configured
+ {/if}
+
+ {#if editingUser === "__new__"}
+
+
+
+ {#each allPages as p}
+ togglePage(p)} class="px-2 py-0.5 text-xs rounded-md transition-colors {editPages.includes(p) ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">{p}
+ {/each}
+
+
+ Save
+ Cancel
+
+
+ {/if}
+
+
+ {/if}
+