From e2a3b286ca13624d2d7e0cff4d57652660129cda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Rame=CC=81?= Date: Mon, 18 May 2026 10:59:25 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20(ci)=20try=20to=20pass=20more=20che?= =?UTF-8?q?cks=20with=20sonarqube?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/core/utils.py | 13 +- .../DecryptionFailedTileOverlay.tsx | 8 +- .../features/encryption/RoomStatusBanner.tsx | 8 +- .../home/components/JoinMeetingDialog.tsx | 3 +- .../features/rooms/components/Conference.tsx | 20 +- .../rooms/components/InviteDialog.tsx | 397 +++++++++--------- .../src/features/rooms/components/Join.tsx | 4 +- .../rooms/livekit/components/Admin.tsx | 5 +- .../rooms/livekit/components/Info.tsx | 3 +- .../rooms/livekit/components/Tools.tsx | 3 +- .../Options/ScreenRecordingMenuItem.tsx | 3 +- .../controls/Options/TranscriptMenuItem.tsx | 3 +- .../livekit/hooks/useCopyRoomToClipboard.ts | 6 +- .../sdk/routes/CreateMeetingButton.tsx | 9 +- .../components/EncryptionDefaultField.tsx | 23 +- src/frontend/src/navigation/navigateTo.ts | 5 +- 16 files changed, 273 insertions(+), 240 deletions(-) diff --git a/src/backend/core/utils.py b/src/backend/core/utils.py index a7a41510..4be14318 100644 --- a/src/backend/core/utils.py +++ b/src/backend/core/utils.py @@ -88,17 +88,18 @@ def generate_token( str: The LiveKit JWT access token. """ - if is_admin_or_owner: - sources = settings.LIVEKIT_DEFAULT_SOURCES + # Local import: core.models loads core.utils mid-import (see models.py:28), + # so importing EncryptionMode at module top would deadlock the bootstrap. + from core.models import EncryptionMode # pylint: disable=import-outside-toplevel - if sources is None: + if is_admin_or_owner or sources is None: sources = settings.LIVEKIT_DEFAULT_SOURCES # In encrypted rooms, no one can change their name/attributes after the # admin accepted them — otherwise a participant authenticated under one # identity could rewrite their JWT-presented name to spoof someone else # mid-meeting. In plain rooms, free naming is fine. - can_update_own_metadata = encryption_mode == "none" + can_update_own_metadata = encryption_mode == EncryptionMode.NONE video_grants = VideoGrants( room=room, @@ -136,7 +137,7 @@ def generate_token( # otherwise. if ( not user.is_anonymous - and encryption_mode != "none" + and encryption_mode != EncryptionMode.NONE and getattr(user, "email", None) ): attributes["email"] = user.email @@ -159,7 +160,7 @@ def generate_token( # that moment — no extra round-trip, no race window where a SIP # caller could read empty metadata before the `room_started` webhook # has time to push it. - if encryption_mode != "none": + if encryption_mode != EncryptionMode.NONE: token = token.with_room_config( RoomConfiguration( name=room, diff --git a/src/frontend/src/features/encryption/DecryptionFailedTileOverlay.tsx b/src/frontend/src/features/encryption/DecryptionFailedTileOverlay.tsx index 83127bfb..34bac9de 100644 --- a/src/frontend/src/features/encryption/DecryptionFailedTileOverlay.tsx +++ b/src/frontend/src/features/encryption/DecryptionFailedTileOverlay.tsx @@ -17,6 +17,7 @@ import { css } from '@/styled-system/css' import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData' import { ParticipantPlaceholder } from '@/features/rooms/livekit/components/ParticipantPlaceholder' +import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom' interface Props { participant: Participant @@ -28,7 +29,7 @@ export function DecryptionFailedTileOverlay({ participant }: Props) { }) const room = useRoomContext() const roomData = useRoomData() - const isEncrypted = roomData?.encryption_mode === 'basic' + const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC const [failed, setFailed] = useState(false) @@ -56,8 +57,7 @@ export function DecryptionFailedTileOverlay({ participant }: Props) { if (!failed) return null return ( -
- + ) } diff --git a/src/frontend/src/features/encryption/RoomStatusBanner.tsx b/src/frontend/src/features/encryption/RoomStatusBanner.tsx index 50c6a6fd..d2f6bd42 100644 --- a/src/frontend/src/features/encryption/RoomStatusBanner.tsx +++ b/src/frontend/src/features/encryption/RoomStatusBanner.tsx @@ -20,6 +20,7 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData' import { RecordingMode, useRecordingStatuses } from '@/features/recording' +import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom' const COLLAPSE_DELAY_MS = 4000 @@ -39,10 +40,9 @@ function StatusPill({ icon, label, background, pulse }: PillProps) { }, []) return ( -
setCollapsed(false)} onMouseLeave={() => setCollapsed(true)} - role="status" aria-label={label} className={css({ display: 'inline-flex', @@ -78,7 +78,7 @@ function StatusPill({ icon, label, background, pulse }: PillProps) { > {label} -
+ ) } @@ -95,7 +95,7 @@ export function RoomStatusBanner() { const isRecording = screenRec.isStarted const isTranscribing = transcript.isStarted - const isEncrypted = roomData?.encryption_mode === 'basic' + const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC if (!isEncrypted && !isRecording && !isTranscribing) { return null diff --git a/src/frontend/src/features/home/components/JoinMeetingDialog.tsx b/src/frontend/src/features/home/components/JoinMeetingDialog.tsx index 49199caa..d5e4b0ba 100644 --- a/src/frontend/src/features/home/components/JoinMeetingDialog.tsx +++ b/src/frontend/src/features/home/components/JoinMeetingDialog.tsx @@ -7,6 +7,7 @@ import { isRoomValid } from '@/features/rooms' import { normalizeRoomId } from '@/features/rooms/utils/isRoomValid' import { fetchRoom } from '@/features/rooms/api/fetchRoom' import { isValidPassphrase } from '@/features/encryption' +import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom' export const JoinMeetingDialog = () => { const { t } = useTranslation('home') @@ -39,7 +40,7 @@ export const JoinMeetingDialog = () => { setIsLoading(true) try { const room = await fetchRoom({ roomId: parsed.roomId }) - if (room.encryption_mode === 'basic') { + if (room.encryption_mode === ApiEncryptionMode.BASIC) { setRoomId(parsed.roomId) setStep('passphrase') return diff --git a/src/frontend/src/features/rooms/components/Conference.tsx b/src/frontend/src/features/rooms/components/Conference.tsx index 0d475498..e922f450 100644 --- a/src/frontend/src/features/rooms/components/Conference.tsx +++ b/src/frontend/src/features/rooms/components/Conference.tsx @@ -103,17 +103,19 @@ export const Conference = ({ const hasValidHash = isValidPassphrase(hashPassphrase) const dbSaysEncrypted = data?.encryption_mode === ApiEncryptionMode.BASIC - const encryptionMismatch: + type EncryptionMismatch = | 'missingPassphrase' | 'unexpectedPassphrase' - | null = - data === undefined - ? null - : dbSaysEncrypted && !hasValidHash - ? 'missingPassphrase' - : !dbSaysEncrypted && hashPassphrase.length > 0 - ? 'unexpectedPassphrase' - : null + | null + + let encryptionMismatch: EncryptionMismatch = null + if (data !== undefined) { + if (dbSaysEncrypted && !hasValidHash) { + encryptionMismatch = 'missingPassphrase' + } else if (!dbSaysEncrypted && hashPassphrase.length > 0) { + encryptionMismatch = 'unexpectedPassphrase' + } + } // We treat the room as encrypted purely because we have a valid hash. // No server condition. If the hash is valid we MUST run E2EE; if not, diff --git a/src/frontend/src/features/rooms/components/InviteDialog.tsx b/src/frontend/src/features/rooms/components/InviteDialog.tsx index 6df4d173..78c751c8 100644 --- a/src/frontend/src/features/rooms/components/InviteDialog.tsx +++ b/src/frontend/src/features/rooms/components/InviteDialog.tsx @@ -17,7 +17,7 @@ import { useMemo } from 'react' import { css } from '@/styled-system/css' import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData' import { FeaturePill } from '@/features/encryption' -import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom' +import { ApiAccessLevel, ApiEncryptionMode } from '@/features/rooms/api/ApiRoom' import { useTelephony } from '@/features/rooms/livekit/hooks/useTelephony' import { formatPinCode } from '@/features/rooms/utils/telephony' import { useCopyRoomToClipboard } from '@/features/rooms/livekit/hooks/useCopyRoomToClipboard' @@ -53,7 +53,7 @@ export const InviteDialog = (props: Omit) => { }) const roomData = useRoomData() - const isEncrypted = roomData?.encryption_mode === 'basic' + const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC const isAdminOrOwner = !!roomData?.is_administrable const baseRoomUrl = getRouteUrl('room', roomData?.slug) // Include the hash (passphrase) for basic encrypted rooms so the full link is visible @@ -108,219 +108,230 @@ export const InviteDialog = (props: Omit) => { ) : (

{t('description')}

)} - {isEncrypted && !isAdminOrOwner ? null : isEncrypted ? ( -
-
- - { + if (isEncrypted && !isAdminOrOwner) return null + if (isEncrypted) { + return ( +
- {tHome('warning')} - -
-
- - {roomUrl?.replace(/^https?:\/\//, '')} - - -
- - {t('encryptedDisabledHeading')} - -
- } - label={tFeatures('features.dialIn')} - /> - } - label={tFeatures('features.meetingRoom')} - /> -
-
- ) : isTelephonyReadyForUse ? ( -
-
- - {roomUrl?.replace(/^https?:\/\//, '')} - - {isTelephonyReadyForUse && roomUrl && ( -
+
+ + {roomUrl?.replace(/^https?:\/\//, '')} + + +
+ + {t('encryptedDisabledHeading')} + +
+ } + label={tFeatures('features.dialIn')} + /> + } + label={tFeatures('features.meetingRoom')} + /> +
+
+ ) + } + if (isTelephonyReadyForUse) { + return ( +
+
+ + {roomUrl?.replace(/^https?:\/\//, '')} + + {isTelephonyReadyForUse && roomUrl && ( + + )} +
+
+ + {t('phone.call')} ({telephony?.country}){' '} + {telephony?.internationalPhoneNumber} + + + {t('phone.pinCode')}{' '} + {formatPinCode(roomData?.pin_code)} + +
+ + - )} -
-
- - {t('phone.call')} ({telephony?.country}){' '} - {telephony?.internationalPhoneNumber} - - - {t('phone.pinCode')}{' '} - {formatPinCode(roomData?.pin_code)} - -
- +
+ ) + } + return ( - - ) : ( - - )} + ) + })()} {roomData?.access_level === ApiAccessLevel.PUBLIC && (
{ {t('access.description')} {(() => { - const isEncrypted = readOnlyData?.encryption_mode === 'basic' + const isEncrypted = + readOnlyData?.encryption_mode === ApiEncryptionMode.BASIC return ( <> {isEncrypted && ( diff --git a/src/frontend/src/features/rooms/livekit/components/Info.tsx b/src/frontend/src/features/rooms/livekit/components/Info.tsx index 4a1acfcf..092164eb 100644 --- a/src/frontend/src/features/rooms/livekit/components/Info.tsx +++ b/src/frontend/src/features/rooms/livekit/components/Info.tsx @@ -16,6 +16,7 @@ import { formatPinCode } from '../../utils/telephony' import { useTelephony } from '../hooks/useTelephony' import { useCopyRoomToClipboard } from '../hooks/useCopyRoomToClipboard' import { FeaturePill } from '@/features/encryption' +import { ApiEncryptionMode } from '../../api/ApiRoom' export const Info = () => { const { t } = useTranslation('rooms', { keyPrefix: 'info' }) @@ -30,7 +31,7 @@ export const Info = () => { : baseRoomUrl const telephony = useTelephony() - const isEncrypted = data?.encryption_mode === 'basic' + const isEncrypted = data?.encryption_mode === ApiEncryptionMode.BASIC const isAdminOrOwner = !!data?.is_administrable const isTelephonyReadyForUse = useMemo(() => { diff --git a/src/frontend/src/features/rooms/livekit/components/Tools.tsx b/src/frontend/src/features/rooms/livekit/components/Tools.tsx index 76ce0989..76399787 100644 --- a/src/frontend/src/features/rooms/livekit/components/Tools.tsx +++ b/src/frontend/src/features/rooms/livekit/components/Tools.tsx @@ -14,6 +14,7 @@ import { } from '@/features/recording' import { useConfig } from '@/api/useConfig' import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData' +import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom' export interface ToolsButtonProps { icon: ReactNode @@ -108,7 +109,7 @@ export const Tools = () => { useSidePanel() const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' }) const roomData = useRoomData() - const isEncrypted = roomData?.encryption_mode === 'basic' + const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC // Restore focus to the element that opened the Tools panel useRestoreFocus(isToolsOpen, { diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Options/ScreenRecordingMenuItem.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Options/ScreenRecordingMenuItem.tsx index bbc6e989..f3ce9dfc 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Options/ScreenRecordingMenuItem.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Options/ScreenRecordingMenuItem.tsx @@ -6,13 +6,14 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel' import { RecordingMode, useHasRecordingAccess } from '@/features/recording' import { FeatureFlags } from '@/features/analytics/enums' import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData' +import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom' export const ScreenRecordingMenuItem = () => { const { t } = useTranslation('rooms', { keyPrefix: 'options.items' }) const { isScreenRecordingOpen, openScreenRecording, toggleTools } = useSidePanel() const roomData = useRoomData() - const isEncrypted = roomData?.encryption_mode === 'basic' + const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC const hasScreenRecordingAccess = useHasRecordingAccess( RecordingMode.ScreenRecording, diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Options/TranscriptMenuItem.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Options/TranscriptMenuItem.tsx index c9d276b5..f744cb49 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Options/TranscriptMenuItem.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Options/TranscriptMenuItem.tsx @@ -6,12 +6,13 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel' import { RecordingMode, useHasRecordingAccess } from '@/features/recording' import { FeatureFlags } from '@/features/analytics/enums' import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData' +import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom' export const TranscriptMenuItem = () => { const { t } = useTranslation('rooms', { keyPrefix: 'options.items' }) const { isTranscriptOpen, openTranscript, toggleTools } = useSidePanel() const roomData = useRoomData() - const isEncrypted = roomData?.encryption_mode === 'basic' + const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC const hasTranscriptAccess = useHasRecordingAccess( RecordingMode.Transcript, diff --git a/src/frontend/src/features/rooms/livekit/hooks/useCopyRoomToClipboard.ts b/src/frontend/src/features/rooms/livekit/hooks/useCopyRoomToClipboard.ts index cedf92b5..26f92533 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/useCopyRoomToClipboard.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/useCopyRoomToClipboard.ts @@ -2,7 +2,7 @@ import { useTelephony } from './useTelephony' import { useTranslation } from 'react-i18next' import { useEffect, useMemo, useState } from 'react' import { formatPinCode } from '@/features/rooms/utils/telephony' -import { ApiRoom } from '@/features/rooms/api/ApiRoom' +import { ApiRoom, ApiEncryptionMode } from '@/features/rooms/api/ApiRoom' import { getRouteUrl } from '@/navigation/getRouteUrl' const COPY_SUCCESS_TIMEOUT = 3000 @@ -47,7 +47,9 @@ export const useCopyRoomToClipboard = ( // so make sure we don't paste a non-functional phone+PIN snippet either. const hasTelephonyInfo = useMemo(() => { return ( - telephony.enabled && room?.pin_code && room?.encryption_mode !== 'basic' + telephony.enabled && + room?.pin_code && + room?.encryption_mode !== ApiEncryptionMode.BASIC ) }, [telephony.enabled, room?.pin_code, room?.encryption_mode]) diff --git a/src/frontend/src/features/sdk/routes/CreateMeetingButton.tsx b/src/frontend/src/features/sdk/routes/CreateMeetingButton.tsx index 27f3405a..4bec83a5 100644 --- a/src/frontend/src/features/sdk/routes/CreateMeetingButton.tsx +++ b/src/frontend/src/features/sdk/routes/CreateMeetingButton.tsx @@ -77,10 +77,17 @@ export const CreateMeetingButton = () => { // Communicate iframe height to parent for proper sizing useEffect(() => { + const targetOrigin = (() => { + try { + return document.referrer ? new URL(document.referrer).origin : '/' + } catch { + return '/' + } + })() const observer = new ResizeObserver(() => { window.parent.postMessage( { type: 'RESIZE', data: { height: document.body.scrollHeight } }, - '*' + targetOrigin ) }) observer.observe(document.body) diff --git a/src/frontend/src/features/settings/components/EncryptionDefaultField.tsx b/src/frontend/src/features/settings/components/EncryptionDefaultField.tsx index 94ffed9a..67d1c5c3 100644 --- a/src/frontend/src/features/settings/components/EncryptionDefaultField.tsx +++ b/src/frontend/src/features/settings/components/EncryptionDefaultField.tsx @@ -30,16 +30,21 @@ export const EncryptionDefaultField = () => { const isOn = user?.default_encryption_mode === ApiEncryptionMode.BASIC - const handleToggle = (next: boolean) => { + const handleToggle = async (next: boolean) => { if (!user) return - void mutateAsync({ - user: { - id: user.id, - default_encryption_mode: next - ? ApiEncryptionMode.BASIC - : ApiEncryptionMode.NONE, - }, - }) + try { + await mutateAsync({ + user: { + id: user.id, + default_encryption_mode: next + ? ApiEncryptionMode.BASIC + : ApiEncryptionMode.NONE, + }, + }) + } catch { + // react-query stores the error on the mutation; the UI shows + // isPending while in flight and reverts the toggle on failure. + } } if (!isLoggedIn) { diff --git a/src/frontend/src/navigation/navigateTo.ts b/src/frontend/src/navigation/navigateTo.ts index b191940e..94369225 100644 --- a/src/frontend/src/navigation/navigateTo.ts +++ b/src/frontend/src/navigation/navigateTo.ts @@ -20,8 +20,7 @@ export const navigateTo = ( // Including the hash in the URL passed to `navigate` lets us avoid a // brittle pushState + replaceState dance at the call site: the URL the // app first renders already carries the fragment. - const target = options?.hash ? `${to}#${options.hash}` : to - const { hash: _hash, ...navigateOptions } = options ?? {} - void _hash + const { hash, ...navigateOptions } = options ?? {} + const target = hash ? `${to}#${hash}` : to return navigate(target, navigateOptions) }