diff --git a/meet-ce/backend/openapi/components/requestBodies/internal/start-recording-request.yaml b/meet-ce/backend/openapi/components/requestBodies/start-recording-request.yaml similarity index 100% rename from meet-ce/backend/openapi/components/requestBodies/internal/start-recording-request.yaml rename to meet-ce/backend/openapi/components/requestBodies/start-recording-request.yaml diff --git a/meet-ce/backend/openapi/components/responses/internal/error-recording-conflict.yaml b/meet-ce/backend/openapi/components/responses/error-recording-conflict.yaml similarity index 92% rename from meet-ce/backend/openapi/components/responses/internal/error-recording-conflict.yaml rename to meet-ce/backend/openapi/components/responses/error-recording-conflict.yaml index 1afe2ae6..298576cc 100644 --- a/meet-ce/backend/openapi/components/responses/internal/error-recording-conflict.yaml +++ b/meet-ce/backend/openapi/components/responses/error-recording-conflict.yaml @@ -2,7 +2,7 @@ description: Conflict — The recording cannot be started due to resource state content: application/json: schema: - $ref: '../../schemas/error.yaml' + $ref: '../schemas/error.yaml' examples: already_recording: summary: Room is already being recorded diff --git a/meet-ce/backend/openapi/components/responses/internal/error-recording-not-active.yaml b/meet-ce/backend/openapi/components/responses/error-recording-not-active.yaml similarity index 93% rename from meet-ce/backend/openapi/components/responses/internal/error-recording-not-active.yaml rename to meet-ce/backend/openapi/components/responses/error-recording-not-active.yaml index 7d27e4b5..26cb3366 100644 --- a/meet-ce/backend/openapi/components/responses/internal/error-recording-not-active.yaml +++ b/meet-ce/backend/openapi/components/responses/error-recording-not-active.yaml @@ -2,7 +2,7 @@ description: Conflict — The recording is starting or already stopped content: application/json: schema: - $ref: '../../schemas/error.yaml' + $ref: '../schemas/error.yaml' examples: starting_recording: summary: Recording is starting diff --git a/meet-ce/backend/openapi/components/responses/internal/error-service-unavailable.yaml b/meet-ce/backend/openapi/components/responses/error-service-unavailable.yaml similarity index 89% rename from meet-ce/backend/openapi/components/responses/internal/error-service-unavailable.yaml rename to meet-ce/backend/openapi/components/responses/error-service-unavailable.yaml index 05206474..31b7ba21 100644 --- a/meet-ce/backend/openapi/components/responses/internal/error-service-unavailable.yaml +++ b/meet-ce/backend/openapi/components/responses/error-service-unavailable.yaml @@ -2,7 +2,7 @@ description: Service Unavailable — The recording service is unavailable content: application/json: schema: - $ref: '../../schemas/error.yaml' + $ref: '../schemas/error.yaml' examples: starting_timeout: summary: Recording service timed out diff --git a/meet-ce/backend/openapi/components/responses/internal/success-start-recording.yaml b/meet-ce/backend/openapi/components/responses/success-start-recording.yaml similarity index 91% rename from meet-ce/backend/openapi/components/responses/internal/success-start-recording.yaml rename to meet-ce/backend/openapi/components/responses/success-start-recording.yaml index 6710d0b1..5f75aa01 100644 --- a/meet-ce/backend/openapi/components/responses/internal/success-start-recording.yaml +++ b/meet-ce/backend/openapi/components/responses/success-start-recording.yaml @@ -2,7 +2,7 @@ description: Successfully created the OpenVidu Meet recording content: application/json: schema: - $ref: '../../schemas/meet-recording.yaml' + $ref: '../schemas/meet-recording.yaml' example: recordingId: 'room-123--EG_XYZ--XX445' roomId: 'room-123' diff --git a/meet-ce/backend/openapi/components/responses/internal/success-stop-recording.yaml b/meet-ce/backend/openapi/components/responses/success-stop-recording.yaml similarity index 92% rename from meet-ce/backend/openapi/components/responses/internal/success-stop-recording.yaml rename to meet-ce/backend/openapi/components/responses/success-stop-recording.yaml index 0e05e00b..0712a02c 100644 --- a/meet-ce/backend/openapi/components/responses/internal/success-stop-recording.yaml +++ b/meet-ce/backend/openapi/components/responses/success-stop-recording.yaml @@ -8,7 +8,7 @@ headers: content: application/json: schema: - $ref: '../../schemas/meet-recording.yaml' + $ref: '../schemas/meet-recording.yaml' example: recordingId: 'room-123--EG_XYZ--XX445' roomId: 'room-123' diff --git a/meet-ce/backend/openapi/openvidu-meet-api.yaml b/meet-ce/backend/openapi/openvidu-meet-api.yaml index 4e7e73b8..999fd423 100644 --- a/meet-ce/backend/openapi/openvidu-meet-api.yaml +++ b/meet-ce/backend/openapi/openvidu-meet-api.yaml @@ -35,6 +35,8 @@ paths: $ref: './paths/recordings.yaml#/~1recordings~1{recordingId}~1media' /recordings/{recordingId}/url: $ref: './paths/recordings.yaml#/~1recordings~1{recordingId}~1url' + /recordings/{recordingId}/stop: + $ref: './paths/recordings.yaml#/~1recordings~1{recordingId}~1stop' components: securitySchemes: $ref: './components/security.yaml' diff --git a/meet-ce/backend/openapi/openvidu-meet-internal-api.yaml b/meet-ce/backend/openapi/openvidu-meet-internal-api.yaml index 57756e53..b5b1b995 100644 --- a/meet-ce/backend/openapi/openvidu-meet-internal-api.yaml +++ b/meet-ce/backend/openapi/openvidu-meet-internal-api.yaml @@ -34,10 +34,6 @@ paths: $ref: './paths/internal/meet-global-config.yaml#/~1config~1rooms~1appearance' /rooms/{roomId}/token: $ref: './paths/internal/rooms.yaml#/~1rooms~1{roomId}~1token' - /recordings: - $ref: './paths/internal/recordings.yaml#/~1recordings' - /recordings/{recordingId}/stop: - $ref: './paths/internal/recordings.yaml#/~1recordings~1{recordingId}~1stop' /meetings/{roomId}: $ref: './paths/internal/meetings.yaml#/~1meetings~1{roomId}' /meetings/{roomId}/participants/{participantIdentity}: @@ -63,8 +59,6 @@ components: $ref: components/schemas/internal/rooms-appearance-config.yaml MeetRoom: $ref: components/schemas/meet-room.yaml - MeetRecording: - $ref: components/schemas/meet-recording.yaml MeetAnalytics: $ref: components/schemas/internal/meet-analytics.yaml Error: diff --git a/meet-ce/backend/openapi/paths/internal/recordings.yaml b/meet-ce/backend/openapi/paths/internal/recordings.yaml deleted file mode 100644 index ba902b3a..00000000 --- a/meet-ce/backend/openapi/paths/internal/recordings.yaml +++ /dev/null @@ -1,58 +0,0 @@ -/recordings: - post: - operationId: startRecording - summary: Start a recording - description: > - Start a new recording for an OpenVidu Meet room with the specified room ID. - tags: - - Internal API - Recordings - security: - - roomMemberTokenHeader: [] - requestBody: - $ref: '../../components/requestBodies/internal/start-recording-request.yaml' - responses: - '201': - $ref: '../../components/responses/internal/success-start-recording.yaml' - '401': - $ref: '../../components/responses/unauthorized-error.yaml' - '403': - $ref: '../../components/responses/forbidden-not-allowed-error.yaml' - '404': - $ref: '../../components/responses/error-room-not-found.yaml' - '409': - $ref: '../../components/responses/internal/error-recording-conflict.yaml' - '422': - $ref: '../../components/responses/validation-error.yaml' - '500': - $ref: '../../components/responses/internal-server-error.yaml' - '503': - $ref: '../../components/responses/internal/error-service-unavailable.yaml' -/recordings/{recordingId}/stop: - post: - operationId: stopRecording - summary: Stop a recording - description: | - Stops a recording with the specified recording ID. - - > **Note:** The recording must be in an `active` state; otherwise, a 409 error is returned. - tags: - - Internal API - Recordings - security: - - roomMemberTokenHeader: [] - parameters: - - $ref: '../../components/parameters/recording-id.yaml' - responses: - '202': - $ref: '../../components/responses/internal/success-stop-recording.yaml' - '401': - $ref: '../../components/responses/unauthorized-error.yaml' - '403': - $ref: '../../components/responses/forbidden-error.yaml' - '404': - $ref: '../../components/responses/error-recording-not-found.yaml' - '409': - $ref: '../../components/responses/internal/error-recording-not-active.yaml' - '422': - $ref: '../../components/responses/validation-error.yaml' - '500': - $ref: '../../components/responses/internal-server-error.yaml' diff --git a/meet-ce/backend/openapi/paths/recordings.yaml b/meet-ce/backend/openapi/paths/recordings.yaml index 7d0e99b4..aab55a47 100644 --- a/meet-ce/backend/openapi/paths/recordings.yaml +++ b/meet-ce/backend/openapi/paths/recordings.yaml @@ -1,4 +1,34 @@ /recordings: + post: + operationId: startRecording + summary: Start a recording + description: > + Start a new recording for an OpenVidu Meet room with the specified room ID. + tags: + - OpenVidu Meet - Recordings + security: + - apiKeyHeader: [] + - roomMemberTokenHeader: [] + requestBody: + $ref: '../components/requestBodies/start-recording-request.yaml' + responses: + '201': + $ref: '../components/responses/success-start-recording.yaml' + '401': + $ref: '../components/responses/unauthorized-error.yaml' + '403': + $ref: '../components/responses/forbidden-not-allowed-error.yaml' + '404': + $ref: '../components/responses/error-room-not-found.yaml' + '409': + $ref: '../components/responses/error-recording-conflict.yaml' + '422': + $ref: '../components/responses/validation-error.yaml' + '500': + $ref: '../components/responses/internal-server-error.yaml' + '503': + $ref: '../components/responses/error-service-unavailable.yaml' + get: operationId: getRecordings summary: Get all recordings @@ -249,6 +279,36 @@ $ref: '../components/responses/validation-error.yaml' '500': $ref: '../components/responses/internal-server-error.yaml' +/recordings/{recordingId}/stop: + post: + operationId: stopRecording + summary: Stop a recording + description: | + Stops a recording with the specified recording ID. + + > **Note:** The recording must be in an `active` state; otherwise, a 409 error is returned. + tags: + - OpenVidu Meet - Recordings + security: + - apiKeyHeader: [] + - roomMemberTokenHeader: [] + parameters: + - $ref: '../components/parameters/recording-id.yaml' + responses: + '202': + $ref: '../components/responses/success-stop-recording.yaml' + '401': + $ref: '../components/responses/unauthorized-error.yaml' + '403': + $ref: '../components/responses/forbidden-error.yaml' + '404': + $ref: '../components/responses/error-recording-not-found.yaml' + '409': + $ref: '../components/responses/error-recording-not-active.yaml' + '422': + $ref: '../components/responses/validation-error.yaml' + '500': + $ref: '../components/responses/internal-server-error.yaml' /recordings/{recordingId}/url: get: operationId: getRecordingUrl diff --git a/meet-ce/backend/src/middlewares/recording.middleware.ts b/meet-ce/backend/src/middlewares/recording.middleware.ts index 3d12dcb4..ee6cd085 100644 --- a/meet-ce/backend/src/middlewares/recording.middleware.ts +++ b/meet-ce/backend/src/middlewares/recording.middleware.ts @@ -46,9 +46,20 @@ export const withCanRecordPermission = async (req: Request, res: Response, next: const requestSessionService = container.get(RequestSessionService); const tokenRoomId = requestSessionService.getRoomIdFromToken(); + + /** + * If there is no token, the user is allowed to access the resource because one of the following reasons: + * + * - The request is invoked using the API key. + * - The user is admin. + */ + if (!tokenRoomId) { + return next(); + } + const permissions = requestSessionService.getRoomMemberMeetPermissions(); - if (!tokenRoomId || !permissions) { + if (!permissions) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } diff --git a/meet-ce/backend/src/routes/recording.routes.ts b/meet-ce/backend/src/routes/recording.routes.ts index 5abcf3e9..be186bcf 100644 --- a/meet-ce/backend/src/routes/recording.routes.ts +++ b/meet-ce/backend/src/routes/recording.routes.ts @@ -79,25 +79,24 @@ recordingRouter.get( withCanRetrieveRecordingsPermission, recordingCtrl.getRecordingUrl ); - -// Internal Recording Routes -export const internalRecordingRouter: Router = Router(); -internalRecordingRouter.use(bodyParser.urlencoded({ extended: true })); -internalRecordingRouter.use(bodyParser.json()); - -internalRecordingRouter.post( +recordingRouter.post( '/', + withAuth(apiKeyValidator, roomMemberTokenValidator), validateStartRecordingReq, withRecordingEnabled, - withAuth(roomMemberTokenValidator), withCanRecordPermission, recordingCtrl.startRecording ); -internalRecordingRouter.post( +recordingRouter.post( '/:recordingId/stop', + withAuth(apiKeyValidator, roomMemberTokenValidator), withValidRecordingId, withRecordingEnabled, - withAuth(roomMemberTokenValidator), withCanRecordPermission, recordingCtrl.stopRecording ); + +// Internal Recording Routes +// export const internalRecordingRouter: Router = Router(); +// internalRecordingRouter.use(bodyParser.urlencoded({ extended: true })); +// internalRecordingRouter.use(bodyParser.json()); diff --git a/meet-ce/backend/src/server.ts b/meet-ce/backend/src/server.ts index dada04fc..c1226a4e 100644 --- a/meet-ce/backend/src/server.ts +++ b/meet-ce/backend/src/server.ts @@ -14,7 +14,7 @@ import { authRouter } from './routes/auth.routes.js'; import { configRouter } from './routes/global-config.routes.js'; import { livekitWebhookRouter } from './routes/livekit.routes.js'; import { internalMeetingRouter } from './routes/meeting.routes.js'; -import { internalRecordingRouter, recordingRouter } from './routes/recording.routes.js'; +import { recordingRouter } from './routes/recording.routes.js'; import { internalRoomRouter, roomRouter } from './routes/room.routes.js'; import { userRouter } from './routes/user.routes.js'; import { @@ -89,7 +89,7 @@ const createApp = () => { app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/users`, userRouter); app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/rooms`, internalRoomRouter); app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/meetings`, internalMeetingRouter); - app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings`, internalRecordingRouter); + // app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings`, internalRecordingRouter); app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/config`, configRouter); app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/analytics`, analyticsRouter); diff --git a/meet-ce/backend/tests/helpers/request-helpers.ts b/meet-ce/backend/tests/helpers/request-helpers.ts index 94845776..75ea3751 100644 --- a/meet-ce/backend/tests/helpers/request-helpers.ts +++ b/meet-ce/backend/tests/helpers/request-helpers.ts @@ -581,21 +581,21 @@ export const endMeeting = async (roomId: string, moderatorToken: string) => { return response; }; -export const startRecording = async (roomId: string, moderatorToken: string) => { +export const startRecording = async (roomId: string) => { checkAppIsRunning(); return await request(app) - .post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings`) - .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, moderatorToken) + .post(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings`) + .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY) .send({ roomId }); }; -export const stopRecording = async (recordingId: string, moderatorToken: string) => { +export const stopRecording = async (recordingId: string) => { checkAppIsRunning(); const response = await request(app) - .post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings/${recordingId}/stop`) - .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, moderatorToken) + .post(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${recordingId}/stop`) + .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY) .send(); await sleep('2.5s'); @@ -685,7 +685,7 @@ export const downloadRecordings = async ( return await req; }; -export const stopAllRecordings = async (moderatorToken: string) => { +export const stopAllRecordings = async () => { checkAppIsRunning(); const response = await getAllRecordings(); @@ -701,8 +701,8 @@ export const stopAllRecordings = async (moderatorToken: string) => { console.log(`Stopping ${recordingIds.length} recordings...`, recordingIds); const tasks = recordingIds.map((recordingId: string) => request(app) - .post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings/${recordingId}/stop`) - .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, moderatorToken) + .post(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${recordingId}/stop`) + .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY) .send() ); const results = await Promise.all(tasks); diff --git a/meet-ce/backend/tests/helpers/test-scenarios.ts b/meet-ce/backend/tests/helpers/test-scenarios.ts index 58986716..a21fa6a2 100644 --- a/meet-ce/backend/tests/helpers/test-scenarios.ts +++ b/meet-ce/backend/tests/helpers/test-scenarios.ts @@ -100,7 +100,7 @@ export const setupSingleRoomWithRecording = async ( roomName = 'TEST_ROOM' ): Promise => { const roomData = await setupSingleRoom(true, roomName); - const response = await startRecording(roomData.room.roomId, roomData.moderatorToken); + const response = await startRecording(roomData.room.roomId); expectValidStartRecordingResponse(response, roomData.room.roomId, roomData.room.roomName); roomData.recordingId = response.body.recordingId; @@ -110,7 +110,7 @@ export const setupSingleRoomWithRecording = async ( } if (stopRecordingCond) { - await stopRecording(roomData.recordingId!, roomData.moderatorToken); + await stopRecording(roomData.recordingId!); } return roomData; @@ -145,7 +145,7 @@ export const setupMultiRecordingsTestContext = async ( } // Send start recording request - const response = await startRecording(roomData.room.roomId, roomData.moderatorToken); + const response = await startRecording(roomData.room.roomId); expectValidStartRecordingResponse(response, roomData.room.roomId, roomData.room.roomName); // Store the recordingId in context @@ -162,7 +162,7 @@ export const setupMultiRecordingsTestContext = async ( // Stop recordings for the first numStops rooms const stopPromises = startedRooms.slice(0, numStops).map(async (roomData) => { if (roomData.recordingId) { - await stopRecording(roomData.recordingId, roomData.moderatorToken); + await stopRecording(roomData.recordingId); console.log(`Recording stopped for room ${roomData.room.roomId}`); return roomData.recordingId; } diff --git a/meet-ce/backend/tests/integration/api/recordings/get-recording.test.ts b/meet-ce/backend/tests/integration/api/recordings/get-recording.test.ts index ae01e631..7a823b81 100644 --- a/meet-ce/backend/tests/integration/api/recordings/get-recording.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/get-recording.test.ts @@ -48,11 +48,7 @@ describe('Recording API Tests', () => { it('should get an ACTIVE recording status', async () => { const contextAux = await setupMultiRecordingsTestContext(1, 1, 0); - const { - room: roomAux, - recordingId: recordingIdAux = '', - moderatorToken: moderatorTokenAux - } = contextAux.getRoomByIndex(0)!; + const { room: roomAux, recordingId: recordingIdAux = '' } = contextAux.getRoomByIndex(0)!; const response = await getRecording(recordingIdAux); expectValidGetRecordingResponse( @@ -63,7 +59,7 @@ describe('Recording API Tests', () => { MeetRecordingStatus.ACTIVE ); - await stopAllRecordings(moderatorTokenAux); + await stopAllRecordings(); }); it('should return 404 when recording does not exist', async () => { diff --git a/meet-ce/backend/tests/integration/api/recordings/race-conditions.test.ts b/meet-ce/backend/tests/integration/api/recordings/race-conditions.test.ts index 465e74f2..01885a94 100644 --- a/meet-ce/backend/tests/integration/api/recordings/race-conditions.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/race-conditions.test.ts @@ -65,7 +65,7 @@ describe('Recording API Race Conditions Tests', () => { try { // Attempt to start recording - const result = await startRecording(roomData.room.roomId, roomData.moderatorToken); + const result = await startRecording(roomData.room.roomId); expect(eventServiceOffSpy).toHaveBeenCalledWith( DistributedEventType.RECORDING_ACTIVE, expect.any(Function) @@ -120,7 +120,7 @@ describe('Recording API Race Conditions Tests', () => { try { // Start recording with a short timeout - const result = await startRecording(roomData.room.roomId, roomData.moderatorToken); + const result = await startRecording(roomData.room.roomId); expect(eventServiceOffSpy).toHaveBeenCalledWith( DistributedEventType.RECORDING_ACTIVE, @@ -180,7 +180,7 @@ describe('Recording API Race Conditions Tests', () => { try { // Start recording in room1 (should timeout) - const rec1 = await startRecording(room1.room.roomId, room1.moderatorToken); + const rec1 = await startRecording(room1.room.roomId); expect(rec1.status).toBe(503); setInternalConfig({ @@ -188,18 +188,18 @@ describe('Recording API Race Conditions Tests', () => { }); // ✅ EXPECTED BEHAVIOR: System should remain stable // Recording in different room should work normally - const rec2 = await startRecording(room2.room.roomId, room2.moderatorToken); + const rec2 = await startRecording(room2.room.roomId); expect(rec2.status).toBe(201); expectValidStartRecordingResponse(rec2, room2.room.roomId, room2.room.roomName); - let response = await stopRecording(rec2.body.recordingId!, room2.moderatorToken); + let response = await stopRecording(rec2.body.recordingId!); expectValidStopRecordingResponse(response, rec2.body.recordingId!, room2.room.roomId, room2.room.roomName); // ✅ EXPECTED BEHAVIOR: After timeout cleanup, room1 should be available again - const rec3 = await startRecording(room1.room.roomId, room1.moderatorToken); + const rec3 = await startRecording(room1.room.roomId); expect(rec3.status).toBe(201); expectValidStartRecordingResponse(rec3, room1.room.roomId, room1.room.roomName); - response = await stopRecording(rec3.body.recordingId!, room1.moderatorToken); + response = await stopRecording(rec3.body.recordingId!); expectValidStopRecordingResponse(response, rec3.body.recordingId!, room1.room.roomId, room1.room.roomName); } finally { startRoomCompositeSpy.mockRestore(); @@ -224,7 +224,7 @@ describe('Recording API Race Conditions Tests', () => { try { // Start recordings in all rooms simultaneously (all should timeout) const results = await Promise.all( - rooms.map((room) => startRecording(room.room.roomId, room.moderatorToken)) + rooms.map((room) => startRecording(room.room.roomId)) ); // All should timeout @@ -239,14 +239,14 @@ describe('Recording API Race Conditions Tests', () => { // ✅ EXPECTED BEHAVIOR: After timeouts, all rooms should be available again const retryResults = await Promise.all( - rooms.map((room) => startRecording(room.room.roomId, room.moderatorToken)) + rooms.map((room) => startRecording(room.room.roomId)) ); for (const startResult of retryResults) { expect(startResult.status).toBe(201); const room = rooms.find((r) => r.room.roomId === startResult.body.roomId)!; expectValidStartRecordingResponse(startResult, room.room.roomId, room.room.roomName); - const stopResult = await stopRecording(startResult.body.recordingId!, room.moderatorToken); + const stopResult = await stopRecording(startResult.body.recordingId!); expectValidStopRecordingResponse( stopResult, startResult.body.recordingId!, @@ -270,18 +270,18 @@ describe('Recording API Race Conditions Tests', () => { eventController.initialize(); eventController.pauseEventsForRoom(roomDataA!.room.roomId); - const recordingPromiseA = startRecording(roomDataA!.room.roomId, roomDataA!.moderatorToken); + const recordingPromiseA = startRecording(roomDataA!.room.roomId); // Brief delay to ensure both recordings start in the right order await sleep('1s'); // Step 2: Start recording in roomB (this will complete quickly) - const recordingResponseB = await startRecording(roomDataB!.room.roomId, roomDataB!.moderatorToken); + const recordingResponseB = await startRecording(roomDataB!.room.roomId); expectValidStartRecordingResponse(recordingResponseB, roomDataB!.room.roomId, roomDataB!.room.roomName); const recordingIdB = recordingResponseB.body.recordingId; // Step 3: Stop recording in roomB while roomA is still waiting for its event - const stopResponseB = await stopRecording(recordingIdB, roomDataB!.moderatorToken); + const stopResponseB = await stopRecording(recordingIdB); expectValidStopRecordingResponse(stopResponseB, recordingIdB, roomDataB!.room.roomId, roomDataB!.room.roomName); eventController.releaseEventsForRoom(roomDataA!.room.roomId); @@ -301,7 +301,7 @@ describe('Recording API Race Conditions Tests', () => { const roomDataList = Array.from({ length: 5 }, (_, index) => context!.getRoomByIndex(index)!); const startResponses = await Promise.all( - roomDataList.map((roomData) => startRecording(roomData.room.roomId, roomData.moderatorToken)) + roomDataList.map((roomData) => startRecording(roomData.room.roomId)) ); startResponses.forEach((response, index) => { @@ -315,7 +315,7 @@ describe('Recording API Race Conditions Tests', () => { const recordingIds = startResponses.map((res) => res.body.recordingId); const stopResponses = await Promise.all( - recordingIds.map((recordingId, index) => stopRecording(recordingId, roomDataList[index].moderatorToken)) + recordingIds.map((recordingId) => stopRecording(recordingId)) ); stopResponses.forEach((response, index) => { @@ -332,14 +332,14 @@ describe('Recording API Race Conditions Tests', () => { context = await setupMultiRoomTestContext(2, true); const roomDataA = context.getRoomByIndex(0); const roomDataB = context.getRoomByIndex(1); - const responseA = await startRecording(roomDataA!.room.roomId, roomDataA!.moderatorToken); - const responseB = await startRecording(roomDataB!.room.roomId, roomDataB!.moderatorToken); + const responseA = await startRecording(roomDataA!.room.roomId); + const responseB = await startRecording(roomDataB!.room.roomId); const recordingIdA = responseA.body.recordingId; const recordingIdB = responseB.body.recordingId; const [stopResponseA, stopResponseB] = await Promise.all([ - stopRecording(recordingIdA, roomDataA!.moderatorToken), - stopRecording(recordingIdB, roomDataB!.moderatorToken) + stopRecording(recordingIdA), + stopRecording(recordingIdB) ]); expectValidStopRecordingResponse(stopResponseA, recordingIdA, roomDataA!.room.roomId, roomDataA!.room.roomName); expectValidStopRecordingResponse(stopResponseB, recordingIdB, roomDataB!.room.roomId, roomDataB!.room.roomName); @@ -350,8 +350,8 @@ describe('Recording API Race Conditions Tests', () => { const roomData = context.getRoomByIndex(0)!; const [firstRecordingResponse, secondRecordingResponse] = await Promise.all([ - startRecording(roomData.room.roomId, roomData.moderatorToken), - startRecording(roomData.room.roomId, roomData.moderatorToken) + startRecording(roomData.room.roomId), + startRecording(roomData.room.roomId) ]); console.log('First recording response:', firstRecordingResponse.body); @@ -366,7 +366,7 @@ describe('Recording API Race Conditions Tests', () => { if (firstRecordingResponse.status === 201) { expectValidStartRecordingResponse(firstRecordingResponse, roomData.room.roomId, roomData.room.roomName); // stop the first recording - const stopResponse = await stopRecording(firstRecordingResponse.body.recordingId, roomData.moderatorToken); + const stopResponse = await stopRecording(firstRecordingResponse.body.recordingId); expectValidStopRecordingResponse( stopResponse, firstRecordingResponse.body.recordingId, @@ -376,7 +376,7 @@ describe('Recording API Race Conditions Tests', () => { } else { expectValidStartRecordingResponse(secondRecordingResponse, roomData.room.roomId, roomData.room.roomName); // stop the second recording - const stopResponse = await stopRecording(secondRecordingResponse.body.recordingId, roomData.moderatorToken); + const stopResponse = await stopRecording(secondRecordingResponse.body.recordingId); expectValidStopRecordingResponse( stopResponse, secondRecordingResponse.body.recordingId, @@ -393,12 +393,12 @@ describe('Recording API Race Conditions Tests', () => { const recordingTaskScheduler = container.get(RecordingScheduledTasksService); const gcSpy = jest.spyOn(recordingTaskScheduler as any, 'performActiveRecordingLocksGC'); - const startResponse = await startRecording(roomData.room.roomId, roomData.moderatorToken); + const startResponse = await startRecording(roomData.room.roomId); expectValidStartRecordingResponse(startResponse, roomData.room.roomId, roomData.room.roomName); const recordingId = startResponse.body.recordingId; // Execute garbage collection while stopping the recording - const stopPromise = stopRecording(recordingId, roomData.moderatorToken); + const stopPromise = stopRecording(recordingId); const gcPromise = recordingTaskScheduler['performActiveRecordingLocksGC'](); // Both operations should complete @@ -465,18 +465,18 @@ describe('Recording API Race Conditions Tests', () => { const room2 = context.getRoomByIndex(1)!; const room3 = context.getRoomByIndex(2)!; - const start1 = await startRecording(room1.room.roomId, room1.moderatorToken); - const start2 = await startRecording(room2.room.roomId, room2.moderatorToken); + const start1 = await startRecording(room1.room.roomId); + const start2 = await startRecording(room2.room.roomId); const recordingId1 = start1.body.recordingId; const recordingId2 = start2.body.recordingId; - await stopRecording(recordingId1, room1.moderatorToken); - await stopRecording(recordingId2, room2.moderatorToken); + await stopRecording(recordingId1); + await stopRecording(recordingId2); // Bulk delete the recordings while starting a new one const bulkDeletePromise = bulkDeleteRecordings([recordingId1, recordingId2]); - const startNewRecordingPromise = startRecording(room3.room.roomId, room3.moderatorToken); + const startNewRecordingPromise = startRecording(room3.room.roomId); // Both operations should complete successfully const [bulkDeleteResult, newRecordingResult] = await Promise.all([bulkDeletePromise, startNewRecordingPromise]); @@ -486,7 +486,7 @@ describe('Recording API Race Conditions Tests', () => { // Check that the new recording started successfully expectValidStartRecordingResponse(newRecordingResult, room3.room.roomId, room3.room.roomName); - const newStopResponse = await stopRecording(newRecordingResult.body.recordingId, room3.moderatorToken); + const newStopResponse = await stopRecording(newRecordingResult.body.recordingId); expectValidStopRecordingResponse( newStopResponse, newRecordingResult.body.recordingId, diff --git a/meet-ce/backend/tests/integration/api/security/recording-security.test.ts b/meet-ce/backend/tests/integration/api/security/recording-security.test.ts index 738081ea..aab1c2da 100644 --- a/meet-ce/backend/tests/integration/api/security/recording-security.test.ts +++ b/meet-ce/backend/tests/integration/api/security/recording-security.test.ts @@ -4,7 +4,6 @@ import { Express } from 'express'; import request from 'supertest'; import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js'; import { MEET_ENV } from '../../../../src/environment.js'; -import { expectValidStopRecordingResponse } from '../../../helpers/assertion-helpers.js'; import { deleteAllRecordings, deleteAllRooms, @@ -15,14 +14,13 @@ import { loginUser, startTestServer, stopAllRecordings, - stopRecording, updateRecordingAccessConfigInRoom } from '../../../helpers/request-helpers.js'; import { setupSingleRoom, setupSingleRoomWithRecording } from '../../../helpers/test-scenarios.js'; import { RoomData } from '../../../interfaces/scenarios.js'; const RECORDINGS_PATH = `${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings`; -const INTERNAL_RECORDINGS_PATH = `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings`; +// const INTERNAL_RECORDINGS_PATH = `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings`; describe('Recording API Security Tests', () => { let app: Express; @@ -45,17 +43,21 @@ describe('Recording API Security Tests', () => { roomData = await setupSingleRoom(true); }); - it('should fail when request includes API key', async () => { + afterEach(async () => { + await stopAllRecordings(); + }); + + it('should success when request includes API key', async () => { const response = await request(app) - .post(INTERNAL_RECORDINGS_PATH) + .post(RECORDINGS_PATH) .send({ roomId: roomData.room.roomId }) .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY); - expect(response.status).toBe(401); + expect(response.status).toBe(201); }); it('should fail when user is authenticated as admin', async () => { const response = await request(app) - .post(INTERNAL_RECORDINGS_PATH) + .post(RECORDINGS_PATH) .send({ roomId: roomData.room.roomId }) .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, adminAccessToken); expect(response.status).toBe(401); @@ -63,22 +65,17 @@ describe('Recording API Security Tests', () => { it('should succeed when participant is moderator', async () => { const response = await request(app) - .post(INTERNAL_RECORDINGS_PATH) + .post(RECORDINGS_PATH) .send({ roomId: roomData.room.roomId }) .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.moderatorToken); expect(response.status).toBe(201); - - // Stop recording to clean up - const recordingId = response.body.recordingId; - const stopResponse = await stopRecording(recordingId, roomData.moderatorToken); - expectValidStopRecordingResponse(stopResponse, recordingId, roomData.room.roomId, roomData.room.roomName); }); it('should fail when participant is moderator of a different room', async () => { const newRoomData = await setupSingleRoom(); const response = await request(app) - .post(INTERNAL_RECORDINGS_PATH) + .post(RECORDINGS_PATH) .send({ roomId: roomData.room.roomId }) .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, newRoomData.moderatorToken); expect(response.status).toBe(403); @@ -86,7 +83,7 @@ describe('Recording API Security Tests', () => { it('should fail when participant is speaker', async () => { const response = await request(app) - .post(INTERNAL_RECORDINGS_PATH) + .post(RECORDINGS_PATH) .send({ roomId: roomData.room.roomId }) .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.speakerToken); expect(response.status).toBe(403); @@ -101,43 +98,36 @@ describe('Recording API Security Tests', () => { }); afterAll(async () => { - await stopAllRecordings(roomData.moderatorToken); + await stopAllRecordings(); }); - it('should fail when request includes API key', async () => { + it('should success when request includes API key', async () => { const response = await request(app) - .post(`${INTERNAL_RECORDINGS_PATH}/${roomData.recordingId}/stop`) + .post(`${RECORDINGS_PATH}/${roomData.recordingId}/stop`) .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY); - expect(response.status).toBe(401); + expect(response.status).toBe(202); }); it('should fail when user is authenticated as admin', async () => { const response = await request(app) - .post(`${INTERNAL_RECORDINGS_PATH}/${roomData.recordingId}/stop`) + .post(`${RECORDINGS_PATH}/${roomData.recordingId}/stop`) .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, adminAccessToken); expect(response.status).toBe(401); }); - it('should succeed when participant is moderator', async () => { - const response = await request(app) - .post(`${INTERNAL_RECORDINGS_PATH}/${roomData.recordingId}/stop`) - .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.moderatorToken); - expect(response.status).toBe(202); - }); - it('should fail when participant is moderator of a different room', async () => { const newRoomData = await setupSingleRoom(); const response = await request(app) - .post(`${INTERNAL_RECORDINGS_PATH}/${roomData.recordingId}/stop`) + .post(`${RECORDINGS_PATH}/${roomData.recordingId}/stop`) .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, newRoomData.moderatorToken); expect(response.status).toBe(403); }); it('should fail when participant is speaker', async () => { const response = await request(app) - .post(`${INTERNAL_RECORDINGS_PATH}/${roomData.recordingId}/stop`) + .post(`${RECORDINGS_PATH}/${roomData.recordingId}/stop`) .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.speakerToken); expect(response.status).toBe(403); }); diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/recordings/services/recording.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/recordings/services/recording.service.ts index 75f73c13..b6320212 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/recordings/services/recording.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/recordings/services/recording.service.ts @@ -11,7 +11,7 @@ import { RecordingShareDialogComponent } from '../components/recording-share-dia }) export class RecordingService { protected readonly RECORDINGS_API = `${HttpService.API_PATH_PREFIX}/recordings`; - protected readonly INTERNAL_RECORDINGS_API = `${HttpService.INTERNAL_API_PATH_PREFIX}/recordings`; + // protected readonly INTERNAL_RECORDINGS_API = `${HttpService.INTERNAL_API_PATH_PREFIX}/recordings`; protected log; @@ -32,7 +32,7 @@ export class RecordingService { */ async startRecording(roomId: string): Promise { try { - return this.httpService.postRequest(this.INTERNAL_RECORDINGS_API, { roomId }); + return this.httpService.postRequest(this.RECORDINGS_API, { roomId }); } catch (error) { console.error('Error starting recording:', error); throw error; @@ -51,7 +51,7 @@ export class RecordingService { } try { - const path = `${this.INTERNAL_RECORDINGS_API}/${recordingId}/stop`; + const path = `${this.RECORDINGS_API}/${recordingId}/stop`; return this.httpService.postRequest(path, {}); } catch (error) { console.error('Error stopping recording:', error);