From 215b11e93ff2984659979cad7f4b2ec3f1343c09 Mon Sep 17 00:00:00 2001 From: CSantosM <4a.santos@gmail.com> Date: Fri, 23 Jan 2026 17:30:06 +0100 Subject: [PATCH] Moves recording API to public endpoint This commit refactors the recording API endpoints from the internal API to the public API. This change allows users to start and stop recordings using API keys, enabling more secure and flexible access control for recording functionality. It also centralizes recording-related logic in the public API, simplifying the codebase and improving maintainability. --- .../start-recording-request.yaml | 0 .../error-recording-conflict.yaml | 2 +- .../error-recording-not-active.yaml | 2 +- .../error-service-unavailable.yaml | 2 +- .../success-start-recording.yaml | 2 +- .../success-stop-recording.yaml | 2 +- .../backend/openapi/openvidu-meet-api.yaml | 2 + .../openapi/openvidu-meet-internal-api.yaml | 6 -- .../openapi/paths/internal/recordings.yaml | 58 ----------------- meet-ce/backend/openapi/paths/recordings.yaml | 60 ++++++++++++++++++ .../src/middlewares/recording.middleware.ts | 13 +++- .../backend/src/routes/recording.routes.ts | 19 +++--- meet-ce/backend/src/server.ts | 4 +- .../backend/tests/helpers/request-helpers.ts | 18 +++--- .../backend/tests/helpers/test-scenarios.ts | 8 +-- .../api/recordings/get-recording.test.ts | 8 +-- .../api/recordings/race-conditions.test.ts | 62 +++++++++---------- .../api/security/recording-security.test.ts | 48 ++++++-------- .../recordings/services/recording.service.ts | 6 +- 19 files changed, 158 insertions(+), 164 deletions(-) rename meet-ce/backend/openapi/components/requestBodies/{internal => }/start-recording-request.yaml (100%) rename meet-ce/backend/openapi/components/responses/{internal => }/error-recording-conflict.yaml (92%) rename meet-ce/backend/openapi/components/responses/{internal => }/error-recording-not-active.yaml (93%) rename meet-ce/backend/openapi/components/responses/{internal => }/error-service-unavailable.yaml (89%) rename meet-ce/backend/openapi/components/responses/{internal => }/success-start-recording.yaml (91%) rename meet-ce/backend/openapi/components/responses/{internal => }/success-stop-recording.yaml (92%) delete mode 100644 meet-ce/backend/openapi/paths/internal/recordings.yaml 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);