From 97a07ba74d885109ea9e91380cc19c4dda04504b Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Wed, 29 Jul 2026 18:58:17 +0200 Subject: [PATCH] wip allow authenticated user to launch recording/transcription --- src/backend/core/api/permissions.py | 34 +++++++++++++++++++ src/backend/core/api/serializers.py | 1 + src/backend/core/api/viewsets.py | 33 ++++++++++++++---- .../recording/components/NoAccessView.tsx | 1 + .../components/ScreenRecordingSidePanel.tsx | 4 ++- .../components/TranscriptSidePanel.tsx | 4 +-- .../features/recording/hooks/useCanRecord.ts | 15 ++++++++ ...=> useHasFeatureWithoutRecordingRights.ts} | 8 ++--- .../recording/hooks/useHasRecordingAccess.ts | 6 ++-- src/frontend/src/features/recording/index.ts | 2 +- .../src/features/rooms/api/ApiRoom.ts | 1 + .../rooms/livekit/components/Admin.tsx | 18 +++++++++- .../livekit/hooks/usePermissionsManager.ts | 30 ++++++++++++++++ 13 files changed, 138 insertions(+), 19 deletions(-) create mode 100644 src/frontend/src/features/recording/hooks/useCanRecord.ts rename src/frontend/src/features/recording/hooks/{useHasFeatureWithoutAdminRights.ts => useHasFeatureWithoutRecordingRights.ts} (76%) diff --git a/src/backend/core/api/permissions.py b/src/backend/core/api/permissions.py index c1fb21b2..b0e1c9ba 100644 --- a/src/backend/core/api/permissions.py +++ b/src/backend/core/api/permissions.py @@ -198,3 +198,37 @@ class IsPresentInMeeting(permissions.BasePermission): return False except ParticipantsManagementException: 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" diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index 315f8900..cc29dcf7 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -330,6 +330,7 @@ class RoomConfiguration(BaseModel): can_publish_sources: list[TrackSource] | None = None everyone_can_mute: bool | None = None + authenticated_can_record: bool | None = None model_config = {"extra": "forbid"} diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 5f1ad94f..5e7dfb80 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -369,7 +369,7 @@ class RoomViewSet( methods=["post"], url_path="start-recording", permission_classes=[ - permissions.HasPrivilegesOnRoom, + permissions.CanRecord, ], ) @FeatureFlag.require("recording") @@ -394,11 +394,30 @@ class RoomViewSet( mode=mode, options=options.model_dump(exclude_none=True) if options else {}, ) - models.RecordingAccess.objects.create( - user=self.request.user, - role=models.RoleChoices.OWNER, - recording=recording, - ) + + if room.is_administrator_or_owner(request.user): + models.RecordingAccess.objects.create( + 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): # DjangoValidationError covers the Python-level check (full_clean); @@ -442,7 +461,7 @@ class RoomViewSet( methods=["post"], url_path="stop-recording", permission_classes=[ - permissions.HasPrivilegesOnRoom, + permissions.CanRecord, ], ) @FeatureFlag.require("recording") diff --git a/src/frontend/src/features/recording/components/NoAccessView.tsx b/src/frontend/src/features/recording/components/NoAccessView.tsx index 76ed02ce..c0d42160 100644 --- a/src/frontend/src/features/recording/components/NoAccessView.tsx +++ b/src/frontend/src/features/recording/components/NoAccessView.tsx @@ -98,6 +98,7 @@ export const NoAccessView = ({ )} + {/*todo - update the message here to adapt to room configuration */} {!isLoggedIn && ( { const { data } = useConfig() @@ -38,6 +39,7 @@ export const ScreenRecordingSidePanel = () => { const [includeTranscript, setIncludeTranscript] = useState(false) + const canRecord = useCanRecord() const isAdminOrOwner = useIsAdminOrOwner() const hasScreenRecordingAccess = useHasRecordingAccess( @@ -110,7 +112,7 @@ export const ScreenRecordingSidePanel = () => { } } - if (!isAdminOrOwner) { + if (!canRecord) { return ( { FeatureFlags.Transcript ) - const hasFeatureWithoutAdminRights = useHasFeatureWithoutAdminRights( + const hasFeatureWithoutAdminRights = useHasFeatureWithoutRecordingRights( RecordingMode.Transcript, FeatureFlags.Transcript ) diff --git a/src/frontend/src/features/recording/hooks/useCanRecord.ts b/src/frontend/src/features/recording/hooks/useCanRecord.ts new file mode 100644 index 00000000..699c005f --- /dev/null +++ b/src/frontend/src/features/recording/hooks/useCanRecord.ts @@ -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) + ) +} diff --git a/src/frontend/src/features/recording/hooks/useHasFeatureWithoutAdminRights.ts b/src/frontend/src/features/recording/hooks/useHasFeatureWithoutRecordingRights.ts similarity index 76% rename from src/frontend/src/features/recording/hooks/useHasFeatureWithoutAdminRights.ts rename to src/frontend/src/features/recording/hooks/useHasFeatureWithoutRecordingRights.ts index 290626ff..f022fea5 100644 --- a/src/frontend/src/features/recording/hooks/useHasFeatureWithoutAdminRights.ts +++ b/src/frontend/src/features/recording/hooks/useHasFeatureWithoutRecordingRights.ts @@ -2,21 +2,21 @@ import { useFeatureFlagEnabled } from 'posthog-js/react' import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled' import type { RecordingMode } from '../types' import { useIsRecordingModeEnabled } from './useIsRecordingModeEnabled' -import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner' +import { useCanRecord } from './useCanRecord' import type { FeatureFlags } from '@/features/analytics/enums' -export const useHasFeatureWithoutAdminRights = ( +export const useHasFeatureWithoutRecordingRights = ( mode: RecordingMode, featureFlag: FeatureFlags ) => { const featureEnabled = useFeatureFlagEnabled(featureFlag) const isAnalyticsEnabled = useIsAnalyticsEnabled() const isRecordingModeEnabled = useIsRecordingModeEnabled(mode) - const isAdminOrOwner = useIsAdminOrOwner() + const canRecord = useCanRecord() return ( (featureEnabled || !isAnalyticsEnabled) && isRecordingModeEnabled && - !isAdminOrOwner + !canRecord ) } diff --git a/src/frontend/src/features/recording/hooks/useHasRecordingAccess.ts b/src/frontend/src/features/recording/hooks/useHasRecordingAccess.ts index 896b8345..03f02ea5 100644 --- a/src/frontend/src/features/recording/hooks/useHasRecordingAccess.ts +++ b/src/frontend/src/features/recording/hooks/useHasRecordingAccess.ts @@ -2,8 +2,8 @@ import { useFeatureFlagEnabled } from 'posthog-js/react' import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled' import type { RecordingMode } from '../types' import { useIsRecordingModeEnabled } from './useIsRecordingModeEnabled' -import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner' import type { FeatureFlags } from '@/features/analytics/enums' +import { useCanRecord } from './useCanRecord' export const useHasRecordingAccess = ( mode: RecordingMode, @@ -12,11 +12,11 @@ export const useHasRecordingAccess = ( const featureEnabled = useFeatureFlagEnabled(featureFlag) const isAnalyticsEnabled = useIsAnalyticsEnabled() const isRecordingModeEnabled = useIsRecordingModeEnabled(mode) - const isAdminOrOwner = useIsAdminOrOwner() + const canRecord = useCanRecord() return ( (featureEnabled || !isAnalyticsEnabled) && - isAdminOrOwner && + canRecord && isRecordingModeEnabled ) } diff --git a/src/frontend/src/features/recording/index.ts b/src/frontend/src/features/recording/index.ts index e9887a36..19b27b2b 100644 --- a/src/frontend/src/features/recording/index.ts +++ b/src/frontend/src/features/recording/index.ts @@ -1,7 +1,7 @@ // hooks export { useIsRecordingModeEnabled } from './hooks/useIsRecordingModeEnabled' export { useHasRecordingAccess } from './hooks/useHasRecordingAccess' -export { useHasFeatureWithoutAdminRights } from './hooks/useHasFeatureWithoutAdminRights' +export { useHasFeatureWithoutRecordingRights } from './hooks/useHasFeatureWithoutRecordingRights.ts' export { useRecordingStatuses } from './hooks/useRecordingStatuses' // api diff --git a/src/frontend/src/features/rooms/api/ApiRoom.ts b/src/frontend/src/features/rooms/api/ApiRoom.ts index 7c8fde65..e9798f1e 100644 --- a/src/frontend/src/features/rooms/api/ApiRoom.ts +++ b/src/frontend/src/features/rooms/api/ApiRoom.ts @@ -16,6 +16,7 @@ export enum ApiAccessLevel { export type RoomConfiguration = { can_publish_sources?: Source[] | null everyone_can_mute?: boolean | null + authenticated_can_record?: boolean | null } export type ApiRoom = { diff --git a/src/frontend/src/features/rooms/livekit/components/Admin.tsx b/src/frontend/src/features/rooms/livekit/components/Admin.tsx index 8942de3b..a17d996d 100644 --- a/src/frontend/src/features/rooms/livekit/components/Admin.tsx +++ b/src/frontend/src/features/rooms/livekit/components/Admin.tsx @@ -50,7 +50,12 @@ export const Admin = () => { isScreenShareEnabled, } = usePublishSourcesManager() - const { toggleMuting, isMutingEnabled } = usePermissionsManager() + const { + toggleMuting, + isMutingEnabled, + isAuthenticatedCanRecordEnabled, + toggleAuthenticatedCanRecord, + } = usePermissionsManager() return (
{ fullWidth: true, }} /> +
{ const roomId = data?.slug const isMutingEnabled = configuration?.everyone_can_mute ?? true + const isAuthenticatedCanRecordEnabled = + configuration?.authenticated_can_record ?? true const toggleMuting = useCallback( async (enabled: boolean) => { @@ -39,8 +41,36 @@ export const usePermissionsManager = () => { [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 { toggleMuting, isMutingEnabled, + isAuthenticatedCanRecordEnabled, + toggleAuthenticatedCanRecord, } }