♻️(frontend) replace direct LiveKit calls with backend API endpoints

Refactor frontend to use backend-mediated API calls instead of direct
LiveKit client-side requests for participant management operations.

Removes hacky direct LiveKit API usage in favor of proper server-side
endpoints, improving security posture and following LiveKit's
recommended architecture for participant control functionality.
This commit is contained in:
lebaudantoine
2025-08-26 22:43:28 +02:00
committed by aleb_the_flash
parent 5f70840398
commit 1539613bf8
10 changed files with 43 additions and 89 deletions

View File

@@ -0,0 +1,27 @@
import { Participant } from 'livekit-client'
import { fetchApi } from '@/api/fetchApi.ts'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
export const useLowerHandParticipant = () => {
const data = useRoomData()
const lowerHandParticipant = async (participant: Participant) => {
if (!data?.id) {
throw new Error('Room id is not available')
}
const newAttributes = {
...participant.attributes,
handRaisedAt: '',
}
return await fetchApi(`rooms/${data.id}/update-participant/`, {
method: 'POST',
body: JSON.stringify({
participant_identity: participant.identity,
attributes: newAttributes,
}),
})
}
return { lowerHandParticipant }
}

View File

@@ -1,5 +1,5 @@
import { Participant } from 'livekit-client'
import { useLowerHandParticipant } from '@/features/rooms/livekit/api/lowerHandParticipant'
import { useLowerHandParticipant } from './lowerHandParticipant'
export const useLowerHandParticipants = () => {
const { lowerHandParticipant } = useLowerHandParticipant()

View File

@@ -1,12 +1,11 @@
import { Participant, Track } from 'livekit-client'
import Source = Track.Source
import { fetchServerApi } from './fetchServerApi'
import { buildServerApiUrl } from './buildServerApiUrl'
import { useRoomData } from '../hooks/useRoomData'
import { useRoomData } from '../livekit/hooks/useRoomData'
import {
useNotifyParticipants,
NotificationType,
} from '@/features/notifications'
import { fetchApi } from '@/api/fetchApi'
export const useMuteParticipant = () => {
const data = useRoomData()
@@ -14,8 +13,8 @@ export const useMuteParticipant = () => {
const { notifyParticipants } = useNotifyParticipants()
const muteParticipant = async (participant: Participant) => {
if (!data || !data?.livekit) {
throw new Error('Room data is not available')
if (!data?.id) {
throw new Error('Room id is not available')
}
const trackSid = participant.getTrackPublication(
Source.Microphone
@@ -26,22 +25,13 @@ export const useMuteParticipant = () => {
}
try {
const response = await fetchServerApi(
buildServerApiUrl(
data.livekit.url,
'twirp/livekit.RoomService/MutePublishedTrack'
),
data.livekit.token,
{
method: 'POST',
body: JSON.stringify({
room: data.livekit.room,
identity: participant.identity,
muted: true,
track_sid: trackSid,
}),
}
)
const response = await fetchApi(`rooms/${data.id}/mute-participant/`, {
method: 'POST',
body: JSON.stringify({
participant_identity: participant.identity,
track_sid: trackSid,
}),
})
await notifyParticipants({
type: NotificationType.ParticipantMuted,

View File

@@ -1,5 +0,0 @@
export const buildServerApiUrl = (origin: string, path: string) => {
const sanitizedOrigin = origin.replace(/\/$/, '')
const sanitizedPath = path.replace(/^\//, '')
return `${sanitizedOrigin}/${sanitizedPath}`
}

View File

@@ -1,21 +0,0 @@
import { ApiError } from '@/api/ApiError'
export const fetchServerApi = async <T = Record<string, unknown>>(
url: string,
token: string,
options?: RequestInit
): Promise<T> => {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
...options?.headers,
},
})
const result = await response.json()
if (!response.ok) {
throw new ApiError(response.status, result)
}
return result
}

View File

@@ -1,37 +0,0 @@
import { Participant } from 'livekit-client'
import { fetchServerApi } from './fetchServerApi'
import { buildServerApiUrl } from './buildServerApiUrl'
import { useRoomData } from '../hooks/useRoomData'
export const useLowerHandParticipant = () => {
const data = useRoomData()
const lowerHandParticipant = (participant: Participant) => {
if (!data || !data?.livekit) {
throw new Error('Room data is not available')
}
const newAttributes = {
...participant.attributes,
handRaisedAt: '',
}
return fetchServerApi(
buildServerApiUrl(
data.livekit.url,
'twirp/livekit.RoomService/UpdateParticipant'
),
data.livekit.token,
{
method: 'POST',
body: JSON.stringify({
room: data.livekit.room,
identity: participant.identity,
attributes: newAttributes,
permission: participant.permissions,
}),
}
)
}
return { lowerHandParticipant }
}

View File

@@ -20,7 +20,7 @@ import { useSidePanel } from '../hooks/useSidePanel'
import { useFullScreen } from '../hooks/useFullScreen'
import { Participant, Track } from 'livekit-client'
import { MuteAlertDialog } from './MuteAlertDialog'
import { useMuteParticipant } from '../api/muteParticipant'
import { useMuteParticipant } from '@/features/rooms/api/muteParticipant'
const ZoomButton = ({
trackRef,

View File

@@ -4,11 +4,11 @@ import { HStack } from '@/styled-system/jsx'
import { Text } from '@/primitives/Text'
import { useTranslation } from 'react-i18next'
import { Avatar } from '@/components/Avatar'
import { useLowerHandParticipant } from '@/features/rooms/api/lowerHandParticipant'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
import { Participant } from 'livekit-client'
import { isLocal } from '@/utils/livekit'
import { RiHand } from '@remixicon/react'
import { useLowerHandParticipant } from '@/features/rooms/livekit/api/lowerHandParticipant.ts'
import { Button } from '@/primitives'
type HandRaisedListItemProps = {

View File

@@ -1,7 +1,7 @@
import { Button } from '@/primitives'
import { useLowerHandParticipants } from '@/features/rooms/livekit/api/lowerHandParticipants'
import { useTranslation } from 'react-i18next'
import { Participant } from 'livekit-client'
import { useLowerHandParticipants } from '@/features/rooms/api/lowerHandParticipants'
type LowerAllHandsButtonProps = {
participants: Array<Participant>

View File

@@ -15,8 +15,8 @@ import Source = Track.Source
import { RiMicFill, RiMicOffFill } from '@remixicon/react'
import { Button } from '@/primitives'
import { useState } from 'react'
import { useMuteParticipant } from '@/features/rooms/livekit/api/muteParticipant'
import { MuteAlertDialog } from '../../MuteAlertDialog'
import { useMuteParticipant } from '@/features/rooms/api/muteParticipant'
type MicIndicatorProps = {
participant: Participant