From a8debcfb4b7ea3538b2c24731d58e9de92abace9 Mon Sep 17 00:00:00 2001 From: "Gan, Jimmy" Date: Sun, 1 Mar 2026 14:48:35 +0800 Subject: [PATCH] feat: access control UI in Settings + sidebar drag-and-drop reordering - Add Access Control card in Settings (admin-only): role defaults display, user overrides CRUD - Add sidebar drag-and-drop reordering with per-user persistence in rbac.json - Backend: GET/PUT /api/auth/preferences endpoints, sidebar order helpers in rbac.py - Frontend: HTML5 drag API with grip handles, smart order merge, debounced save - Preserve sidebar_order when updating page overrides --- dashboard/backend/rbac.py | 12 ++ dashboard/backend/routers/auth.py | 20 ++- dashboard/frontend/src/App.svelte | 19 +- .../frontend/src/components/Sidebar.svelte | 123 +++++++++++-- dashboard/frontend/src/lib/api.js | 12 ++ dashboard/frontend/src/routes/Settings.svelte | 168 +++++++++++++++++- 6 files changed, 334 insertions(+), 20 deletions(-) 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

+ +
+ + {#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__"} + + {/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} + + {/each} +
+
+ + +
+
+ {:else} +
+
+ {username} + {(cfg.pages || []).join(", ")} +
+
+ + +
+
+ {/if} + {/each} +
+ {:else if editingUser !== "__new__"} +

No user overrides configured

+ {/if} + + {#if editingUser === "__new__"} +
+ +
+ {#each allPages as p} + + {/each} +
+
+ + +
+
+ {/if} +
+
+ {/if} +

Audit Log