feat: add UI to edit role default page permissions #11
@@ -403,3 +403,19 @@ async def rbac_delete_override(username: str, current_user = Depends(auth.get_cu
|
|||||||
rbac.get("user_overrides", {}).pop(username, None)
|
rbac.get("user_overrides", {}).pop(username, None)
|
||||||
save_rbac(rbac)
|
save_rbac(rbac)
|
||||||
return {"ok": True}
|
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}
|
||||||
|
|||||||
@@ -140,6 +140,8 @@
|
|||||||
let editingUser = $state("");
|
let editingUser = $state("");
|
||||||
let editPages = $state([]);
|
let editPages = $state([]);
|
||||||
let newUsername = $state("");
|
let newUsername = $state("");
|
||||||
|
let editingRole = $state("");
|
||||||
|
let editRolePages = $state([]);
|
||||||
|
|
||||||
async function loadRbac() {
|
async function loadRbac() {
|
||||||
rbacLoading = true;
|
rbacLoading = true;
|
||||||
@@ -153,18 +155,38 @@
|
|||||||
editingUser = username;
|
editingUser = username;
|
||||||
editPages = [...pages];
|
editPages = [...pages];
|
||||||
newUsername = "";
|
newUsername = "";
|
||||||
|
editingRole = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function startAdd() {
|
function startAdd() {
|
||||||
editingUser = "__new__";
|
editingUser = "__new__";
|
||||||
editPages = ["dashboard"];
|
editPages = ["dashboard"];
|
||||||
newUsername = "";
|
newUsername = "";
|
||||||
|
editingRole = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelEdit() {
|
function cancelEdit() {
|
||||||
editingUser = "";
|
editingUser = "";
|
||||||
editPages = [];
|
editPages = [];
|
||||||
newUsername = "";
|
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) {
|
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(() => {
|
onMount(() => {
|
||||||
loadPasskeys();
|
loadPasskeys();
|
||||||
if (currentUser.role === "admin") loadRbac();
|
if (currentUser.role === "admin") loadRbac();
|
||||||
@@ -299,12 +336,31 @@
|
|||||||
<!-- Role Defaults -->
|
<!-- Role Defaults -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<p class="text-xs font-medium text-surface-500 mb-2">Role Defaults</p>
|
<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]}
|
{#each Object.entries(rbacConfig.role_defaults || {}) as [role, cfg]}
|
||||||
<div class="flex items-center gap-2 text-xs">
|
{#if editingRole === role}
|
||||||
<span class="font-medium text-surface-700 dark:text-surface-300 w-16">{role}</span>
|
<div class="p-3 bg-surface-50 dark:bg-surface-700 rounded-lg space-y-2">
|
||||||
<span class="text-surface-400">{cfg.pages === "*" ? "All pages" : (cfg.pages || []).join(", ")}</span>
|
<p class="text-xs font-medium text-surface-700 dark:text-surface-200">{role}</p>
|
||||||
</div>
|
<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}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user