feat(rbac): implement NIP-29 room roles and permission gating (#47)

This commit is contained in:
2026-04-17 05:57:10 +05:30
parent 4a967de184
commit abc6dc2860
11 changed files with 951 additions and 140 deletions
+48 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import {sortBy} from "@welshman/lib"
import {goto} from "$app/navigation"
import type {RoomMeta} from "@welshman/util"
import {displayRelayUrl, makeRoomMeta} from "@welshman/util"
@@ -27,10 +28,12 @@
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte"
import RoleBadge from "@app/components/RoleBadge.svelte"
import RoomMembers from "@app/components/RoomMembers.svelte"
import RoomEdit from "@app/components/RoomEdit.svelte"
import RoomName from "@app/components/RoomName.svelte"
import RoomImage from "@app/components/RoomImage.svelte"
import {deriveRoomRoles, hasPermission} from "@app/core/roles"
import {
deriveRoom,
deriveRoomMembers,
@@ -58,13 +61,29 @@
const room = deriveRoom(url, h)
const members = deriveRoomMembers(url, h)
const roomRoles = deriveRoomRoles(url, h)
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
const canEditMetadata = hasPermission(url, h, 9002)
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
const userRooms = deriveUserRooms(url)
const isFavorite = $derived($userRooms.includes(h))
const shouldNotify = deriveShouldNotify(url, h)
const roleRows = $derived.by(() =>
sortBy(
role => -(role.order ?? -Infinity),
Array.from($roomRoles.roles.values()).map(role => ({
name: role.name,
label: role.label,
color: role.color,
order: role.order,
permissionsLabel: role.permissions.join(", "),
accessLabel: Array.from(role.access).join(", "),
})),
),
)
const back = () => history.back()
const toggleMenu = () => {
@@ -152,7 +171,7 @@
<ul
transition:fly
class="bg-alt menu absolute right-0 z-popover w-48 gap-1 rounded-box p-2 shadow-md">
{#if $userIsAdmin}
{#if $canEditMetadata}
<li>
<Button onclick={startEdit}>
<Icon icon={Pen} />
@@ -247,7 +266,7 @@
<div class="card2 card2-sm bg-alt flex items-center justify-between gap-4">
<div class="flex items-center gap-4">
<span>Members:</span>
<ProfileCircles pubkeys={$members} />
<ProfileCircles pubkeys={$members.map(member => member.pubkey)} />
</div>
<Button class="btn btn-neutral btn-sm" onclick={showMembers}>View All</Button>
</div>
@@ -256,6 +275,33 @@
<span class="text-error">Member list not available from this relay</span>
</div>
{/if}
{#if $userIsAdmin && roleRows.length > 0}
<div class="card2 card2-sm bg-alt col-4">
<strong class="text-lg">Role Definitions</strong>
<div class="flex flex-col gap-2">
{#each roleRows as role (role.name)}
<div class="rounded-box bg-base-300 p-3 flex flex-col gap-2">
<div class="flex items-center gap-2">
<RoleBadge
role={role.name}
label={role.label}
color={role.color}
class="badge-md" />
{#if role.order !== undefined}
<span class="text-xs opacity-70">Order {role.order}</span>
{/if}
</div>
{#if role.permissionsLabel}
<p class="text-xs opacity-75">Permissions: {role.permissionsLabel}</p>
{/if}
{#if role.accessLabel}
<p class="text-xs opacity-75">Access: {role.accessLabel}</p>
{/if}
</div>
{/each}
</div>
</div>
{/if}
<div class="card2 card2-sm bg-alt col-4">
<strong class="text-lg">Room Settings</strong>
<div class="flex items-center justify-between">