diff --git a/meet-ce/backend/openapi/components/responses/error-room-active-meeting.yaml b/meet-ce/backend/openapi/components/responses/error-room-active-meeting.yaml new file mode 100644 index 00000000..a9591d1e --- /dev/null +++ b/meet-ce/backend/openapi/components/responses/error-room-active-meeting.yaml @@ -0,0 +1,8 @@ +description: Room not found +content: + application/json: + schema: + $ref: '../schemas/error.yaml' + example: + error: 'Room Error' + message: 'Room "room_123" has an active meeting' diff --git a/meet-ce/backend/openapi/paths/rooms.yaml b/meet-ce/backend/openapi/paths/rooms.yaml index efeea64e..5f6f3a2b 100644 --- a/meet-ce/backend/openapi/paths/rooms.yaml +++ b/meet-ce/backend/openapi/paths/rooms.yaml @@ -56,10 +56,10 @@ description: | Delete multiple OpenVidu Meet rooms at once with the specified room IDs. - If any of the rooms have active meetings or recordings, + If any of the rooms have active meetings or recordings, deletion behavior is determined by the provided `withMeeting` and `withRecordings` deletion policies. - Depending on these policies, the rooms may be deleted/closed immediately, scheduled to be deleted/closed once the meetings end, + Depending on these policies, the rooms may be deleted/closed immediately, scheduled to be deleted/closed once the meetings end, or the operation may fail if deletion is not permitted. tags: - OpenVidu Meet - Rooms @@ -120,10 +120,10 @@ description: | Deletes the specified OpenVidu Meet room by its room ID. - If the room has an active meeting or existing recordings, + If the room has an active meeting or existing recordings, deletion behavior is determined by the provided `withMeeting` and `withRecordings` deletion policies. - Depending on these policies, the room may be deleted/closed immediately, scheduled to be deleted/closed once the meeting ends, + Depending on these policies, the room may be deleted/closed immediately, scheduled to be deleted/closed once the meeting ends, or the operation may fail if deletion is not permitted. tags: - OpenVidu Meet - Rooms @@ -204,6 +204,8 @@ $ref: '../components/responses/forbidden-error.yaml' '404': $ref: '../components/responses/error-room-not-found.yaml' + '409': + $ref: '../components/responses/error-room-active-meeting.yaml' '422': $ref: '../components/responses/validation-error.yaml' '500': diff --git a/meet-ce/backend/src/models/error.model.ts b/meet-ce/backend/src/models/error.model.ts index d8b72bee..b34a9de0 100644 --- a/meet-ce/backend/src/models/error.model.ts +++ b/meet-ce/backend/src/models/error.model.ts @@ -205,6 +205,10 @@ export const errorRoomClosed = (roomId: string): OpenViduMeetError => { return new OpenViduMeetError('Room Error', `Room '${roomId}' is closed and cannot be joined`, 409); }; +export const errorRoomActiveMeeting = (roomId: string): OpenViduMeetError => { + return new OpenViduMeetError('Room Error', `Room '${roomId}' has an active meeting`, 409); +}; + export const errorInvalidRoomSecret = (roomId: string, secret: string): OpenViduMeetError => { return new OpenViduMeetError('Room Error', `Secret '${secret}' is not recognized for room '${roomId}'`, 400); }; diff --git a/meet-ce/backend/src/services/room.service.ts b/meet-ce/backend/src/services/room.service.ts index 513500a3..28fcbf12 100644 --- a/meet-ce/backend/src/services/room.service.ts +++ b/meet-ce/backend/src/services/room.service.ts @@ -25,6 +25,7 @@ import { validateRecordingTokenMetadata } from '../middlewares/index.js'; import { errorDeletingRoom, errorInvalidRoomSecret, + errorRoomActiveMeeting, errorRoomNotFound, internalError, OpenViduMeetError @@ -139,6 +140,12 @@ export class RoomService { */ async updateMeetRoomConfig(roomId: string, config: MeetRoomConfig): Promise { const room = await this.getMeetRoom(roomId); + + if (room.status === MeetRoomStatus.ACTIVE_MEETING) { + // Reject config updates during active meetings + throw errorRoomActiveMeeting(roomId); + } + room.config = config; await this.roomRepository.update(room); diff --git a/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts b/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts index c016f9ba..379b0d13 100644 --- a/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts @@ -5,10 +5,12 @@ import { FrontendEventService } from '../../../../src/services/index.js'; import { createRoom, deleteAllRooms, + disconnectFakeParticipants, getRoom, startTestServer, updateRoomConfig } from '../../../helpers/request-helpers.js'; +import { RoomData, setupSingleRoom } from '../../../helpers/test-scenarios.js'; describe('Room API Tests', () => { beforeAll(async () => { @@ -133,6 +135,42 @@ describe('Room API Tests', () => { }); }); + describe('Update Room Config with Active Meeting Tests', () => { + let roomData: RoomData; + + afterEach(async () => { + await disconnectFakeParticipants(); + }); + + it('should reject room config update when there is an active meeting', async () => { + // Create a room and start a meeting + roomData = await setupSingleRoom(true); + + // Try to update room config + const newConfig = { + recording: { + enabled: false + }, + chat: { + enabled: false + }, + virtualBackground: { + enabled: false + }, + e2ee: { + enabled: false + } + }; + + const response = await updateRoomConfig(roomData.room.roomId, newConfig); + + // Should return 409 Conflict + expect(response.status).toBe(409); + expect(response.body.error).toBe('Room Error'); + expect(response.body.message).toContain(`Room '${roomData.room.roomId}' has an active meeting`); + }); + }); + describe('Update Room Config Validation failures', () => { it('should fail when config has incorrect structure', async () => { const { roomId } = await createRoom({ diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/rooms-lists/rooms-lists.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/rooms-lists/rooms-lists.component.ts index c3752a8c..fe301737 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/rooms-lists/rooms-lists.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/rooms-lists/rooms-lists.component.ts @@ -323,8 +323,8 @@ export class RoomsListsComponent implements OnInit, OnChanges { return room.status !== MeetRoomStatus.CLOSED; } - canEditRoom(_room: MeetRoom): boolean { - return true; + canEditRoom(room: MeetRoom): boolean { + return room.status !== MeetRoomStatus.ACTIVE_MEETING; } // ===== UI HELPER METHODS ===== diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/check-room-edit.guard.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/check-room-edit.guard.ts new file mode 100644 index 00000000..ae785548 --- /dev/null +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/check-room-edit.guard.ts @@ -0,0 +1,37 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { MeetRoomStatus } from '@openvidu-meet/typings'; +import { RoomService } from '../services/room.service'; + +/** + * Guard that prevents editing a room when there's an active meeting. + * Redirects to /rooms if the room has an active meeting. + */ +export const checkRoomEditGuard: CanActivateFn = async (route) => { + const roomService = inject(RoomService); + const router = inject(Router); + + const roomId = route.paramMap.get('roomId'); + + if (!roomId) { + console.warn('No roomId provided in route params'); + router.navigate(['/rooms']); + return false; + } + + try { + const room = await roomService.getRoom(roomId); + + if (room.status === MeetRoomStatus.ACTIVE_MEETING) { + console.warn(`Cannot edit room ${roomId} - active meeting in progress`); + router.navigate(['/rooms']); + return false; + } + + return true; + } catch (error) { + console.error(`Error checking room status for ${roomId}:`, error); + router.navigate(['/rooms']); + return false; + } +}; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/index.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/index.ts index 435d7e29..c5214044 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/index.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/index.ts @@ -1,4 +1,5 @@ export * from './auth.guard'; +export * from './check-room-edit.guard'; export * from './extract-query-params.guard'; export * from './remove-query-params.guard'; export * from './run-serially.guard'; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts index 386d4d01..cca40eb8 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts @@ -2,6 +2,7 @@ import { Routes } from '@angular/router'; import { checkParticipantRoleAndAuthGuard, checkRecordingAuthGuard, + checkRoomEditGuard, checkUserAuthenticatedGuard, checkUserNotAuthenticatedGuard, extractRecordingQueryParamsGuard, @@ -90,7 +91,8 @@ export const baseRoutes: Routes = [ }, { path: 'rooms/:roomId/edit', - component: RoomWizardComponent + component: RoomWizardComponent, + canActivate: [checkRoomEditGuard] }, { path: 'recordings',