Compare commits

...

1 Commits

Author SHA1 Message Date
lebaudantoine 8972c8dbb8 wip allow authenticated user to launch recording/transcription 2026-08-05 17:27:32 +02:00
13 changed files with 138 additions and 19 deletions
+34
View File
@@ -198,3 +198,37 @@ class IsPresentInMeeting(permissions.BasePermission):
return False return False
except ParticipantsManagementException: except ParticipantsManagementException:
return False return False
class RoomCapabilityPermission(permissions.BasePermission):
"""Wip."""
capability = None
def has_object_permission(self, request, view, obj):
"""Wip."""
user = request.user
if not user or not user.is_authenticated:
return False
if obj.is_administrator_or_owner(user):
return True
if not obj.configuration.get(self.capability, True):
return False
try:
return ParticipantsManagement().check_if_in_meeting(
room_name=str(obj.pk), identity=str(user.sub)
)
except ParticipantNotFoundException:
return False
except ParticipantsManagementException:
return False
class CanRecord(RoomCapabilityPermission):
"""Wip."""
capability = "authenticated_can_record"
+1
View File
@@ -349,6 +349,7 @@ class RoomConfiguration(BaseModel):
can_publish_sources: list[TrackSource] | None = None can_publish_sources: list[TrackSource] | None = None
everyone_can_mute: bool | None = None everyone_can_mute: bool | None = None
authenticated_can_record: bool | None = None
model_config = {"extra": "forbid"} model_config = {"extra": "forbid"}
+26 -7
View File
@@ -396,7 +396,7 @@ class RoomViewSet(
methods=["post"], methods=["post"],
url_path="start-recording", url_path="start-recording",
permission_classes=[ permission_classes=[
permissions.HasPrivilegesOnRoom, permissions.CanRecord,
], ],
) )
@FeatureFlag.require("recording") @FeatureFlag.require("recording")
@@ -421,11 +421,30 @@ class RoomViewSet(
mode=mode, mode=mode,
options=options.model_dump(exclude_none=True) if options else {}, options=options.model_dump(exclude_none=True) if options else {},
) )
models.RecordingAccess.objects.create(
user=self.request.user, if room.is_administrator_or_owner(request.user):
role=models.RoleChoices.OWNER, models.RecordingAccess.objects.create(
recording=recording, user=self.request.user,
) role=models.RoleChoices.OWNER,
recording=recording,
)
else:
# todo - encapsulate it in a clear method which replicate privileges role on obj
accesses = models.ResourceAccess.objects.filter(
resource=room,
role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN],
).values_list("user_id", "role")
models.RecordingAccess.objects.bulk_create(
[
models.RecordingAccess(
user_id=user_id,
role=role,
recording=recording,
)
for user_id, role in accesses
]
)
except (DjangoValidationError, IntegrityError): except (DjangoValidationError, IntegrityError):
# DjangoValidationError covers the Python-level check (full_clean); # DjangoValidationError covers the Python-level check (full_clean);
@@ -469,7 +488,7 @@ class RoomViewSet(
methods=["post"], methods=["post"],
url_path="stop-recording", url_path="stop-recording",
permission_classes=[ permission_classes=[
permissions.HasPrivilegesOnRoom, permissions.CanRecord,
], ],
) )
@FeatureFlag.require("recording") @FeatureFlag.require("recording")
@@ -98,6 +98,7 @@ export const NoAccessView = ({
)} )}
</Text> </Text>
</VStack> </VStack>
{/*todo - update the message here to adapt to room configuration */}
{!isLoggedIn && ( {!isLoggedIn && (
<LoginPrompt <LoginPrompt
heading={t(`${i18nKey}.login.heading`)} heading={t(`${i18nKey}.login.heading`)}
@@ -29,6 +29,7 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner' import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import { FeatureFlags } from '@/features/analytics/enums' import { FeatureFlags } from '@/features/analytics/enums'
import { LimitDescription } from './LimitDescription' import { LimitDescription } from './LimitDescription'
import { useCanRecord } from '@/features/recording/hooks/useCanRecord'
export const ScreenRecordingSidePanel = () => { export const ScreenRecordingSidePanel = () => {
const { data } = useConfig() const { data } = useConfig()
@@ -38,6 +39,7 @@ export const ScreenRecordingSidePanel = () => {
const [includeTranscript, setIncludeTranscript] = useState(false) const [includeTranscript, setIncludeTranscript] = useState(false)
const canRecord = useCanRecord()
const isAdminOrOwner = useIsAdminOrOwner() const isAdminOrOwner = useIsAdminOrOwner()
const hasScreenRecordingAccess = useHasRecordingAccess( const hasScreenRecordingAccess = useHasRecordingAccess(
@@ -110,7 +112,7 @@ export const ScreenRecordingSidePanel = () => {
} }
} }
if (!isAdminOrOwner) { if (!canRecord) {
return ( return (
<NoAccessView <NoAccessView
i18nKeyPrefix={keyPrefix} i18nKeyPrefix={keyPrefix}
@@ -6,7 +6,7 @@ import { useRoomContext } from '@livekit/components-react'
import { import {
RecordingMode, RecordingMode,
useHasRecordingAccess, useHasRecordingAccess,
useHasFeatureWithoutAdminRights, useHasFeatureWithoutRecordingRights,
useRecordingStatuses, useRecordingStatuses,
} from '../index' } from '../index'
import { useState } from 'react' import { useState } from 'react'
@@ -53,7 +53,7 @@ export const TranscriptSidePanel = () => {
FeatureFlags.Transcript FeatureFlags.Transcript
) )
const hasFeatureWithoutAdminRights = useHasFeatureWithoutAdminRights( const hasFeatureWithoutAdminRights = useHasFeatureWithoutRecordingRights(
RecordingMode.Transcript, RecordingMode.Transcript,
FeatureFlags.Transcript FeatureFlags.Transcript
) )
@@ -0,0 +1,15 @@
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
import { useUser } from '@/features/auth/api/useUser'
export const useCanRecord = () => {
const apiRoomData = useRoomData()
const isAdminOrOwner = useIsAdminOrOwner()
const { isLoggedIn } = useUser()
return (
isAdminOrOwner ||
(!!isLoggedIn &&
apiRoomData?.configuration?.authenticated_can_record !== false)
)
}
@@ -2,21 +2,21 @@ import { useFeatureFlagEnabled } from 'posthog-js/react'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled' import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
import type { RecordingMode } from '../types' import type { RecordingMode } from '../types'
import { useIsRecordingModeEnabled } from './useIsRecordingModeEnabled' import { useIsRecordingModeEnabled } from './useIsRecordingModeEnabled'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner' import { useCanRecord } from './useCanRecord'
import type { FeatureFlags } from '@/features/analytics/enums' import type { FeatureFlags } from '@/features/analytics/enums'
export const useHasFeatureWithoutAdminRights = ( export const useHasFeatureWithoutRecordingRights = (
mode: RecordingMode, mode: RecordingMode,
featureFlag: FeatureFlags featureFlag: FeatureFlags
) => { ) => {
const featureEnabled = useFeatureFlagEnabled(featureFlag) const featureEnabled = useFeatureFlagEnabled(featureFlag)
const isAnalyticsEnabled = useIsAnalyticsEnabled() const isAnalyticsEnabled = useIsAnalyticsEnabled()
const isRecordingModeEnabled = useIsRecordingModeEnabled(mode) const isRecordingModeEnabled = useIsRecordingModeEnabled(mode)
const isAdminOrOwner = useIsAdminOrOwner() const canRecord = useCanRecord()
return ( return (
(featureEnabled || !isAnalyticsEnabled) && (featureEnabled || !isAnalyticsEnabled) &&
isRecordingModeEnabled && isRecordingModeEnabled &&
!isAdminOrOwner !canRecord
) )
} }
@@ -2,8 +2,8 @@ import { useFeatureFlagEnabled } from 'posthog-js/react'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled' import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
import type { RecordingMode } from '../types' import type { RecordingMode } from '../types'
import { useIsRecordingModeEnabled } from './useIsRecordingModeEnabled' import { useIsRecordingModeEnabled } from './useIsRecordingModeEnabled'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import type { FeatureFlags } from '@/features/analytics/enums' import type { FeatureFlags } from '@/features/analytics/enums'
import { useCanRecord } from './useCanRecord'
export const useHasRecordingAccess = ( export const useHasRecordingAccess = (
mode: RecordingMode, mode: RecordingMode,
@@ -12,11 +12,11 @@ export const useHasRecordingAccess = (
const featureEnabled = useFeatureFlagEnabled(featureFlag) const featureEnabled = useFeatureFlagEnabled(featureFlag)
const isAnalyticsEnabled = useIsAnalyticsEnabled() const isAnalyticsEnabled = useIsAnalyticsEnabled()
const isRecordingModeEnabled = useIsRecordingModeEnabled(mode) const isRecordingModeEnabled = useIsRecordingModeEnabled(mode)
const isAdminOrOwner = useIsAdminOrOwner() const canRecord = useCanRecord()
return ( return (
(featureEnabled || !isAnalyticsEnabled) && (featureEnabled || !isAnalyticsEnabled) &&
isAdminOrOwner && canRecord &&
isRecordingModeEnabled isRecordingModeEnabled
) )
} }
+1 -1
View File
@@ -1,7 +1,7 @@
// hooks // hooks
export { useIsRecordingModeEnabled } from './hooks/useIsRecordingModeEnabled' export { useIsRecordingModeEnabled } from './hooks/useIsRecordingModeEnabled'
export { useHasRecordingAccess } from './hooks/useHasRecordingAccess' export { useHasRecordingAccess } from './hooks/useHasRecordingAccess'
export { useHasFeatureWithoutAdminRights } from './hooks/useHasFeatureWithoutAdminRights' export { useHasFeatureWithoutRecordingRights } from './hooks/useHasFeatureWithoutRecordingRights.ts'
export { useRecordingStatuses } from './hooks/useRecordingStatuses' export { useRecordingStatuses } from './hooks/useRecordingStatuses'
// api // api
@@ -16,6 +16,7 @@ export enum ApiAccessLevel {
export type RoomConfiguration = { export type RoomConfiguration = {
can_publish_sources?: Source[] | null can_publish_sources?: Source[] | null
everyone_can_mute?: boolean | null everyone_can_mute?: boolean | null
authenticated_can_record?: boolean | null
} }
export type ParticipantRole = 'member' | 'administrator' | 'owner' export type ParticipantRole = 'member' | 'administrator' | 'owner'
@@ -49,7 +49,12 @@ export const Admin = () => {
isScreenShareEnabled, isScreenShareEnabled,
} = usePublishSourcesManager() } = usePublishSourcesManager()
const { toggleMuting, isMutingEnabled } = usePermissionsManager() const {
toggleMuting,
isMutingEnabled,
isAuthenticatedCanRecordEnabled,
toggleAuthenticatedCanRecord,
} = usePermissionsManager()
return ( return (
<Div <Div
@@ -154,6 +159,17 @@ export const Admin = () => {
fullWidth: true, fullWidth: true,
}} }}
/> />
<Field
type="switch"
label={'Authenticated can record'}
description={'Authenticated can record'}
isSelected={isAuthenticatedCanRecordEnabled}
onChange={toggleAuthenticatedCanRecord}
wrapperProps={{
noMargin: true,
fullWidth: true,
}}
/>
</div> </div>
</div> </div>
<div <div
@@ -10,6 +10,8 @@ export const usePermissionsManager = () => {
const roomId = data?.slug const roomId = data?.slug
const isMutingEnabled = configuration?.everyone_can_mute ?? true const isMutingEnabled = configuration?.everyone_can_mute ?? true
const isAuthenticatedCanRecordEnabled =
configuration?.authenticated_can_record ?? true
const toggleMuting = useCallback( const toggleMuting = useCallback(
async (enabled: boolean) => { async (enabled: boolean) => {
@@ -35,8 +37,36 @@ export const usePermissionsManager = () => {
[configuration, roomId, patchRoom] [configuration, roomId, patchRoom]
) )
const toggleAuthenticatedCanRecord = useCallback(
async (enabled: boolean) => {
if (!roomId) return
try {
const newConfiguration = {
...configuration,
authenticated_can_record: enabled,
}
const room = await patchRoom({
roomId,
room: { configuration: newConfiguration },
})
queryClient.setQueryData([keys.room, roomId], room)
return { configuration: newConfiguration }
} catch (error) {
console.error('Failed to update recording permission:', error)
return { success: false, error }
}
},
[configuration, roomId, patchRoom]
)
return { return {
toggleMuting, toggleMuting,
isMutingEnabled, isMutingEnabled,
isAuthenticatedCanRecordEnabled,
toggleAuthenticatedCanRecord,
} }
} }