mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-17 05:57:48 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8972c8dbb8 |
@@ -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"
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
+4
-4
@@ -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,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user