feat: add UI to edit role default page permissions #11

Merged
jimmy merged 1 commits from dev into main 2026-03-01 17:58:40 +08:00
2 changed files with 77 additions and 5 deletions
+16
View File
@@ -403,3 +403,19 @@ async def rbac_delete_override(username: str, current_user = Depends(auth.get_cu
rbac.get("user_overrides", {}).pop(username, None)
save_rbac(rbac)
return {"ok": True}
@router.put("/rbac/roles/{role}")
async def rbac_update_role(role: str, request: Request, current_user = Depends(auth.get_current_user)):
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="Admin only")
from rbac import load_rbac, save_rbac
body = await request.json()
pages = body.get("pages")
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")
rbac = load_rbac()
rbac.setdefault("role_defaults", {})[role] = {"pages": pages}
save_rbac(rbac)
return {"ok": True}
+57 -1
View File
@@ -140,6 +140,8 @@
let editingUser = $state("");
let editPages = $state([]);
let newUsername = $state("");
let editingRole = $state("");
let editRolePages = $state([]);
async function loadRbac() {
rbacLoading = true;
@@ -153,18 +155,38 @@
editingUser = username;
editPages = [...pages];
newUsername = "";
editingRole = "";
}
function startAdd() {
editingUser = "__new__";
editPages = ["dashboard"];
newUsername = "";
editingRole = "";
}
function cancelEdit() {
editingUser = "";
editPages = [];
newUsername = "";
editingRole = "";
editRolePages = [];
}
function startEditRole(role, pages) {
editingRole = role;
editRolePages = pages === "*" ? ["*"] : [...pages];
editingUser = "";
}
function toggleRolePage(p) {
if (p === "*") {
editRolePages = ["*"];
} else {
if (editRolePages.includes("*")) editRolePages = [];
if (editRolePages.includes(p)) editRolePages = editRolePages.filter(x => x !== p);
else editRolePages = [...editRolePages, p];
}
}
function togglePage(p) {
@@ -199,6 +221,21 @@
}
}
async function saveRoleDefault() {
rbacMsg = ""; rbacErr = "";
if (!editingRole) return;
if (!editRolePages.length) { rbacErr = "Select at least one page or '*' for all"; return; }
try {
const pages = editRolePages.includes("*") ? "*" : editRolePages;
await put(`/auth/rbac/roles/${encodeURIComponent(editingRole)}`, { pages });
rbacMsg = `Updated role default for ${editingRole}`;
cancelEdit();
await loadRbac();
} catch (e) {
rbacErr = e.body?.detail || "Failed to save role";
}
}
onMount(() => {
loadPasskeys();
if (currentUser.role === "admin") loadRbac();
@@ -299,12 +336,31 @@
<!-- Role Defaults -->
<div class="mb-4">
<p class="text-xs font-medium text-surface-500 mb-2">Role Defaults</p>
<div class="space-y-1.5">
<div class="space-y-2">
{#each Object.entries(rbacConfig.role_defaults || {}) as [role, cfg]}
{#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>
<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>
<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>
</div>
</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>
<button onclick={() => startEditRole(role, cfg.pages)} class="text-xs text-primary-500 hover:text-primary-700">Edit</button>
</div>
{/if}
{/each}
</div>
</div>