Compare commits

...

2 Commits

Author SHA1 Message Date
lebaudantoine b29e04d635 (frontend) allow unauthenticated participants to mute via LiveKit token
Pass the LiveKit token when calling the mute-participant endpoint
to authenticate the request.

This enables non-authenticated participants to mute others through
the API while preserving proper authorization checks.
2026-03-24 19:07:55 +01:00
lebaudantoine 9ff6177004 🛂(backend) allow participants to mute others based on room configuration
Enable any participant to mute others when the room configuration
allows it. This is enabled by default for all meetings unless
explicitly disabled by an administrator.

Privileged users retain the ability to mute any participant
regardless of the room configuration.
2026-03-24 18:59:29 +01:00
3 changed files with 56 additions and 11 deletions
+32
View File
@@ -136,3 +136,35 @@ class FilePermission(IsAuthenticated):
raise Http404
return obj.get_abilities(request.user).get(view.action, False)
class CanMuteParticipant(permissions.BasePermission):
"""
Grant muting rights based on role or room configuration.
Allows muting unconditionally for admins/owners. For other users, requires
both the room's ``everyone_can_mute`` option to be enabled and a valid
authentication (session/JWT or a LiveKit token matching the room ID).
"""
def has_object_permission(self, request, view, obj):
"""Check if the requesting user is allowed to mute a participant in the given room."""
# Always allow admins/owners
if obj.is_administrator_or_owner(request.user):
return True
everyone_can_mute = obj.configuration.get("everyone_can_mute", True)
if not everyone_can_mute:
return False
# Standard backend auth (e.g. session/JWT) — no LiveKit token needed
if request.user and request.user.is_authenticated:
return True
# LiveKit token auth
if request.auth and hasattr(request.auth, "video"):
return request.auth.video.room == str(obj.id)
return False
+7 -1
View File
@@ -30,6 +30,7 @@ from rest_framework import (
from rest_framework import (
status as drf_status,
)
from rest_framework.settings import api_settings
from core import enums, models, utils
from core.api.filters import ListFileFilter
@@ -583,7 +584,12 @@ class RoomViewSet(
methods=["post"],
url_path="mute-participant",
url_name="mute-participant",
permission_classes=[permissions.HasPrivilegesOnRoom],
permission_classes=[permissions.CanMuteParticipant],
# Accept both session auth (privileged users) and LiveKit token auth (participants)
authentication_classes=[
*api_settings.DEFAULT_AUTHENTICATION_CLASSES,
LiveKitTokenAuthentication,
],
)
def mute_participant(self, request, pk=None): # pylint: disable=unused-argument
"""Mute a specific track for a participant in the room."""
@@ -7,13 +7,13 @@ import {
} from '@/features/notifications'
import { fetchApi } from '@/api/fetchApi'
export const useMuteParticipant = () => {
const data = useRoomData()
export const useMuteParticipant = () => {
const apiRoomData = useRoomData()
const { notifyParticipants } = useNotifyParticipants()
const muteParticipant = async (participant: Participant) => {
if (!data?.id) {
if (!apiRoomData?.livekit?.room) {
throw new Error('Room id is not available')
}
const trackSid = participant.getTrackPublication(
@@ -25,24 +25,31 @@ export const useMuteParticipant = () => {
}
try {
const response = await fetchApi(`rooms/${data.id}/mute-participant/`, {
const response = fetchApi(`rooms/${apiRoomData?.livekit?.room}/mute-participant/`, {
method: 'POST',
body: JSON.stringify({
token: apiRoomData?.livekit?.token,
participant_identity: participant.identity,
track_sid: trackSid,
}),
})
await notifyParticipants({
type: NotificationType.ParticipantMuted,
destinationIdentities: [participant.identity],
})
return response
} catch (error) {
console.error(
`Failed to mute participant ${participant.identity}: ${error instanceof Error ? error.message : 'Unknown error'}`
)
return
}
try {
await notifyParticipants({
type: NotificationType.ParticipantMuted,
destinationIdentities: [participant.identity],
})
} catch (e) {
console.error(
`Failed to notify muted participant ${participant.identity}: ${e}`
)
}
}
return { muteParticipant }