feat: add role-based sidebar link visibility control
Deploy Dashboard (Dev) / deploy-dev (push) Successful in 1m37s
Deploy Dashboard (Dev) / deploy-dev (push) Successful in 1m37s
This commit is contained in:
@@ -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 @@
|
||||
<Login />
|
||||
{:else}
|
||||
<div class="flex h-screen overflow-hidden bg-surface-50 dark:bg-surface-900">
|
||||
<Sidebar bind:page {dark} {toggleTheme} {logout} bind:sidebarOpen {allowedPages} {userRole} {sidebarOrder} onOrderChange={handleOrderChange} />
|
||||
<Sidebar bind:page {dark} {toggleTheme} {logout} bind:sidebarOpen {allowedPages} {allowedSidebarLinks} {userRole} {sidebarOrder} onOrderChange={handleOrderChange} />
|
||||
<main class="flex-1 overflow-auto">
|
||||
<div class="max-w-[1400px] mx-auto p-6 lg:p-8">
|
||||
<!-- Mobile hamburger -->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
let { page = $bindable("dashboard"), dark = false, toggleTheme = () => {}, logout = () => {}, sidebarOpen = $bindable(false), allowedPages = "*", userRole = "admin", sidebarOrder = null, onOrderChange = () => {} } = $props();
|
||||
let { page = $bindable("dashboard"), dark = false, toggleTheme = () => {}, logout = () => {}, sidebarOpen = $bindable(false), allowedPages = "*", allowedSidebarLinks = "*", userRole = "admin", sidebarOrder = null, onOrderChange = () => {} } = $props();
|
||||
|
||||
function canAccess(id) {
|
||||
if (!id) return true;
|
||||
@@ -7,6 +7,13 @@
|
||||
return Array.isArray(allowedPages) && allowedPages.includes(id);
|
||||
}
|
||||
|
||||
function canSeeSidebarLink(item) {
|
||||
if (allowedSidebarLinks === "*") return true;
|
||||
// Check by label or id
|
||||
const key = item.label || item.id;
|
||||
return Array.isArray(allowedSidebarLinks) && allowedSidebarLinks.includes(key);
|
||||
}
|
||||
|
||||
const defaultLinks = [
|
||||
{ id: "dashboard", label: "Overview", icon: "grid" },
|
||||
{ id: "docker", label: "Docker", icon: "box" },
|
||||
@@ -283,7 +290,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#each items as item, i}
|
||||
{#if canAccess(item.id)}
|
||||
{#if canAccess(item.id) && canSeeSidebarLink(item)}
|
||||
{#if item.external || item.remoteHref || item.port}
|
||||
<a
|
||||
href={extHref(item)}
|
||||
|
||||
@@ -133,6 +133,7 @@
|
||||
|
||||
// Access Control
|
||||
const allPages = ["dashboard", "docker", "files", "terminal", "security", "openclaw", "chat-digest", "gitea", "settings"];
|
||||
const allSidebarLinks = ["Overview", "Docker", "Files", "Terminal", "Security", "Navidrome", "Jellyfin", "Audiobookshelf", "Immich", "OpenClaw", "Chat Digest", "Repos", "Gitea Web", "Stirling PDF", "Vaultwarden", "n8n", "Speedtest"];
|
||||
let rbacConfig = $state(null);
|
||||
let rbacLoading = $state(false);
|
||||
let rbacMsg = $state("");
|
||||
@@ -142,6 +143,7 @@
|
||||
let newUsername = $state("");
|
||||
let editingRole = $state("");
|
||||
let editRolePages = $state([]);
|
||||
let editRoleSidebarLinks = $state([]);
|
||||
|
||||
async function loadRbac() {
|
||||
rbacLoading = true;
|
||||
@@ -171,11 +173,13 @@
|
||||
newUsername = "";
|
||||
editingRole = "";
|
||||
editRolePages = [];
|
||||
editRoleSidebarLinks = [];
|
||||
}
|
||||
|
||||
function startEditRole(role, pages) {
|
||||
function startEditRole(role, pages, sidebarLinks) {
|
||||
editingRole = role;
|
||||
editRolePages = pages === "*" ? ["*"] : [...pages];
|
||||
editRoleSidebarLinks = sidebarLinks === "*" ? ["*"] : (sidebarLinks ? [...sidebarLinks] : ["*"]);
|
||||
editingUser = "";
|
||||
}
|
||||
|
||||
@@ -189,6 +193,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRoleSidebarLink(link) {
|
||||
if (link === "*") {
|
||||
editRoleSidebarLinks = ["*"];
|
||||
} else {
|
||||
if (editRoleSidebarLinks.includes("*")) editRoleSidebarLinks = [];
|
||||
if (editRoleSidebarLinks.includes(link)) editRoleSidebarLinks = editRoleSidebarLinks.filter(x => x !== link);
|
||||
else editRoleSidebarLinks = [...editRoleSidebarLinks, link];
|
||||
}
|
||||
}
|
||||
|
||||
function togglePage(p) {
|
||||
if (editPages.includes(p)) editPages = editPages.filter(x => x !== p);
|
||||
else editPages = [...editPages, p];
|
||||
@@ -225,9 +239,11 @@
|
||||
rbacMsg = ""; rbacErr = "";
|
||||
if (!editingRole) return;
|
||||
if (!editRolePages.length) { rbacErr = "Select at least one page or '*' for all"; return; }
|
||||
if (!editRoleSidebarLinks.length) { rbacErr = "Select at least one sidebar link or '*' for all"; return; }
|
||||
try {
|
||||
const pages = editRolePages.includes("*") ? "*" : editRolePages;
|
||||
await put(`/auth/rbac/roles/${encodeURIComponent(editingRole)}`, { pages });
|
||||
const sidebar_links = editRoleSidebarLinks.includes("*") ? "*" : editRoleSidebarLinks;
|
||||
await put(`/auth/rbac/roles/${encodeURIComponent(editingRole)}`, { pages, sidebar_links });
|
||||
rbacMsg = `Updated role default for ${editingRole}`;
|
||||
cancelEdit();
|
||||
await loadRbac();
|
||||
@@ -341,12 +357,20 @@
|
||||
{#if editingRole === role}
|
||||
<div class="p-3 bg-surface-50 dark:bg-surface-700 rounded-lg space-y-2">
|
||||
<p class="text-xs font-medium text-surface-700 dark:text-surface-200">{role}</p>
|
||||
<p class="text-[10px] text-surface-500 mt-1">Pages (internal dashboard pages)</p>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button onclick={() => toggleRolePage("*")} class="px-2 py-0.5 text-xs rounded-md transition-colors {editRolePages.includes('*') ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">* (all)</button>
|
||||
{#each allPages as p}
|
||||
<button onclick={() => toggleRolePage(p)} class="px-2 py-0.5 text-xs rounded-md transition-colors {editRolePages.includes(p) ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">{p}</button>
|
||||
{/each}
|
||||
</div>
|
||||
<p class="text-[10px] text-surface-500 mt-2">Sidebar Links (all services)</p>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button onclick={() => toggleRoleSidebarLink("*")} class="px-2 py-0.5 text-xs rounded-md transition-colors {editRoleSidebarLinks.includes('*') ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">* (all)</button>
|
||||
{#each allSidebarLinks as link}
|
||||
<button onclick={() => toggleRoleSidebarLink(link)} class="px-2 py-0.5 text-xs rounded-md transition-colors {editRoleSidebarLinks.includes(link) ? 'bg-primary-600 text-white' : 'bg-surface-200 dark:bg-surface-600 text-surface-500'}">{link}</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick={saveRoleDefault} class="px-3 py-1 text-xs font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-md transition-colors">Save</button>
|
||||
<button onclick={cancelEdit} class="px-3 py-1 text-xs font-medium text-surface-600 hover:text-surface-800 dark:text-surface-400 dark:hover:text-surface-200">Cancel</button>
|
||||
@@ -354,11 +378,17 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center justify-between p-2 bg-surface-50 dark:bg-surface-700 rounded-lg">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="font-medium text-surface-700 dark:text-surface-300 w-16">{role}</span>
|
||||
<span class="text-surface-400">{cfg.pages === "*" ? "All pages" : (cfg.pages || []).join(", ")}</span>
|
||||
<div class="flex flex-col gap-1 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-surface-700 dark:text-surface-300 w-16">{role}</span>
|
||||
<span class="text-surface-400">Pages: {cfg.pages === "*" ? "All" : (cfg.pages || []).join(", ")}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-16"></span>
|
||||
<span class="text-surface-400">Links: {cfg.sidebar_links === "*" ? "All" : (cfg.sidebar_links || ["*"]).join(", ")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick={() => startEditRole(role, cfg.pages)} class="text-xs text-primary-500 hover:text-primary-700">Edit</button>
|
||||
<button onclick={() => startEditRole(role, cfg.pages, cfg.sidebar_links)} class="text-xs text-primary-500 hover:text-primary-700">Edit</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
Reference in New Issue
Block a user