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.
This commit is contained in:
CSantosM 2026-01-23 17:30:06 +01:00
parent 55aab084b0
commit 215b11e93f
19 changed files with 158 additions and 164 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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:

View File

@ -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'

View File

@ -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

View File

@ -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);
}

View File

@ -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());

View File

@ -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);

View File

@ -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);

View File

@ -100,7 +100,7 @@ export const setupSingleRoomWithRecording = async (
roomName = 'TEST_ROOM'
): Promise<RoomData> => {
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;
}

View File

@ -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 () => {

View File

@ -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,

View File

@ -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);
});

View File

@ -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<MeetRecordingInfo> {
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);