fix: enforce channel-level RBAC across WS broadcasts, REST endpoints, and frontend reactivity

Wire the bitwise permission engine end-to-end:

- Add sendToChannel() to ConnectionManager, filtering WS recipients by VIEW_CHANNEL
- Convert 6 channel-scoped events (messages, typing, reactions) from sendToServer to sendToChannel
- Add broadcastOverrideChange() to push channel_updated/channel_deleted per-user on override mutations
- Bridge legacy server_members.role TEXT to member_roles junction table on PATCH
- Add pushReadyPayload() to force re-sync frontend store after role changes
- Filter channels by VIEW_CHANNEL in GET /api/servers/:id to prevent initial load data leak
- Pre-compute viewers before CASCADE delete on channel_deleted
- Fix frontend channel event handlers to upsert/cleanup channelToServerMap and channelPermissions
- Add ChannelSettingsModal with Private Channel toggle and gear icon in ChannelSidebar
This commit is contained in:
Jannis Braun
2026-02-24 06:10:11 +01:00
parent 8030c89c6c
commit 76b8a43be2
10 changed files with 366 additions and 24 deletions
+26 -4
View File
@@ -318,29 +318,51 @@ function handleEvent(event: ServerEvent): void {
}
case 'channel_created': {
const { currentServerId: curServerId, channels: curChannels, setChannels } = useServerStore.getState();
const { currentServerId: curServerId, channels: curChannels, setChannels, channelToServerMap, channelPermissions } = useServerStore.getState();
if (event.serverId === curServerId) {
// Deduplicate: only add if not already present
if (!curChannels.find(c => c.id === event.channel.id)) {
setChannels([...curChannels, event.channel].sort((a, b) => a.position - b.position));
}
}
// Update auxiliary maps
channelToServerMap.set(event.channel.id, event.serverId);
if (event.channel.myPermissions) {
channelPermissions.set(event.channel.id, event.channel.myPermissions);
}
break;
}
case 'channel_updated': {
const { currentServerId: curServerId2, channels: curChannels2, setChannels: setChannels2 } = useServerStore.getState();
const { currentServerId: curServerId2, channels: curChannels2, setChannels: setChannels2, channelPermissions: chPermsMap2 } = useServerStore.getState();
if (event.serverId === curServerId2) {
setChannels2(curChannels2.map(c => c.id === event.channel.id ? event.channel : c).sort((a, b) => a.position - b.position));
const exists = curChannels2.some(c => c.id === event.channel.id);
if (exists) {
// Replace existing channel data
setChannels2(curChannels2.map(c => c.id === event.channel.id ? event.channel : c).sort((a, b) => a.position - b.position));
} else {
// Upsert: user just gained access to this channel
setChannels2([...curChannels2, event.channel].sort((a, b) => a.position - b.position));
// Populate channelToServerMap for the new channel
const { channelToServerMap: ctsMmap } = useServerStore.getState();
ctsMmap.set(event.channel.id, event.serverId);
}
}
// Sync channelPermissions with the server's computed value
if (event.channel.myPermissions) {
chPermsMap2.set(event.channel.id, event.channel.myPermissions);
}
break;
}
case 'channel_deleted': {
const { currentServerId: curServerId3, channels: curChannels3, setChannels: setChannels3 } = useServerStore.getState();
const { currentServerId: curServerId3, channels: curChannels3, setChannels: setChannels3, channelPermissions: chPermsMap3, channelToServerMap: ctsMap3 } = useServerStore.getState();
if (event.serverId === curServerId3) {
setChannels3(curChannels3.filter(c => c.id !== event.channelId));
}
// Clean up auxiliary maps
chPermsMap3.delete(event.channelId);
ctsMap3.delete(event.channelId);
// If the user is currently viewing this channel, navigate away
{
const { currentChannelId } = useChatStore.getState();