Polish RBAC role/member stores and remove state-role cycle
This commit is contained in:
@@ -4,29 +4,21 @@
|
|||||||
import {roleColorToCSS} from "@app/core/roles"
|
import {roleColorToCSS} from "@app/core/roles"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
role: RoleDefinition | string
|
role: RoleDefinition
|
||||||
label?: string
|
|
||||||
color?: number
|
|
||||||
class?: string
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {role, label, color, ...props}: Props = $props()
|
const {role, ...props}: Props = $props()
|
||||||
|
|
||||||
const roleName = $derived(typeof role === "string" ? role : role.name)
|
|
||||||
const roleLabel = $derived(
|
|
||||||
label || (typeof role === "string" ? undefined : role.label) || roleName,
|
|
||||||
)
|
|
||||||
const roleColor = $derived(color ?? (typeof role === "string" ? undefined : role.color))
|
|
||||||
|
|
||||||
const style = $derived(
|
const style = $derived(
|
||||||
roleColor === undefined
|
role.color === undefined
|
||||||
? ""
|
? ""
|
||||||
: `color: ${roleColorToCSS(roleColor)}; border-color: ${roleColorToCSS(roleColor)};`,
|
: `color: ${roleColorToCSS(role.color)}; border-color: ${roleColorToCSS(role.color)};`,
|
||||||
)
|
)
|
||||||
|
|
||||||
const className = $derived(cx("badge badge-outline badge-sm", props.class))
|
const className = $derived(cx("badge badge-outline badge-sm", props.class))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class={className} {style}>
|
<span class={className} {style}>
|
||||||
{roleLabel}
|
{role.label || role.name}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -253,7 +253,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if $members !== undefined && $members.length > 0}
|
{#if $members.length > 0}
|
||||||
<div class="card2 card2-sm bg-alt flex items-center justify-between gap-4">
|
<div class="card2 card2-sm bg-alt flex items-center justify-between gap-4">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<span>Members:</span>
|
<span>Members:</span>
|
||||||
@@ -261,10 +261,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<Button class="btn btn-neutral btn-sm" onclick={showMembers}>View All</Button>
|
<Button class="btn btn-neutral btn-sm" onclick={showMembers}>View All</Button>
|
||||||
</div>
|
</div>
|
||||||
{:else if $members === undefined}
|
|
||||||
<div class="card2 card2-sm bg-base-200 flex items-center gap-4">
|
|
||||||
<span class="text-error">Member list not available from this relay</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if $userIsAdmin && $roleDefinitions.length > 0}
|
{#if $userIsAdmin && $roleDefinitions.length > 0}
|
||||||
<div class="card2 card2-sm bg-alt col-4">
|
<div class="card2 card2-sm bg-alt col-4">
|
||||||
|
|||||||
@@ -83,11 +83,7 @@
|
|||||||
</ModalSubtitle>
|
</ModalSubtitle>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
{#if $members === undefined}
|
{#if $members.length === 0}
|
||||||
<div class="card2 bg-base-200 p-4">
|
|
||||||
<span class="text-error">Member list not available from this relay</span>
|
|
||||||
</div>
|
|
||||||
{:else if $members.length === 0}
|
|
||||||
<div class="card2 bg-base-200 p-4">
|
<div class="card2 bg-base-200 p-4">
|
||||||
<span class="text-base-content/70">No members yet</span>
|
<span class="text-base-content/70">No members yet</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,13 +22,10 @@
|
|||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import SpaceMembersAdd from "@app/components/SpaceMembersAdd.svelte"
|
import SpaceMembersAdd from "@app/components/SpaceMembersAdd.svelte"
|
||||||
import SpaceMembersBanned from "@app/components/SpaceMembersBanned.svelte"
|
import SpaceMembersBanned from "@app/components/SpaceMembersBanned.svelte"
|
||||||
import {
|
import {deriveSpaceMembers, deriveSpaceBannedPubkeyItems} from "@app/core/state"
|
||||||
deriveSpaceMembers,
|
|
||||||
deriveSpaceBannedPubkeyItems,
|
|
||||||
deriveSupportedMethods,
|
|
||||||
} from "@app/core/state"
|
|
||||||
import {
|
import {
|
||||||
deriveGroupedSpaceMembers,
|
deriveGroupedSpaceMembers,
|
||||||
|
deriveSupportedMethods,
|
||||||
deriveUserHasSpacePermission,
|
deriveUserHasSpacePermission,
|
||||||
ROOM_PERMISSION_ADD_MEMBER,
|
ROOM_PERMISSION_ADD_MEMBER,
|
||||||
ROOM_PERMISSION_REMOVE_MEMBER,
|
ROOM_PERMISSION_REMOVE_MEMBER,
|
||||||
@@ -126,11 +123,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
{#if $members === undefined}
|
{#if $members.length === 0}
|
||||||
<div class="card2 bg-base-200 p-4">
|
|
||||||
<span class="text-error">Member list not available from this space</span>
|
|
||||||
</div>
|
|
||||||
{:else if $members.length === 0}
|
|
||||||
<div class="card2 bg-base-200 p-4">
|
<div class="card2 bg-base-200 p-4">
|
||||||
<span class="text-base-content/70">No members yet</span>
|
<span class="text-base-content/70">No members yet</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import {deriveSpaceBannedPubkeyItems, deriveSupportedMethods} from "@app/core/state"
|
import {deriveSupportedMethods} from "@app/core/roles"
|
||||||
|
import {deriveSpaceBannedPubkeyItems} from "@app/core/state"
|
||||||
import {addSpaceMembers} from "@app/core/commands"
|
import {addSpaceMembers} from "@app/core/commands"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
|
|||||||
+17
-28
@@ -1,8 +1,9 @@
|
|||||||
import {derived, readable, type Readable} from "svelte/store"
|
import {derived, readable, type Readable} from "svelte/store"
|
||||||
import {first, memoize, removeUndefined, simpleCache, sortBy, uniq} from "@welshman/lib"
|
import {first, memoize, removeUndefined, simpleCache, sortBy, uniq} from "@welshman/lib"
|
||||||
import {deriveArray, deriveEventsByIdForUrl} from "@welshman/store"
|
import {deriveArray, deriveEventsByIdForUrl} from "@welshman/store"
|
||||||
import {pubkey, repository, tracker} from "@welshman/app"
|
import {pubkey, repository, tracker, manageRelay} from "@welshman/app"
|
||||||
import {
|
import {
|
||||||
|
ManagementMethod,
|
||||||
ROOM_ADD_MEMBER,
|
ROOM_ADD_MEMBER,
|
||||||
ROOM_REMOVE_MEMBER,
|
ROOM_REMOVE_MEMBER,
|
||||||
ROOM_EDIT_META,
|
ROOM_EDIT_META,
|
||||||
@@ -13,7 +14,6 @@ import {
|
|||||||
isRelayUrl,
|
isRelayUrl,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {Filter, TrustedEvent} from "@welshman/util"
|
import type {Filter, TrustedEvent} from "@welshman/util"
|
||||||
import {deriveSupportedMethods} from "@app/core/state"
|
|
||||||
|
|
||||||
export const ROOM_ROLES = 39003
|
export const ROOM_ROLES = 39003
|
||||||
|
|
||||||
@@ -31,6 +31,15 @@ const ALL_ROOM_PERMISSIONS = [
|
|||||||
ROOM_PERMISSION_BAN_USER,
|
ROOM_PERMISSION_BAN_USER,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const deriveSupportedMethods = simpleCache(([url]: [string]) =>
|
||||||
|
readable<ManagementMethod[]>([], set => {
|
||||||
|
manageRelay(url, {
|
||||||
|
method: ManagementMethod.SupportedMethods,
|
||||||
|
params: [],
|
||||||
|
}).then(({result = []}) => set(result))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export type RoleAccess = "read" | "write" | "join"
|
export type RoleAccess = "read" | "write" | "join"
|
||||||
|
|
||||||
export type RoleDefinition = {
|
export type RoleDefinition = {
|
||||||
@@ -53,13 +62,13 @@ export type RoomMember = {
|
|||||||
roles: string[]
|
roles: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MemberRoleInfo = {
|
type MemberRoleInfo = {
|
||||||
pubkey: string
|
pubkey: string
|
||||||
roles: RoleDefinition[]
|
roles: RoleDefinition[]
|
||||||
primaryRole?: RoleDefinition
|
primaryRole?: RoleDefinition
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MemberRoleGroup = {
|
type MemberRoleGroup = {
|
||||||
key: string
|
key: string
|
||||||
role?: RoleDefinition
|
role?: RoleDefinition
|
||||||
members: MemberRoleInfo[]
|
members: MemberRoleInfo[]
|
||||||
@@ -77,8 +86,6 @@ type RoomSnapshot = {
|
|||||||
admins: RoomMember[]
|
admins: RoomMember[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SpaceMemberRoleInfo = MemberRoleInfo
|
|
||||||
|
|
||||||
type SpaceRoleState = {
|
type SpaceRoleState = {
|
||||||
hasPermissionTags: boolean
|
hasPermissionTags: boolean
|
||||||
userPermissions: Set<number>
|
userPermissions: Set<number>
|
||||||
@@ -283,14 +290,10 @@ const getResolvedRoles = (rolesByName: Map<string, RoleDefinition>, roleNames: s
|
|||||||
export const sortRolesDesc = <T extends {order?: number}>(items: T[]) =>
|
export const sortRolesDesc = <T extends {order?: number}>(items: T[]) =>
|
||||||
sortBy(item => -(item.order ?? -Infinity), items)
|
sortBy(item => -(item.order ?? -Infinity), items)
|
||||||
|
|
||||||
export const getRoleLabel = (role: RoleDefinition) => role.label || role.name
|
|
||||||
|
|
||||||
export const getRolePermissionsLabel = (role: RoleDefinition) => role.permissions.join(", ")
|
export const getRolePermissionsLabel = (role: RoleDefinition) => role.permissions.join(", ")
|
||||||
|
|
||||||
export const getRoleAccessLabel = (role: RoleDefinition) => Array.from(role.access).join(", ")
|
export const getRoleAccessLabel = (role: RoleDefinition) => Array.from(role.access).join(", ")
|
||||||
|
|
||||||
const getPrimaryRole = (roles: RoleDefinition[]) => first(sortRolesDesc(roles))
|
|
||||||
|
|
||||||
const toMemberRoleInfo = (pubkey: string, roles: RoleDefinition[]): MemberRoleInfo => {
|
const toMemberRoleInfo = (pubkey: string, roles: RoleDefinition[]): MemberRoleInfo => {
|
||||||
const sortedRoles = sortRolesDesc(roles)
|
const sortedRoles = sortRolesDesc(roles)
|
||||||
|
|
||||||
@@ -304,7 +307,7 @@ const toMemberRoleInfo = (pubkey: string, roles: RoleDefinition[]): MemberRoleIn
|
|||||||
const sortMemberRoleInfos = (members: MemberRoleInfo[]) =>
|
const sortMemberRoleInfos = (members: MemberRoleInfo[]) =>
|
||||||
sortBy(member => -(member.primaryRole?.order ?? -Infinity), members)
|
sortBy(member => -(member.primaryRole?.order ?? -Infinity), members)
|
||||||
|
|
||||||
export const groupMemberRoleInfos = (members: MemberRoleInfo[]) => {
|
const groupMemberRoleInfos = (members: MemberRoleInfo[]) => {
|
||||||
const byRole = new Map<string, MemberRoleGroup>()
|
const byRole = new Map<string, MemberRoleGroup>()
|
||||||
const ungrouped: MemberRoleGroup = {
|
const ungrouped: MemberRoleGroup = {
|
||||||
key: "members",
|
key: "members",
|
||||||
@@ -578,19 +581,12 @@ export const deriveHasPermission = (url: string, h: string, kind: number) =>
|
|||||||
([$permissions, $isSpaceAdmin]) => $isSpaceAdmin || $permissions.has(kind),
|
([$permissions, $isSpaceAdmin]) => $isSpaceAdmin || $permissions.has(kind),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const deriveUserRoleColor = (url: string, h: string, targetPubkey: string) =>
|
|
||||||
derived(
|
|
||||||
[deriveUserRoles(url, h, targetPubkey), deriveRoomRoles(url, h)],
|
|
||||||
([$roleNames, $roomRoles]) =>
|
|
||||||
getPrimaryRole(getResolvedRoles($roomRoles.roles, $roleNames))?.color,
|
|
||||||
)
|
|
||||||
|
|
||||||
export const deriveRoomRoleDefinitions = (url: string, h: string) =>
|
export const deriveRoomRoleDefinitions = (url: string, h: string) =>
|
||||||
derived(deriveRoomRoles(url, h), $roomRoles =>
|
derived(deriveRoomRoles(url, h), $roomRoles =>
|
||||||
sortRolesDesc(Array.from($roomRoles.roles.values())),
|
sortRolesDesc(Array.from($roomRoles.roles.values())),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const deriveRoomMemberRoleInfo = (url: string, h: string) =>
|
const deriveRoomMemberRoleInfo = (url: string, h: string) =>
|
||||||
derived([deriveRoomMembers(url, h), deriveRoomRoles(url, h)], ([$members, $roomRoles]) =>
|
derived([deriveRoomMembers(url, h), deriveRoomRoles(url, h)], ([$members, $roomRoles]) =>
|
||||||
sortMemberRoleInfos(
|
sortMemberRoleInfos(
|
||||||
$members.map(member =>
|
$members.map(member =>
|
||||||
@@ -602,16 +598,9 @@ export const deriveRoomMemberRoleInfo = (url: string, h: string) =>
|
|||||||
export const deriveGroupedRoomMembers = (url: string, h: string) =>
|
export const deriveGroupedRoomMembers = (url: string, h: string) =>
|
||||||
derived(deriveRoomMemberRoleInfo(url, h), $members => groupMemberRoleInfos($members))
|
derived(deriveRoomMemberRoleInfo(url, h), $members => groupMemberRoleInfos($members))
|
||||||
|
|
||||||
export const getRoleSortKey = (url: string, h: string, targetPubkey: string) =>
|
const deriveSpaceMemberRoleInfo = (url: string) =>
|
||||||
derived(
|
|
||||||
[deriveUserRoles(url, h, targetPubkey), deriveRoomRoles(url, h)],
|
|
||||||
([$roleNames, $roomRoles]) =>
|
|
||||||
getPrimaryRole(getResolvedRoles($roomRoles.roles, $roleNames))?.order,
|
|
||||||
)
|
|
||||||
|
|
||||||
export const deriveSpaceMemberRoleInfo = (url: string) =>
|
|
||||||
derived(deriveSpaceRoleState(url), $spaceRoleState => {
|
derived(deriveSpaceRoleState(url), $spaceRoleState => {
|
||||||
const roleInfoByPubkey = new Map<string, SpaceMemberRoleInfo>()
|
const roleInfoByPubkey = new Map<string, MemberRoleInfo>()
|
||||||
|
|
||||||
for (const [pubkey, roles] of $spaceRoleState.memberRoles.entries()) {
|
for (const [pubkey, roles] of $spaceRoleState.memberRoles.entries()) {
|
||||||
roleInfoByPubkey.set(pubkey, toMemberRoleInfo(pubkey, roles))
|
roleInfoByPubkey.set(pubkey, toMemberRoleInfo(pubkey, roles))
|
||||||
|
|||||||
@@ -1300,15 +1300,6 @@ export const deriveSocketStatus = (url: string) =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const deriveSupportedMethods = simpleCache(([url]: [string]) => {
|
|
||||||
return readable<ManagementMethod[]>([], set => {
|
|
||||||
manageRelay(url, {
|
|
||||||
method: ManagementMethod.SupportedMethods,
|
|
||||||
params: [],
|
|
||||||
}).then(({result = []}) => set(result))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
export const deriveHasLivekit = simpleCache(([url]: [string]) =>
|
export const deriveHasLivekit = simpleCache(([url]: [string]) =>
|
||||||
readable<boolean | undefined>(undefined, set => {
|
readable<boolean | undefined>(undefined, set => {
|
||||||
checkRelayHasLivekit(url).then(has => set(has))
|
checkRelayHasLivekit(url).then(has => set(has))
|
||||||
|
|||||||
Reference in New Issue
Block a user