fix: resolve federated identity checks for server mute/deafen pipeline

- The client now dynamically resolves the user's federated identity via `getMyUserIdForOrigin` when evaluating incoming `voice_server_muted` and `voice_server_deafened` events. Previously, the client incorrectly compared the remote event's federated `userId` against the local `authStore` home `userId`, causing federated users to silently drop restriction events.
- Client-side mic/deafen toggles (`toggleMic`, `toggleDeafen`) now accurately evaluate the user's origin-specific ID against the restriction sets, preventing federated users from bypassing locks.
- UI state selectors (`VoiceControlBar`, `ChannelSidebar`) now compute `myOriginId` to correctly render the yellow server-lockdown indicators for cross-instance users.
This commit is contained in:
Jannis Braun
2026-03-09 19:54:15 +01:00
parent 6a12fe2024
commit db1909d785
4 changed files with 97 additions and 73 deletions
@@ -1,6 +1,6 @@
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { useSpaceStore, getChannelOrigin } from '../../stores/spaceStore'; import { useSpaceStore, getChannelOrigin, getMyUserIdForOrigin } from '../../stores/spaceStore';
import { useChatStore } from '../../stores/chatStore'; import { useChatStore } from '../../stores/chatStore';
import { useUIStore } from '../../stores/uiStore'; import { useUIStore } from '../../stores/uiStore';
import { useAuthStore } from '../../stores/authStore'; import { useAuthStore } from '../../stores/authStore';
@@ -35,10 +35,11 @@ export function ChannelSidebar() {
const toggleMic = useVoiceStore((s) => s.toggleMic); const toggleMic = useVoiceStore((s) => s.toggleMic);
const toggleDeafen = useVoiceStore((s) => s.toggleDeafen); const toggleDeafen = useVoiceStore((s) => s.toggleDeafen);
const spaceId = useSpaceStore((s) => currentVoiceChannelId ? s.channelToSpaceMap.get(currentVoiceChannelId) : null); const spaceId = useSpaceStore((s) => currentVoiceChannelId ? s.channelToSpaceMap.get(currentVoiceChannelId) : null);
const myOriginId = useSpaceStore((s) => currentVoiceChannelId ? getMyUserIdForOrigin(getChannelOrigin(currentVoiceChannelId)) : s.members.find(m => m.userId === user?.id)?.userId ?? user?.id);
const serverMutedUserIds = useVoiceStore((s) => s.serverMutedUserIds); const serverMutedUserIds = useVoiceStore((s) => s.serverMutedUserIds);
const serverDeafenedUserIds = useVoiceStore((s) => s.serverDeafenedUserIds); const serverDeafenedUserIds = useVoiceStore((s) => s.serverDeafenedUserIds);
const isServerMuted = !!(user && spaceId && serverMutedUserIds.has(`${spaceId}:${user.id}`)); const isServerMuted = !!(myOriginId && spaceId && serverMutedUserIds.has(`${spaceId}:${myOriginId}`));
const isServerDeafened = !!(user && spaceId && serverDeafenedUserIds.has(`${spaceId}:${user.id}`)); const isServerDeafened = !!(myOriginId && spaceId && serverDeafenedUserIds.has(`${spaceId}:${myOriginId}`));
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@@ -4,7 +4,7 @@ import { useUIStore } from '../../stores/uiStore';
import { useAuthStore } from '../../stores/authStore'; import { useAuthStore } from '../../stores/authStore';
import { getActiveRoom } from '../../hooks/useLiveKit'; import { getActiveRoom } from '../../hooks/useLiveKit';
import { wsSend } from '../../hooks/useWebSocket'; import { wsSend } from '../../hooks/useWebSocket';
import { useSpaceStore, getChannelOrigin } from '../../stores/spaceStore'; import { useSpaceStore, getChannelOrigin, getMyUserIdForOrigin } from '../../stores/spaceStore';
import { ScreenShareSettingsPopover } from './ScreenShareSettingsPopover'; import { ScreenShareSettingsPopover } from './ScreenShareSettingsPopover';
import { CAMERA_PRESET, startScreenShare, stopScreenShare } from '../../utils/screenShare'; import { CAMERA_PRESET, startScreenShare, stopScreenShare } from '../../utils/screenShare';
@@ -28,11 +28,12 @@ export function VoiceControlBar() {
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId); const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
const myUser = useAuthStore((s) => s.user); const myUser = useAuthStore((s) => s.user);
const spaceId = useSpaceStore((s) => currentVoiceChannelId ? s.channelToSpaceMap.get(currentVoiceChannelId) : null); const spaceId = useSpaceStore((s) => currentVoiceChannelId ? s.channelToSpaceMap.get(currentVoiceChannelId) : null);
const voiceOrigin = currentVoiceChannelId ? getChannelOrigin(currentVoiceChannelId) : '';
const myOriginId = useSpaceStore((s) => currentVoiceChannelId ? getMyUserIdForOrigin(getChannelOrigin(currentVoiceChannelId)) : s.members.find(m => m.userId === myUser?.id)?.userId ?? myUser?.id);
const serverMutedUserIds = useVoiceStore((s) => s.serverMutedUserIds); const serverMutedUserIds = useVoiceStore((s) => s.serverMutedUserIds);
const serverDeafenedUserIds = useVoiceStore((s) => s.serverDeafenedUserIds); const serverDeafenedUserIds = useVoiceStore((s) => s.serverDeafenedUserIds);
const isServerMuted = !!(myUser && spaceId && serverMutedUserIds.has(`${spaceId}:${myUser.id}`)); const isServerMuted = !!(myOriginId && spaceId && serverMutedUserIds.has(`${spaceId}:${myOriginId}`));
const isServerDeafened = !!(myUser && spaceId && serverDeafenedUserIds.has(`${spaceId}:${myUser.id}`)); const isServerDeafened = !!(myOriginId && spaceId && serverDeafenedUserIds.has(`${spaceId}:${myOriginId}`));
const voiceOrigin = currentVoiceChannelId ? getChannelOrigin(currentVoiceChannelId) : '';
const [qualityOpen, setQualityOpen] = useState(false); const [qualityOpen, setQualityOpen] = useState(false);
const qualityBtnRef = useRef<HTMLButtonElement>(null); const qualityBtnRef = useRef<HTMLButtonElement>(null);
+22 -2
View File
@@ -319,7 +319,8 @@ function handleEvent(origin: string, event: ServerEvent): void {
case 'voice_server_muted': { case 'voice_server_muted': {
const { setServerMutedUser } = useVoiceStore.getState(); const { setServerMutedUser } = useVoiceStore.getState();
setServerMutedUser(event.spaceId, event.userId, event.muted); setServerMutedUser(event.spaceId, event.userId, event.muted);
const myUserId = useAuthStore.getState().user?.id;
const checkMute = (myUserId: string | undefined) => {
if (event.userId === myUserId) { if (event.userId === myUserId) {
if (event.muted) { if (event.muted) {
// Force-mute the mic // Force-mute the mic
@@ -341,13 +342,23 @@ function handleEvent(origin: string, event: ServerEvent): void {
} }
} }
} }
};
if (isHome) {
checkMute(useAuthStore.getState().user?.id);
} else {
import('../stores/spaceStore').then(({ getMyUserIdForOrigin }) => {
checkMute(getMyUserIdForOrigin(origin));
});
}
break; break;
} }
case 'voice_server_deafened': { case 'voice_server_deafened': {
const { setServerDeafenedUser } = useVoiceStore.getState(); const { setServerDeafenedUser } = useVoiceStore.getState();
setServerDeafenedUser(event.spaceId, event.userId, event.deafened); setServerDeafenedUser(event.spaceId, event.userId, event.deafened);
const myUid = useAuthStore.getState().user?.id;
const checkDeafen = (myUid: string | undefined) => {
if (event.userId === myUid) { if (event.userId === myUid) {
if (event.deafened) { if (event.deafened) {
// Force-deafen (smart toggle sets both muted+deafened) // Force-deafen (smart toggle sets both muted+deafened)
@@ -395,6 +406,15 @@ function handleEvent(origin: string, event: ServerEvent): void {
} }
} }
} }
};
if (isHome) {
checkDeafen(useAuthStore.getState().user?.id);
} else {
import('../stores/spaceStore').then(({ getMyUserIdForOrigin }) => {
checkDeafen(getMyUserIdForOrigin(origin));
});
}
break; break;
} }
+4 -2
View File
@@ -241,7 +241,8 @@ export const useVoiceStore = create<VoiceState>()(
toggleMic: () => set((state) => { toggleMic: () => set((state) => {
// Server-muted/deafened users cannot unmute themselves // Server-muted/deafened users cannot unmute themselves
const myId = useAuthStore.getState().user?.id; const origin = state.currentVoiceChannelId ? getChannelOrigin(state.currentVoiceChannelId) : '';
const myId = getMyUserIdForOrigin(origin);
const spaceId = state.currentVoiceChannelId ? useSpaceStore.getState().channelToSpaceMap.get(state.currentVoiceChannelId) : null; const spaceId = state.currentVoiceChannelId ? useSpaceStore.getState().channelToSpaceMap.get(state.currentVoiceChannelId) : null;
if (myId && spaceId && state.isMuted && (state.serverMutedUserIds.has(`${spaceId}:${myId}`) || state.serverDeafenedUserIds.has(`${spaceId}:${myId}`))) { if (myId && spaceId && state.isMuted && (state.serverMutedUserIds.has(`${spaceId}:${myId}`) || state.serverDeafenedUserIds.has(`${spaceId}:${myId}`))) {
return {}; return {};
@@ -254,7 +255,8 @@ export const useVoiceStore = create<VoiceState>()(
}), }),
toggleDeafen: () => set((state) => { toggleDeafen: () => set((state) => {
// Server-deafened users cannot undeafen themselves // Server-deafened users cannot undeafen themselves
const myId = useAuthStore.getState().user?.id; const origin = state.currentVoiceChannelId ? getChannelOrigin(state.currentVoiceChannelId) : '';
const myId = getMyUserIdForOrigin(origin);
const spaceId = state.currentVoiceChannelId ? useSpaceStore.getState().channelToSpaceMap.get(state.currentVoiceChannelId) : null; const spaceId = state.currentVoiceChannelId ? useSpaceStore.getState().channelToSpaceMap.get(state.currentVoiceChannelId) : null;
if (myId && spaceId && state.isDeafened && state.serverDeafenedUserIds.has(`${spaceId}:${myId}`)) { if (myId && spaceId && state.isDeafened && state.serverDeafenedUserIds.has(`${spaceId}:${myId}`)) {
return {}; return {};