From d7afa1ded6660302a631b0b83983830289fb2d76 Mon Sep 17 00:00:00 2001 From: Adam Lamers Date: Wed, 22 Apr 2026 00:25:38 -0400 Subject: [PATCH] RBAC frontend --- spacetimedb/src/reducers.rs | 24 +- .../components/ServerSettingsPanel.svelte | 17 +- .../settings/MemberPermissionsSettings.svelte | 355 ++++++++++++++++++ src/chat/services/chat.svelte.ts | 7 + src/chat/services/server-management.svelte.ts | 5 + 5 files changed, 401 insertions(+), 7 deletions(-) create mode 100644 src/chat/components/settings/MemberPermissionsSettings.svelte diff --git a/spacetimedb/src/reducers.rs b/spacetimedb/src/reducers.rs index ddb8b59..ee012d0 100644 --- a/spacetimedb/src/reducers.rs +++ b/spacetimedb/src/reducers.rs @@ -202,12 +202,16 @@ pub fn delete_server(ctx: &ReducerContext, server_id: u64) { #[spacetimedb::reducer] pub fn edit_message(ctx: &ReducerContext, message_id: u64, new_text: String) { - validate_message_length(&ctx.db, &new_text).expect("Message too long"); + if let Err(e) = validate_message_length(&ctx.db, &new_text) { + return report_error(&ctx.db, ctx.sender(), "edit_message", &e, ctx.timestamp); + } if let Some(mut msg) = ctx.db.message().id().find(message_id) { if msg.sender == ctx.sender() { msg.text = new_text; msg.edited = true; - ctx.db.message().id().update(msg); + let msg = ctx.db.message().id().update(msg); + sync_recent_message(&ctx.db, msg); + report_success(&ctx.db, ctx.sender(), "edit_message", ctx.timestamp); } } } @@ -220,6 +224,11 @@ pub fn delete_message(ctx: &ReducerContext, message_id: u64) { if is_owner || can_moderate { ctx.db.message().id().delete(message_id); + // We should also delete from recent_message if it's there + if let Some(recent) = ctx.db.recent_message().id().find(message_id) { + ctx.db.recent_message().id().delete(recent.id); + } + report_success(&ctx.db, ctx.sender(), "delete_message", ctx.timestamp); } } } @@ -272,17 +281,22 @@ pub fn toggle_reaction( custom_emoji_id, }); } - ctx.db.message().id().update(msg); + let msg = ctx.db.message().id().update(msg); + sync_recent_message(&ctx.db, msg); + report_success(&ctx.db, ctx.sender(), "toggle_reaction", ctx.timestamp); } } #[spacetimedb::reducer] pub fn set_name(ctx: &ReducerContext, name: String) { - validate_name(&name).expect("Invalid name"); + if let Err(e) = validate_name(&name) { + return report_error(&ctx.db, ctx.sender(), "update_profile", &e, ctx.timestamp); + } if let Some(mut user) = ctx.db.user().identity().find(ctx.sender()) { user.name = Some(name); ctx.db.user().identity().update(user); sync_server_member_info(&ctx.db, ctx.sender()); + report_success(&ctx.db, ctx.sender(), "update_profile", ctx.timestamp); } } @@ -292,6 +306,7 @@ pub fn set_avatar(ctx: &ReducerContext, avatar_id: Option) { user.avatar_id = avatar_id; ctx.db.user().identity().update(user); sync_server_member_info(&ctx.db, ctx.sender()); + report_success(&ctx.db, ctx.sender(), "update_profile", ctx.timestamp); } } @@ -300,6 +315,7 @@ pub fn update_public_key(ctx: &ReducerContext, public_key: Option) { if let Some(mut user) = ctx.db.user().identity().find(ctx.sender()) { user.public_key = public_key; ctx.db.user().identity().update(user); + report_success(&ctx.db, ctx.sender(), "update_keys", ctx.timestamp); } } diff --git a/src/chat/components/ServerSettingsPanel.svelte b/src/chat/components/ServerSettingsPanel.svelte index 3e4bbf9..8724410 100644 --- a/src/chat/components/ServerSettingsPanel.svelte +++ b/src/chat/components/ServerSettingsPanel.svelte @@ -6,6 +6,7 @@ import Button from "./ui/Button.svelte"; import Input from "./ui/Input.svelte"; import Switch from "./ui/Switch.svelte"; + import MemberPermissionsSettings from "./settings/MemberPermissionsSettings.svelte"; let { onClose } = $props<{ onClose: () => void }>(); @@ -88,9 +89,15 @@ } }; - const categories = [ - { id: "overview", name: "Overview", icon: "fas fa-info-circle" }, - ]; + const canManageRoles = $derived(chat.can(Permissions.MANAGE_ROLES)); + + const categories = $derived.by(() => { + const list = [{ id: "overview", name: "Overview", icon: "fas fa-info-circle" }]; + if (canManageRoles || canManageServer) { + list.push({ id: "members", name: "Members", icon: "fas fa-users" }); + } + return list; + }); const handleOverlayClick = (e: MouseEvent) => { if (e.target === e.currentTarget) { @@ -229,6 +236,10 @@ {/if} {/if} + + {#if activeCategory === "members"} + + {/if} diff --git a/src/chat/components/settings/MemberPermissionsSettings.svelte b/src/chat/components/settings/MemberPermissionsSettings.svelte new file mode 100644 index 0000000..27186d2 --- /dev/null +++ b/src/chat/components/settings/MemberPermissionsSettings.svelte @@ -0,0 +1,355 @@ + + +
+ +
+
+ +
+ +
+ {#each filteredMembers as member (member.identity.toHexString())} + + {/each} + + {#if filteredMembers.length === 0} +
No members found.
+ {/if} +
+
+ + +
+ {#if selectedMember} + {@const isMe = chat.identity?.isEqual(selectedMember.identity)} + {@const memberPerms = getMemberPermissions(selectedMember.identity)} + +
+ +
+

{selectedMember.name || "Unknown"}

+ {selectedMember.identity.toHexString()} +
+ {#if isMe} + YOU + {/if} +
+ + {#if !canManageRoles} +
+ + Read-only: You lack 'Manage Roles' permissions. +
+ {/if} + +
+ {#each permissionList as p} + {@const hasBit = (memberPerms & p.bit) !== 0n} + {@const isRoleLock = isMe && p.bit === Permissions.MANAGE_ROLES} + +
+
+ {p.name} + {p.desc} +
+ +
+ {/each} +
+ {:else} +
+ +

Select a member from the list to manage their permissions.

+
+ {/if} +
+
+ + diff --git a/src/chat/services/chat.svelte.ts b/src/chat/services/chat.svelte.ts index 60cbd17..5025ff1 100644 --- a/src/chat/services/chat.svelte.ts +++ b/src/chat/services/chat.svelte.ts @@ -809,6 +809,9 @@ export class ChatService { get directMessages() { return this.#db.directMessages; } + get allServerPermissions() { + return this.#db.serverPermissions; + } get reducerStatus() { const myId = this.identity; if (!myId) return null; @@ -819,6 +822,10 @@ export class ChatService { this.#server.handleDeleteServer(serverId); }; + handleSetMemberPermissions = (serverId: bigint, identity: Identity, permissions: bigint) => { + this.#server.handleSetMemberPermissions(serverId, identity, permissions); + }; + handleOpenDirectMessage = (recipient: Identity) => { this.#dm.handleOpenDirectMessage(recipient); this.activeServerId = null; diff --git a/src/chat/services/server-management.svelte.ts b/src/chat/services/server-management.svelte.ts index 883f4c8..42a446b 100644 --- a/src/chat/services/server-management.svelte.ts +++ b/src/chat/services/server-management.svelte.ts @@ -11,6 +11,7 @@ export class ServerManagementService { #setServerPublicReducer = useReducer(reducers.setServerPublic); #deleteServerReducer = useReducer(reducers.deleteServer); #createInviteReducer = useReducer(reducers.createInvite); + #setMemberPermissionsReducer = useReducer(reducers.setMemberPermissions); handleCreateServer = (name: string) => { if (name.trim()) { @@ -57,4 +58,8 @@ export class ServerManagementService { handleDeleteServer = (serverId: bigint) => { this.#deleteServerReducer({ serverId }); }; + + handleSetMemberPermissions = (serverId: bigint, identity: any, permissions: bigint) => { + this.#setMemberPermissionsReducer({ serverId, identity, permissions }); + }; }