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:
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -319,27 +319,37 @@ 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;
|
|
||||||
if (event.userId === myUserId) {
|
const checkMute = (myUserId: string | undefined) => {
|
||||||
if (event.muted) {
|
if (event.userId === myUserId) {
|
||||||
// Force-mute the mic
|
if (event.muted) {
|
||||||
const vs = useVoiceStore.getState();
|
// Force-mute the mic
|
||||||
if (!vs.isMuted) {
|
const vs = useVoiceStore.getState();
|
||||||
useVoiceStore.setState({ isMuted: true });
|
if (!vs.isMuted) {
|
||||||
const fresh = useVoiceStore.getState();
|
useVoiceStore.setState({ isMuted: true });
|
||||||
const voiceOrigin = fresh.currentVoiceChannelId ? getChannelOrigin(fresh.currentVoiceChannelId) : '';
|
const fresh = useVoiceStore.getState();
|
||||||
wsSend({ type: 'voice_status', isMuted: true, isDeafened: fresh.isDeafened, isCameraOn: fresh.isCameraOn, isScreenSharing: fresh.isScreenSharing }, voiceOrigin);
|
const voiceOrigin = fresh.currentVoiceChannelId ? getChannelOrigin(fresh.currentVoiceChannelId) : '';
|
||||||
}
|
wsSend({ type: 'voice_status', isMuted: true, isDeafened: fresh.isDeafened, isCameraOn: fresh.isCameraOn, isScreenSharing: fresh.isScreenSharing }, voiceOrigin);
|
||||||
} else {
|
}
|
||||||
// Server unmuted — auto-restore mic unless still server-deafened
|
} else {
|
||||||
const vs = useVoiceStore.getState();
|
// Server unmuted — auto-restore mic unless still server-deafened
|
||||||
if (!vs.serverDeafenedUserIds.has(`${event.spaceId}:${myUserId}`) && vs.isMuted) {
|
const vs = useVoiceStore.getState();
|
||||||
useVoiceStore.setState({ isMuted: false });
|
if (!vs.serverDeafenedUserIds.has(`${event.spaceId}:${myUserId}`) && vs.isMuted) {
|
||||||
const fresh = useVoiceStore.getState();
|
useVoiceStore.setState({ isMuted: false });
|
||||||
const voiceOrigin = fresh.currentVoiceChannelId ? getChannelOrigin(fresh.currentVoiceChannelId) : '';
|
const fresh = useVoiceStore.getState();
|
||||||
wsSend({ type: 'voice_status', isMuted: false, isDeafened: fresh.isDeafened, isCameraOn: fresh.isCameraOn, isScreenSharing: fresh.isScreenSharing }, voiceOrigin);
|
const voiceOrigin = fresh.currentVoiceChannelId ? getChannelOrigin(fresh.currentVoiceChannelId) : '';
|
||||||
|
wsSend({ type: 'voice_status', isMuted: false, isDeafened: fresh.isDeafened, isCameraOn: fresh.isCameraOn, isScreenSharing: fresh.isScreenSharing }, voiceOrigin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isHome) {
|
||||||
|
checkMute(useAuthStore.getState().user?.id);
|
||||||
|
} else {
|
||||||
|
import('../stores/spaceStore').then(({ getMyUserIdForOrigin }) => {
|
||||||
|
checkMute(getMyUserIdForOrigin(origin));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -347,53 +357,63 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
|||||||
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;
|
|
||||||
if (event.userId === myUid) {
|
const checkDeafen = (myUid: string | undefined) => {
|
||||||
if (event.deafened) {
|
if (event.userId === myUid) {
|
||||||
// Force-deafen (smart toggle sets both muted+deafened)
|
if (event.deafened) {
|
||||||
const vs = useVoiceStore.getState();
|
// Force-deafen (smart toggle sets both muted+deafened)
|
||||||
if (!vs.isDeafened) {
|
const vs = useVoiceStore.getState();
|
||||||
useVoiceStore.setState({ isMuted: true, isDeafened: true });
|
if (!vs.isDeafened) {
|
||||||
const fresh = useVoiceStore.getState();
|
useVoiceStore.setState({ isMuted: true, isDeafened: true });
|
||||||
const voiceOrigin = fresh.currentVoiceChannelId ? getChannelOrigin(fresh.currentVoiceChannelId) : '';
|
const fresh = useVoiceStore.getState();
|
||||||
wsSend({ type: 'voice_status', isMuted: true, isDeafened: true, isCameraOn: fresh.isCameraOn, isScreenSharing: fresh.isScreenSharing }, voiceOrigin);
|
const voiceOrigin = fresh.currentVoiceChannelId ? getChannelOrigin(fresh.currentVoiceChannelId) : '';
|
||||||
// Broadcast deafen to in-room participants via LiveKit data channel
|
wsSend({ type: 'voice_status', isMuted: true, isDeafened: true, isCameraOn: fresh.isCameraOn, isScreenSharing: fresh.isScreenSharing }, voiceOrigin);
|
||||||
import('./useLiveKit').then(({ getActiveRoom }) => {
|
// Broadcast deafen to in-room participants via LiveKit data channel
|
||||||
const room = getActiveRoom();
|
import('./useLiveKit').then(({ getActiveRoom }) => {
|
||||||
if (room) {
|
const room = getActiveRoom();
|
||||||
const encoder = new TextEncoder();
|
if (room) {
|
||||||
room.localParticipant.publishData(
|
const encoder = new TextEncoder();
|
||||||
encoder.encode(JSON.stringify({ type: 'deafen', deafened: true })),
|
room.localParticipant.publishData(
|
||||||
{ reliable: true }
|
encoder.encode(JSON.stringify({ type: 'deafen', deafened: true })),
|
||||||
).catch(() => {});
|
{ reliable: true }
|
||||||
}
|
).catch(() => {});
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
} else {
|
}
|
||||||
// Server un-deafened — auto-restore
|
} else {
|
||||||
const vs = useVoiceStore.getState();
|
// Server un-deafened — auto-restore
|
||||||
if (vs.isDeafened) {
|
const vs = useVoiceStore.getState();
|
||||||
const stillServerMuted = vs.serverMutedUserIds.has(`${event.spaceId}:${myUid}`);
|
if (vs.isDeafened) {
|
||||||
useVoiceStore.setState({
|
const stillServerMuted = vs.serverMutedUserIds.has(`${event.spaceId}:${myUid}`);
|
||||||
isDeafened: false,
|
useVoiceStore.setState({
|
||||||
...(stillServerMuted ? {} : { isMuted: false }),
|
isDeafened: false,
|
||||||
});
|
...(stillServerMuted ? {} : { isMuted: false }),
|
||||||
const fresh = useVoiceStore.getState();
|
});
|
||||||
const voiceOrigin = fresh.currentVoiceChannelId ? getChannelOrigin(fresh.currentVoiceChannelId) : '';
|
const fresh = useVoiceStore.getState();
|
||||||
wsSend({ type: 'voice_status', isMuted: fresh.isMuted, isDeafened: false, isCameraOn: fresh.isCameraOn, isScreenSharing: fresh.isScreenSharing }, voiceOrigin);
|
const voiceOrigin = fresh.currentVoiceChannelId ? getChannelOrigin(fresh.currentVoiceChannelId) : '';
|
||||||
// Broadcast undeafen via LiveKit data channel
|
wsSend({ type: 'voice_status', isMuted: fresh.isMuted, isDeafened: false, isCameraOn: fresh.isCameraOn, isScreenSharing: fresh.isScreenSharing }, voiceOrigin);
|
||||||
import('./useLiveKit').then(({ getActiveRoom }) => {
|
// Broadcast undeafen via LiveKit data channel
|
||||||
const room = getActiveRoom();
|
import('./useLiveKit').then(({ getActiveRoom }) => {
|
||||||
if (room) {
|
const room = getActiveRoom();
|
||||||
const encoder = new TextEncoder();
|
if (room) {
|
||||||
room.localParticipant.publishData(
|
const encoder = new TextEncoder();
|
||||||
encoder.encode(JSON.stringify({ type: 'deafen', deafened: false })),
|
room.localParticipant.publishData(
|
||||||
{ reliable: true }
|
encoder.encode(JSON.stringify({ type: 'deafen', deafened: false })),
|
||||||
).catch(() => {});
|
{ reliable: true }
|
||||||
}
|
).catch(() => {});
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isHome) {
|
||||||
|
checkDeafen(useAuthStore.getState().user?.id);
|
||||||
|
} else {
|
||||||
|
import('../stores/spaceStore').then(({ getMyUserIdForOrigin }) => {
|
||||||
|
checkDeafen(getMyUserIdForOrigin(origin));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {};
|
||||||
|
|||||||
Reference in New Issue
Block a user