Merge branch 'e2ee_feature'
This commit is contained in:
commit
7dd368476e
@ -1,4 +1,4 @@
|
||||
description: Forbidden — Insufficient permissions
|
||||
description: Forbidden - Insufficient Permissions
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
description: Forbidden
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/error.yaml'
|
||||
examples:
|
||||
forbidden_error:
|
||||
summary: Forbidden Error Example
|
||||
value:
|
||||
error: Authorization Error
|
||||
message: 'Insufficient permissions to access this resource'
|
||||
recording_not_allowed:
|
||||
summary: Recording Not Allowed in Room Example
|
||||
value:
|
||||
error: Recording Error
|
||||
message: 'Recording is disabled for room room-123'
|
||||
@ -21,6 +21,8 @@ content:
|
||||
enabled: false
|
||||
virtualBackground:
|
||||
enabled: true
|
||||
e2ee:
|
||||
enabled: false
|
||||
moderatorUrl: 'http://localhost:6080/room/room-123?secret=123456'
|
||||
speakerUrl: 'http://localhost:6080/room/room-123?secret=654321'
|
||||
status: open
|
||||
@ -45,6 +47,8 @@ content:
|
||||
enabled: false
|
||||
virtualBackground:
|
||||
enabled: true
|
||||
e2ee:
|
||||
enabled: false
|
||||
|
||||
fields=moderatorUrl,speakerUrl:
|
||||
summary: Response containing only moderator and speaker URLs
|
||||
|
||||
@ -30,6 +30,8 @@ content:
|
||||
enabled: false
|
||||
virtualBackground:
|
||||
enabled: true
|
||||
e2ee:
|
||||
enabled: false
|
||||
moderatorUrl: 'http://localhost:6080/room/room-123?secret=123456'
|
||||
speakerUrl: 'http://localhost:6080/room/room-123?secret=654321'
|
||||
status: open
|
||||
@ -48,6 +50,8 @@ content:
|
||||
enabled: true
|
||||
virtualBackground:
|
||||
enabled: false
|
||||
e2ee:
|
||||
enabled: false
|
||||
moderatorUrl: 'http://localhost:6080/room/room-456?secret=789012'
|
||||
speakerUrl: 'http://localhost:6080/room/room-456?secret=210987'
|
||||
status: open
|
||||
@ -80,6 +84,8 @@ content:
|
||||
enabled: false
|
||||
virtualBackground:
|
||||
enabled: true
|
||||
e2ee:
|
||||
enabled: false
|
||||
- roomId: 'room-456'
|
||||
roomName: 'room'
|
||||
creationDate: 1620001000000
|
||||
@ -91,6 +97,8 @@ content:
|
||||
enabled: true
|
||||
virtualBackground:
|
||||
enabled: false
|
||||
e2ee:
|
||||
enabled: false
|
||||
pagination:
|
||||
isTruncated: true
|
||||
nextPageToken: 'abc123'
|
||||
|
||||
@ -10,6 +10,9 @@ MeetRoomConfig:
|
||||
virtualBackground:
|
||||
$ref: '#/MeetVirtualBackgroundConfig'
|
||||
description: Config for virtual background in the room.
|
||||
e2ee:
|
||||
$ref: '#/MeetE2EEConfig'
|
||||
description: Config for End-to-End Encryption (E2EE) in the room.
|
||||
MeetChatConfig:
|
||||
type: object
|
||||
properties:
|
||||
@ -47,3 +50,14 @@ MeetVirtualBackgroundConfig:
|
||||
default: true
|
||||
example: true
|
||||
description: If true, the room will be allowed to use virtual background.
|
||||
MeetE2EEConfig:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
default: false
|
||||
example: false
|
||||
description: >
|
||||
If true, the room will have End-to-End Encryption (E2EE) enabled.<br/>
|
||||
This ensures that the media streams are encrypted from the sender to the receiver, providing enhanced privacy and security for the participants.<br/>
|
||||
**Enabling E2EE will disable the recording feature for the room**.
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
'401':
|
||||
$ref: '../../components/responses/unauthorized-error.yaml'
|
||||
'403':
|
||||
$ref: '../../components/responses/forbidden-error.yaml'
|
||||
$ref: '../../components/responses/forbidden-not-allowed-error.yaml'
|
||||
'404':
|
||||
$ref: '../../components/responses/error-room-not-found.yaml'
|
||||
'409':
|
||||
|
||||
@ -45,7 +45,8 @@
|
||||
"test:integration-users": "node --experimental-vm-modules ../../node_modules/.bin/jest --runInBand --forceExit --testPathPattern \"tests/integration/api/users\" --ci --reporters=default --reporters=jest-junit",
|
||||
"test:unit": "node --experimental-vm-modules ../../node_modules/.bin/jest --runInBand --forceExit --testPathPattern \"tests/unit\" --ci --reporters=default --reporters=jest-junit",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"format:code": "prettier --ignore-path .gitignore --write '**/*.{ts,js,json,md}'"
|
||||
"format:code": "prettier --ignore-path .gitignore --write '**/*.{ts,js,json,md}'",
|
||||
"clean": "rm -rf node_modules dist public test-results"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openvidu-meet/typings": "workspace:*",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
MeetChatConfig,
|
||||
MeetE2EEConfig,
|
||||
MeetRecordingAccess,
|
||||
MeetRecordingConfig,
|
||||
MeetRoomAutoDeletionPolicy,
|
||||
@ -90,6 +91,10 @@ const VirtualBackgroundConfigSchema: z.ZodType<MeetVirtualBackgroundConfig> = z.
|
||||
enabled: z.boolean()
|
||||
});
|
||||
|
||||
const E2EEConfigSchema: z.ZodType<MeetE2EEConfig> = z.object({
|
||||
enabled: z.boolean()
|
||||
});
|
||||
|
||||
const ThemeModeSchema: z.ZodType<MeetRoomThemeMode> = z.enum([MeetRoomThemeMode.LIGHT, MeetRoomThemeMode.DARK]);
|
||||
|
||||
const hexColorSchema = z
|
||||
@ -118,12 +123,28 @@ export const AppearanceConfigSchema = z.object({
|
||||
themes: z.array(RoomThemeSchema).length(1, 'There must be exactly one theme defined')
|
||||
});
|
||||
|
||||
const RoomConfigSchema: z.ZodType<MeetRoomConfig> = z.object({
|
||||
recording: RecordingConfigSchema,
|
||||
chat: ChatConfigSchema,
|
||||
virtualBackground: VirtualBackgroundConfigSchema
|
||||
// appearance: AppearanceConfigSchema,
|
||||
});
|
||||
const RoomConfigSchema: z.ZodType<MeetRoomConfig> = z
|
||||
.object({
|
||||
recording: RecordingConfigSchema,
|
||||
chat: ChatConfigSchema,
|
||||
virtualBackground: VirtualBackgroundConfigSchema,
|
||||
e2ee: E2EEConfigSchema.optional().default({ enabled: false }),
|
||||
// appearance: AppearanceConfigSchema,
|
||||
})
|
||||
.transform((data) => {
|
||||
// Automatically disable recording when E2EE is enabled
|
||||
if (data.e2ee?.enabled && data.recording.enabled) {
|
||||
return {
|
||||
...data,
|
||||
recording: {
|
||||
...data.recording,
|
||||
enabled: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
const RoomDeletionPolicyWithMeetingSchema: z.ZodType<MeetRoomDeletionPolicyWithMeeting> = z.enum([
|
||||
MeetRoomDeletionPolicyWithMeeting.FORCE,
|
||||
@ -183,7 +204,8 @@ const RoomRequestOptionsSchema: z.ZodType<MeetRoomOptions> = z.object({
|
||||
config: RoomConfigSchema.optional().default({
|
||||
recording: { enabled: true, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true }
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: false }
|
||||
})
|
||||
// maxParticipants: z
|
||||
// .number()
|
||||
|
||||
@ -132,7 +132,7 @@ export class RoomService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the config of a specific meeting room.
|
||||
* Updates the configuration of a specific meeting room.
|
||||
*
|
||||
* @param roomId - The unique identifier of the meeting room to update
|
||||
* @param config - The new config to apply to the meeting room
|
||||
|
||||
@ -2,6 +2,8 @@ import { expect } from '@jest/globals';
|
||||
import { container } from '../../src/config/dependency-injector.config';
|
||||
import { INTERNAL_CONFIG } from '../../src/config/internal-config';
|
||||
import { TokenService } from '../../src/services';
|
||||
import { Response } from 'supertest';
|
||||
|
||||
import {
|
||||
MeetingEndAction,
|
||||
MeetRecordingAccess,
|
||||
@ -18,7 +20,7 @@ import {
|
||||
} from '@openvidu-meet/typings';
|
||||
|
||||
export const expectErrorResponse = (
|
||||
response: any,
|
||||
response: Response,
|
||||
status = 422,
|
||||
error = 'Unprocessable Entity',
|
||||
message = 'Invalid request',
|
||||
@ -51,7 +53,7 @@ export const expectErrorResponse = (
|
||||
);
|
||||
};
|
||||
|
||||
export const expectValidationError = (response: any, field: string, message: string) => {
|
||||
export const expectValidationError = (response: Response, field: string, message: string) => {
|
||||
expectErrorResponse(response, 422, 'Unprocessable Entity', 'Invalid request', [{ field, message }]);
|
||||
};
|
||||
|
||||
@ -68,7 +70,7 @@ export const expectValidationError = (response: any, field: string, message: str
|
||||
* if false, expects nextPageToken to be undefined)
|
||||
*/
|
||||
export const expectSuccessRoomsResponse = (
|
||||
response: any,
|
||||
response: Response,
|
||||
expectedRoomLength: number,
|
||||
expectedMaxItems: number,
|
||||
expectedTruncated: boolean,
|
||||
@ -90,7 +92,7 @@ export const expectSuccessRoomsResponse = (
|
||||
};
|
||||
|
||||
export const expectSuccessRoomResponse = (
|
||||
response: any,
|
||||
response: Response,
|
||||
roomName: string,
|
||||
autoDeletionDate?: number,
|
||||
config?: MeetRoomConfig
|
||||
@ -99,7 +101,7 @@ export const expectSuccessRoomResponse = (
|
||||
expectValidRoom(response.body, roomName, config, autoDeletionDate);
|
||||
};
|
||||
|
||||
export const expectSuccessRoomConfigResponse = (response: any, config: MeetRoomConfig) => {
|
||||
export const expectSuccessRoomConfigResponse = (response: Response, config: MeetRoomConfig) => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toBeDefined();
|
||||
expect(response.body).toEqual(config);
|
||||
@ -151,7 +153,8 @@ export const expectValidRoom = (
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true }
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: false }
|
||||
});
|
||||
}
|
||||
|
||||
@ -197,7 +200,7 @@ export const expectValidRecordingWithFields = (rec: MeetRecordingInfo, fields: s
|
||||
expectObjectFields(rec, fields);
|
||||
};
|
||||
|
||||
const expectObjectFields = (obj: any, present: string[] = [], absent: string[] = []) => {
|
||||
const expectObjectFields = (obj: unknown, present: string[] = [], absent: string[] = []) => {
|
||||
present.forEach((key) => {
|
||||
expect(obj).toHaveProperty(key);
|
||||
expect((obj as any)[key]).not.toBeUndefined();
|
||||
@ -209,7 +212,7 @@ const expectObjectFields = (obj: any, present: string[] = [], absent: string[] =
|
||||
};
|
||||
|
||||
// Validate recording location header in the response
|
||||
export const expectValidRecordingLocationHeader = (response: any) => {
|
||||
export const expectValidRecordingLocationHeader = (response: Response) => {
|
||||
const locationHeader = response.headers.location;
|
||||
expect(locationHeader).toBeDefined();
|
||||
const locationHeaderUrl = new URL(locationHeader);
|
||||
@ -230,7 +233,7 @@ export const expectValidRecordingLocationHeader = (response: any) => {
|
||||
* - expectedStatus: Override the expected status code (default: auto-determined based on range)
|
||||
*/
|
||||
export const expectSuccessRecordingMediaResponse = (
|
||||
response: any,
|
||||
response: Response,
|
||||
range?: string,
|
||||
fullSize?: number,
|
||||
options?: {
|
||||
@ -348,7 +351,7 @@ export const expectSuccessRecordingMediaResponse = (
|
||||
}
|
||||
};
|
||||
|
||||
export const expectValidStartRecordingResponse = (response: any, roomId: string, roomName: string) => {
|
||||
export const expectValidStartRecordingResponse = (response: Response, roomId: string, roomName: string) => {
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toHaveProperty('recordingId');
|
||||
|
||||
@ -369,7 +372,7 @@ export const expectValidStartRecordingResponse = (response: any, roomId: string,
|
||||
};
|
||||
|
||||
export const expectValidStopRecordingResponse = (
|
||||
response: any,
|
||||
response: Response,
|
||||
recordingId: string,
|
||||
roomId: string,
|
||||
roomName: string
|
||||
@ -388,7 +391,7 @@ export const expectValidStopRecordingResponse = (
|
||||
};
|
||||
|
||||
export const expectValidGetRecordingResponse = (
|
||||
response: any,
|
||||
response: Response,
|
||||
recordingId: string,
|
||||
roomId: string,
|
||||
roomName: string,
|
||||
@ -444,7 +447,7 @@ export const expectValidGetRecordingResponse = (
|
||||
};
|
||||
|
||||
export const expectSuccessListRecordingResponse = (
|
||||
response: any,
|
||||
response: Response,
|
||||
recordingLength: number,
|
||||
isTruncated: boolean,
|
||||
nextPageToken: boolean,
|
||||
@ -470,7 +473,7 @@ export const expectSuccessListRecordingResponse = (
|
||||
expect(response.body.pagination.maxItems).toBe(maxItems);
|
||||
};
|
||||
|
||||
export const expectValidGetRecordingUrlResponse = (response: any, recordingId: string) => {
|
||||
export const expectValidGetRecordingUrlResponse = (response: Response, recordingId: string) => {
|
||||
expect(response.status).toBe(200);
|
||||
const recordingUrl = response.body.url;
|
||||
expect(recordingUrl).toBeDefined();
|
||||
@ -480,7 +483,7 @@ export const expectValidGetRecordingUrlResponse = (response: any, recordingId: s
|
||||
expect(parsedUrl.searchParams.get('secret')).toBeDefined();
|
||||
};
|
||||
|
||||
export const expectValidRoomRolesAndPermissionsResponse = (response: any, roomId: string) => {
|
||||
export const expectValidRoomRolesAndPermissionsResponse = (response: Response, roomId: string) => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(
|
||||
expect.arrayContaining([
|
||||
@ -497,7 +500,7 @@ export const expectValidRoomRolesAndPermissionsResponse = (response: any, roomId
|
||||
};
|
||||
|
||||
export const expectValidRoomRoleAndPermissionsResponse = (
|
||||
response: any,
|
||||
response: Response,
|
||||
roomId: string,
|
||||
participantRole: ParticipantRole
|
||||
) => {
|
||||
@ -552,7 +555,7 @@ export const getPermissions = (
|
||||
};
|
||||
|
||||
export const expectValidParticipantTokenResponse = (
|
||||
response: any,
|
||||
response: Response,
|
||||
roomId: string,
|
||||
participantRole: ParticipantRole,
|
||||
participantName?: string,
|
||||
@ -601,7 +604,7 @@ export const expectValidParticipantTokenResponse = (
|
||||
};
|
||||
|
||||
export const expectValidRecordingTokenResponse = (
|
||||
response: any,
|
||||
response: Response,
|
||||
roomId: string,
|
||||
participantRole: ParticipantRole,
|
||||
canRetrieveRecordings: boolean,
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
MeetRecordingInfo,
|
||||
MeetRecordingStatus,
|
||||
MeetRoom,
|
||||
MeetRoomConfig,
|
||||
MeetRoomDeletionPolicyWithMeeting,
|
||||
MeetRoomDeletionPolicyWithRecordings,
|
||||
MeetRoomOptions,
|
||||
@ -284,7 +285,7 @@ export const getRoom = async (roomId: string, fields?: string, participantToken?
|
||||
return await req;
|
||||
};
|
||||
|
||||
export const getRoomConfig = async (roomId: string) => {
|
||||
export const getRoomConfig = async (roomId: string): Promise<Response> => {
|
||||
checkAppIsRunning();
|
||||
|
||||
return await request(app)
|
||||
@ -293,7 +294,7 @@ export const getRoomConfig = async (roomId: string) => {
|
||||
.send();
|
||||
};
|
||||
|
||||
export const updateRoomConfig = async (roomId: string, config: any) => {
|
||||
export const updateRoomConfig = async (roomId: string, config: MeetRoomConfig) => {
|
||||
checkAppIsRunning();
|
||||
|
||||
return await request(app)
|
||||
|
||||
@ -76,11 +76,11 @@ export const setupSingleRoom = async (
|
||||
* @param withParticipants Whether to join fake participants in the rooms.
|
||||
* @returns Test context with created rooms and their data.
|
||||
*/
|
||||
export const setupMultiRoomTestContext = async (numRooms: number, withParticipants: boolean): Promise<TestContext> => {
|
||||
export const setupMultiRoomTestContext = async (numRooms: number, withParticipants: boolean, roomConfig?: MeetRoomConfig): Promise<TestContext> => {
|
||||
const rooms: RoomData[] = [];
|
||||
|
||||
for (let i = 0; i < numRooms; i++) {
|
||||
const roomData = await setupSingleRoom(withParticipants, 'TEST_ROOM');
|
||||
const roomData = await setupSingleRoom(withParticipants, 'TEST_ROOM', roomConfig);
|
||||
rooms.push(roomData);
|
||||
}
|
||||
|
||||
|
||||
@ -63,7 +63,8 @@ describe('Room API Tests', () => {
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: false },
|
||||
virtualBackground: { enabled: true }
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: true }
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -0,0 +1,263 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from '@jest/globals';
|
||||
import { Express } from 'express';
|
||||
import request from 'supertest';
|
||||
import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js';
|
||||
import { MEET_INITIAL_API_KEY } from '../../../../src/environment.js';
|
||||
import { MeetRecordingAccess, MeetRoom } from '@openvidu-meet/typings';
|
||||
import { expectValidRoom } from '../../../helpers/assertion-helpers.js';
|
||||
import {
|
||||
createRoom,
|
||||
deleteAllRecordings,
|
||||
deleteAllRooms,
|
||||
getRoomConfig,
|
||||
startRecording,
|
||||
startTestServer,
|
||||
updateRoomConfig
|
||||
} from '../../../helpers/request-helpers.js';
|
||||
import { setupMultiRoomTestContext } from '../../../helpers/test-scenarios.js';
|
||||
|
||||
const ROOMS_PATH = `${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms`;
|
||||
|
||||
describe('E2EE Room Configuration Tests', () => {
|
||||
let app: Express;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = startTestServer();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await deleteAllRecordings();
|
||||
await deleteAllRooms();
|
||||
});
|
||||
|
||||
describe('E2EE Default Configuration', () => {
|
||||
it('Should create a room with E2EE disabled by default', async () => {
|
||||
const room = await createRoom({
|
||||
roomName: 'Test E2EE Default'
|
||||
});
|
||||
|
||||
expectValidRoom(room, 'Test E2EE Default');
|
||||
expect(room.config.e2ee).toBeDefined();
|
||||
expect(room.config.e2ee?.enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('E2EE Enabled Configuration', () => {
|
||||
it('Should create a room with E2EE enabled and recording automatically disabled', async () => {
|
||||
const payload = {
|
||||
roomName: 'Test E2EE Enabled',
|
||||
config: {
|
||||
recording: {
|
||||
enabled: true, // This should be automatically disabled
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: true }
|
||||
}
|
||||
};
|
||||
|
||||
const room = await createRoom(payload);
|
||||
|
||||
expect(room.roomName).toBe('Test E2EE Enabled');
|
||||
expect(room.config.e2ee?.enabled).toBe(true);
|
||||
expect(room.config.recording.enabled).toBe(false); // Recording should be disabled
|
||||
});
|
||||
});
|
||||
|
||||
describe('E2EE and Recording Interaction', () => {
|
||||
it('Should not allow starting recording in a room with E2EE enabled', async () => {
|
||||
const context = await setupMultiRoomTestContext(1, true, {
|
||||
recording: {
|
||||
enabled: true,
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: true }
|
||||
});
|
||||
|
||||
const { room, moderatorToken } = context.getRoomByIndex(0)!;
|
||||
|
||||
// Try to start recording (should fail because recording is not enabled in room config)
|
||||
const response = await startRecording(room.roomId, moderatorToken);
|
||||
|
||||
// The endpoint returns 404 when the recording endpoint doesn't exist for disabled recording rooms
|
||||
expect(403).toBe(response.status);
|
||||
expect(response.body.message).toBe(`Recording is disabled for room '${room.roomId}'`);
|
||||
});
|
||||
|
||||
it('Should disable recording when updating room config to enable E2EE', async () => {
|
||||
// Create room with recording enabled and E2EE disabled
|
||||
const room = await createRoom({
|
||||
roomName: 'Test E2EE Update',
|
||||
config: {
|
||||
recording: {
|
||||
enabled: true,
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: false }
|
||||
}
|
||||
});
|
||||
|
||||
expect(room.config.recording.enabled).toBe(true);
|
||||
expect(room.config.e2ee?.enabled).toBe(false);
|
||||
|
||||
// Update room to enable E2EE (recording should be automatically disabled)
|
||||
const updatedConfig = {
|
||||
recording: {
|
||||
enabled: true, // This should be automatically disabled
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: true }
|
||||
};
|
||||
|
||||
const response = await updateRoomConfig(room.roomId, updatedConfig);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
// Fetch the updated room to verify changes
|
||||
const { status, body: config } = await getRoomConfig(room.roomId);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(config.e2ee?.enabled).toBe(true);
|
||||
expect(config.recording.enabled).toBe(false);
|
||||
});
|
||||
|
||||
// TODO: Add test for enabling E2EE when there are active recordings in the room
|
||||
});
|
||||
|
||||
describe('E2EE Validation Tests', () => {
|
||||
it('Should fail when e2ee is not an object', async () => {
|
||||
const payload = {
|
||||
roomName: 'Test Invalid E2EE',
|
||||
config: {
|
||||
recording: {
|
||||
enabled: true,
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: 'invalid-e2ee' // Should be an object
|
||||
}
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post(ROOMS_PATH)
|
||||
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY)
|
||||
.send(payload)
|
||||
.expect(422);
|
||||
|
||||
expect(JSON.stringify(response.body.details)).toContain('Expected object');
|
||||
});
|
||||
|
||||
it('Should fail when e2ee.enabled is not a boolean', async () => {
|
||||
const payload = {
|
||||
roomName: 'Test Invalid E2EE Enabled',
|
||||
config: {
|
||||
recording: {
|
||||
enabled: true,
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: 'yes' } // Should be a boolean
|
||||
}
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post(ROOMS_PATH)
|
||||
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY)
|
||||
.send(payload)
|
||||
.expect(422);
|
||||
|
||||
expect(JSON.stringify(response.body.details)).toContain('Expected boolean');
|
||||
});
|
||||
});
|
||||
|
||||
describe('E2EE Update Configuration Tests', () => {
|
||||
it('Should successfully update room config with E2EE disabled to enabled', async () => {
|
||||
const room = await createRoom({
|
||||
roomName: 'Test E2EE Update Enabled'
|
||||
});
|
||||
|
||||
expect(room.config.e2ee?.enabled).toBe(false);
|
||||
|
||||
const { status, body } = await updateRoomConfig(room.roomId, {
|
||||
recording: {
|
||||
enabled: false,
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: true }
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.message).toBeDefined();
|
||||
|
||||
// Fetch the updated room to verify changes
|
||||
const { body: config } = await getRoomConfig(room.roomId);
|
||||
|
||||
expect(config.e2ee?.enabled).toBe(true);
|
||||
expect(config.recording.enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('E2EE and Room Status Tests', () => {
|
||||
it('Should return E2EE configuration when listing rooms', async () => {
|
||||
await deleteAllRooms();
|
||||
|
||||
const room1 = await createRoom({
|
||||
roomName: 'E2EE Enabled Room',
|
||||
config: {
|
||||
recording: {
|
||||
enabled: false,
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: true }
|
||||
}
|
||||
});
|
||||
|
||||
const room2 = await createRoom({
|
||||
roomName: 'E2EE Disabled Room',
|
||||
config: {
|
||||
recording: {
|
||||
enabled: true,
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: false }
|
||||
}
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get(ROOMS_PATH)
|
||||
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY)
|
||||
.expect(200);
|
||||
|
||||
// Filter out any rooms from other test suites
|
||||
const testRooms = response.body.rooms.filter(
|
||||
(r: MeetRoom) => r.roomId === room1.roomId || r.roomId === room2.roomId
|
||||
);
|
||||
|
||||
expect(testRooms).toHaveLength(2);
|
||||
|
||||
const e2eeEnabledRoom = testRooms.find((r: MeetRoom) => r.roomId === room1.roomId);
|
||||
const e2eeDisabledRoom = testRooms.find((r: MeetRoom) => r.roomId === room2.roomId);
|
||||
|
||||
expect(e2eeEnabledRoom.config.e2ee?.enabled).toBe(true);
|
||||
expect(e2eeEnabledRoom.config.recording.enabled).toBe(false);
|
||||
|
||||
expect(e2eeDisabledRoom.config.e2ee?.enabled).toBe(false);
|
||||
expect(e2eeDisabledRoom.config.recording.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -3,6 +3,7 @@ import { MeetRecordingAccess } from '@openvidu-meet/typings';
|
||||
import { expectSuccessRoomConfigResponse } from '../../../helpers/assertion-helpers.js';
|
||||
import { deleteAllRooms, getRoomConfig, startTestServer } from '../../../helpers/request-helpers.js';
|
||||
import { setupSingleRoom } from '../../../helpers/test-scenarios.js';
|
||||
import { Response } from 'supertest';
|
||||
|
||||
describe('Room API Tests', () => {
|
||||
const DEFAULT_CONFIG = {
|
||||
@ -11,7 +12,8 @@ describe('Room API Tests', () => {
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true }
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: false }
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
@ -28,7 +30,7 @@ describe('Room API Tests', () => {
|
||||
const roomData = await setupSingleRoom();
|
||||
const roomId = roomData.room.roomId;
|
||||
|
||||
const response = await getRoomConfig(roomId);
|
||||
const response: Response = await getRoomConfig(roomId);
|
||||
expectSuccessRoomConfigResponse(response, DEFAULT_CONFIG);
|
||||
});
|
||||
|
||||
@ -41,7 +43,8 @@ describe('Room API Tests', () => {
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: false }
|
||||
virtualBackground: { enabled: false },
|
||||
e2ee: { enabled: false }
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -41,7 +41,8 @@ describe('Room API Tests', () => {
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: false }
|
||||
virtualBackground: { enabled: false },
|
||||
e2ee: { enabled: false }
|
||||
}
|
||||
};
|
||||
// Create a room with custom config
|
||||
|
||||
@ -38,7 +38,8 @@ describe('Room API Tests', () => {
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true }
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: false }
|
||||
}
|
||||
});
|
||||
|
||||
@ -49,7 +50,8 @@ describe('Room API Tests', () => {
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN
|
||||
},
|
||||
chat: { enabled: false },
|
||||
virtualBackground: { enabled: false }
|
||||
virtualBackground: { enabled: false },
|
||||
e2ee: { enabled: true }
|
||||
};
|
||||
const updateResponse = await updateRoomConfig(createdRoom.roomId, updatedConfig);
|
||||
|
||||
@ -86,7 +88,8 @@ describe('Room API Tests', () => {
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true }
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: false }
|
||||
}
|
||||
});
|
||||
|
||||
@ -97,7 +100,8 @@ describe('Room API Tests', () => {
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true }
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: false }
|
||||
};
|
||||
const updateResponse = await updateRoomConfig(createdRoom.roomId, partialConfig);
|
||||
|
||||
|
||||
@ -24,7 +24,15 @@
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
{
|
||||
"glob": "livekit-client.e2ee.worker.mjs",
|
||||
"input": "../../node_modules/livekit-client/dist/",
|
||||
"output": "assets/livekit/"
|
||||
}
|
||||
],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": [],
|
||||
"preserveSymlinks": true
|
||||
@ -186,31 +194,31 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"type": "component"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"type": "directive"
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"type": "service"
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:module": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"typeSeparator": "."
|
||||
}
|
||||
}
|
||||
},
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"type": "component"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"type": "directive"
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"type": "service"
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:module": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"typeSeparator": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"name": "@openvidu-meet/frontend",
|
||||
"version": "3.4.1",
|
||||
"scripts": {
|
||||
"clean": "rm -rf node_modules dist test-results",
|
||||
"dev": "pnpm exec ng build --configuration development --watch",
|
||||
"build": "func() { pnpm exec ng build --configuration production --base-href=\"${1:-/}\"; }; func",
|
||||
"lib:serve": "ng build shared-meet-components --watch",
|
||||
|
||||
@ -5,6 +5,12 @@
|
||||
<mat-icon class="ov-room-icon room-icon">video_chat</mat-icon>
|
||||
<div class="room-info">
|
||||
<h1 class="room-title">{{ roomName }}</h1>
|
||||
@if (isE2EEEnabled) {
|
||||
<span class="encryption-badge" matTooltip="End-to-end encrypted">
|
||||
<mat-icon class="badge-icon">lock</mat-icon>
|
||||
This meeting is end-to-end encrypted
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -27,6 +33,7 @@
|
||||
<mat-card-content class="card-content">
|
||||
@if (!roomClosed) {
|
||||
<form [formGroup]="participantForm" (ngSubmit)="onFormSubmit()" class="join-form">
|
||||
<!-- Participant Name Input -->
|
||||
<mat-form-field appearance="outline" class="name-field">
|
||||
<mat-label>Your display name</mat-label>
|
||||
<input
|
||||
@ -42,13 +49,33 @@
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<!-- E2EE Key Input (shown when E2EE is enabled) -->
|
||||
@if (isE2EEEnabled) {
|
||||
<mat-form-field appearance="outline" class="e2eekey-field fade-in">
|
||||
<mat-label>Encryption Key</mat-label>
|
||||
<input
|
||||
id="participant-e2eekey-input"
|
||||
matInput
|
||||
type="password"
|
||||
placeholder="Enter room encryption key"
|
||||
formControlName="e2eeKey"
|
||||
required
|
||||
/>
|
||||
<mat-icon matSuffix class="ov-action-icon">vpn_key</mat-icon>
|
||||
@if (participantForm.get('e2eeKey')?.hasError('required')) {
|
||||
<mat-error> The encryption key is <strong>required</strong> </mat-error>
|
||||
}
|
||||
<mat-hint>This room requires an encryption key to join</mat-hint>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
id="participant-name-submit"
|
||||
type="submit"
|
||||
class="join-button"
|
||||
[disabled]="participantForm.invalid"
|
||||
[disabled]="!participantForm.valid"
|
||||
>
|
||||
<span>Join Meeting</span>
|
||||
</button>
|
||||
@ -66,7 +93,7 @@
|
||||
</mat-card>
|
||||
|
||||
<!-- View Recordings Card -->
|
||||
@if (showRecordingsCard) {
|
||||
@if (showRecordingCard) {
|
||||
<mat-card class="action-card secondary-card fade-in-delayed">
|
||||
<mat-card-header class="card-header">
|
||||
<mat-icon class="ov-recording-icon card-icon">video_library</mat-icon>
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
@include design-tokens.ov-flex-center;
|
||||
flex-direction: column;
|
||||
gap: var(--ov-meet-spacing-md);
|
||||
margin-bottom: var(--ov-meet-spacing-xxl);
|
||||
margin-bottom: var(--ov-meet-spacing-lg);
|
||||
text-align: center;
|
||||
|
||||
.room-icon {
|
||||
@ -24,16 +24,58 @@
|
||||
}
|
||||
|
||||
.room-info {
|
||||
flex: 1;
|
||||
.room-title {
|
||||
margin: 0;
|
||||
font-size: var(--ov-meet-font-size-hero);
|
||||
font-weight: var(--ov-meet-font-weight-light);
|
||||
color: var(--ov-meet-text-primary);
|
||||
line-height: var(--ov-meet-line-height-tight);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--ov-meet-spacing-md);
|
||||
flex-wrap: wrap;
|
||||
|
||||
@include design-tokens.ov-tablet-down {
|
||||
font-size: var(--ov-meet-font-size-xxl);
|
||||
flex-direction: column;
|
||||
gap: var(--ov-meet-spacing-sm);
|
||||
|
||||
.encryption-badge {
|
||||
font-size: var(--ov-meet-font-size-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.encryption-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ov-meet-spacing-xs);
|
||||
padding: var(--ov-meet-spacing-xs) var(--ov-meet-spacing-md);
|
||||
background: linear-gradient(135deg, rgba(33, 150, 243, 0.15) 0%, rgba(33, 150, 243, 0.08) 100%);
|
||||
border: 1px solid rgba(33, 150, 243, 0.3);
|
||||
border-radius: var(--ov-meet-radius-lg);
|
||||
font-size: var(--ov-meet-font-size-sm);
|
||||
font-weight: var(--ov-meet-font-weight-medium);
|
||||
color: var(--ov-meet-color-info);
|
||||
|
||||
.badge-icon {
|
||||
@include design-tokens.ov-icon(sm);
|
||||
color: var(--ov-meet-color-info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// E2EE Warning Container
|
||||
.e2ee-warning-container {
|
||||
margin-bottom: var(--ov-meet-spacing-lg);
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
// Action Cards Grid - Responsive layout
|
||||
.action-cards-grid {
|
||||
@include design-tokens.ov-grid-responsive(320px);
|
||||
@ -172,15 +214,21 @@
|
||||
.join-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ov-meet-spacing-lg);
|
||||
gap: var(--ov-meet-spacing-sm);
|
||||
flex: 1;
|
||||
|
||||
.name-field {
|
||||
width: 100%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mat-mdc-form-field-icon-suffix {
|
||||
color: var(--ov-meet-text-hint);
|
||||
}
|
||||
.e2eeey-field {
|
||||
width: 100%;
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
.mat-mdc-form-field-icon-suffix {
|
||||
color: var(--ov-meet-text-hint);
|
||||
}
|
||||
|
||||
.join-button {
|
||||
|
||||
@ -6,7 +6,7 @@ import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { ShareMeetingLinkComponent } from '../share-meeting-link/share-meeting-link.component';
|
||||
import { ShareMeetingLinkComponent } from '../../components';
|
||||
|
||||
/**
|
||||
* Reusable component for the meeting lobby page.
|
||||
@ -45,9 +45,9 @@ export class MeetingLobbyComponent {
|
||||
@Input() roomClosed = false;
|
||||
|
||||
/**
|
||||
* Whether to show the recordings card
|
||||
* Whether to show the recording card
|
||||
*/
|
||||
@Input() showRecordingsCard = false;
|
||||
@Input() showRecordingCard = false;
|
||||
|
||||
/**
|
||||
* Whether to show the share meeting link component
|
||||
@ -64,6 +64,11 @@ export class MeetingLobbyComponent {
|
||||
*/
|
||||
@Input() backButtonText = 'Back';
|
||||
|
||||
/**
|
||||
* Whether E2EE is enabled for the meeting
|
||||
*/
|
||||
@Input() isE2EEEnabled = false;
|
||||
|
||||
/**
|
||||
* The participant form group
|
||||
*/
|
||||
|
||||
@ -13,6 +13,7 @@ export interface LobbyState {
|
||||
showRecordingCard: boolean;
|
||||
showBackButton: boolean;
|
||||
backButtonText: string;
|
||||
isE2EEEnabled: boolean;
|
||||
participantForm: FormGroup;
|
||||
participantToken: string;
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
@if (isBasicCreation()) {
|
||||
<!-- Basic Room Creation -->
|
||||
<ov-room-basic-creation
|
||||
(createRoom)="createRoom($event)"
|
||||
(createRoom)="createRoomBasic($event)"
|
||||
(openAdvancedMode)="onOpenAdvancedMode()"
|
||||
></ov-room-basic-creation>
|
||||
} @else {
|
||||
@ -93,7 +93,7 @@
|
||||
(next)="onNext()"
|
||||
(cancel)="onCancel()"
|
||||
(back)="onBack()"
|
||||
(finish)="onFinish()"
|
||||
(finish)="createRoomAdvance()"
|
||||
>
|
||||
</ov-wizard-nav>
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { StepIndicatorComponent, WizardNavComponent } from '../../../../components';
|
||||
import { WizardNavigationConfig, WizardStep } from '../../../../models';
|
||||
import { NavigationService, NotificationService, RoomService, RoomWizardStateService } from '../../../../services';
|
||||
import { MeetRoomOptions } from '@openvidu-meet/typings';
|
||||
import { BaseRoomOptions, MeetRoomOptions } from '@openvidu-meet/typings';
|
||||
import { RoomBasicCreationComponent } from '../room-basic-creation/room-basic-creation.component';
|
||||
import { RecordingConfigComponent } from './steps/recording-config/recording-config.component';
|
||||
import { RecordingLayoutComponent } from './steps/recording-layout/recording-layout.component';
|
||||
@ -125,9 +125,18 @@ export class RoomWizardComponent implements OnInit {
|
||||
await this.navigationService.navigateTo('rooms', undefined, true);
|
||||
}
|
||||
|
||||
async createRoom(roomName?: string) {
|
||||
async createRoomBasic(roomName?: string) {
|
||||
try {
|
||||
const { moderatorUrl } = await this.roomService.createRoom({ roomName });
|
||||
// Create room with basic config including e2ee: false (default settings)
|
||||
const { moderatorUrl } = await this.roomService.createRoom({
|
||||
roomName,
|
||||
config: {
|
||||
chat: { enabled: true },
|
||||
recording: { enabled: true },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: false }
|
||||
}
|
||||
});
|
||||
|
||||
// Extract the path and query parameters from the moderator URL and navigate to it
|
||||
const url = new URL(moderatorUrl);
|
||||
@ -144,8 +153,8 @@ export class RoomWizardComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
async onFinish() {
|
||||
const roomOptions = this.wizardService.roomOptions();
|
||||
async createRoomAdvance() {
|
||||
const roomOptions: BaseRoomOptions = this.wizardService.roomOptions();
|
||||
console.log('Wizard completed with data:', roomOptions);
|
||||
|
||||
// Activate loading state
|
||||
|
||||
@ -41,9 +41,39 @@
|
||||
margin-bottom: var(--ov-meet-spacing-md);
|
||||
|
||||
.recording-form {
|
||||
.e2ee-warning {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--ov-meet-spacing-sm);
|
||||
padding: var(--ov-meet-spacing-md);
|
||||
margin-bottom: var(--ov-meet-spacing-lg);
|
||||
background: linear-gradient(135deg, var(--ov-meet-color-info-alpha-10) 0%, var(--ov-meet-color-info-alpha-5) 100%);
|
||||
border: 1px solid var(--ov-meet-color-info-alpha-30);
|
||||
border-radius: var(--ov-meet-border-radius-md);
|
||||
|
||||
.warning-icon {
|
||||
@include design-tokens.ov-icon(md);
|
||||
color: var(--ov-meet-color-info);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: var(--ov-meet-font-size-sm);
|
||||
color: var(--ov-meet-text-primary);
|
||||
line-height: var(--ov-meet-line-height-normal);
|
||||
}
|
||||
}
|
||||
|
||||
.options-grid {
|
||||
@include design-tokens.ov-grid-responsive(260px);
|
||||
gap: var(--ov-meet-spacing-md);
|
||||
|
||||
&.disabled-grid {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.access-selection-section {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
@ -18,30 +17,33 @@ interface RecordingAccessOption {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ov-recording-config',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatCardModule,
|
||||
MatRadioModule,
|
||||
MatSelectModule,
|
||||
MatFormFieldModule,
|
||||
SelectableCardComponent
|
||||
],
|
||||
templateUrl: './recording-config.component.html',
|
||||
styleUrl: './recording-config.component.scss'
|
||||
selector: 'ov-recording-config',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatCardModule,
|
||||
MatRadioModule,
|
||||
MatSelectModule,
|
||||
MatFormFieldModule,
|
||||
SelectableCardComponent
|
||||
],
|
||||
templateUrl: './recording-config.component.html',
|
||||
styleUrl: './recording-config.component.scss'
|
||||
})
|
||||
export class RecordingConfigComponent implements OnDestroy {
|
||||
recordingForm: FormGroup;
|
||||
isAnimatingOut = false;
|
||||
|
||||
// Store the previous E2EE state before recording disables it
|
||||
private e2eeStateBeforeRecording: boolean | null = null;
|
||||
|
||||
recordingOptions: SelectableOption[] = [
|
||||
{
|
||||
id: 'enabled',
|
||||
title: 'Allow Recording',
|
||||
description:
|
||||
'Enable recording capabilities for this room. Recordings can be started manually or automatically.',
|
||||
'Enable recording features for this room, allowing authorized participants to start and manage recordings.',
|
||||
icon: 'video_library'
|
||||
// recommended: true
|
||||
},
|
||||
@ -102,7 +104,43 @@ export class RecordingConfigComponent implements OnDestroy {
|
||||
const previouslyEnabled = this.isRecordingEnabled;
|
||||
const willBeEnabled = event.optionId === 'enabled';
|
||||
|
||||
// If we are disabling the recording, we want to animate out
|
||||
const configStep = this.wizardState.steps().find((step) => step.id === 'config');
|
||||
|
||||
// Handle E2EE state when recording changes
|
||||
if (configStep) {
|
||||
if (!previouslyEnabled && willBeEnabled) {
|
||||
// Enabling recording: save E2EE state and disable it if needed
|
||||
const e2eeEnabled = configStep.formGroup.get('e2eeEnabled')?.value;
|
||||
|
||||
if (e2eeEnabled) {
|
||||
// Save the E2EE state before disabling it
|
||||
this.e2eeStateBeforeRecording = true;
|
||||
|
||||
// Disable E2EE when enabling recording
|
||||
configStep.formGroup.patchValue(
|
||||
{
|
||||
e2eeEnabled: false
|
||||
},
|
||||
{ emitEvent: true }
|
||||
);
|
||||
}
|
||||
} else if (previouslyEnabled && !willBeEnabled) {
|
||||
// Disabling recording: restore E2EE state if it was saved
|
||||
if (this.e2eeStateBeforeRecording !== null) {
|
||||
configStep.formGroup.patchValue(
|
||||
{
|
||||
e2eeEnabled: this.e2eeStateBeforeRecording
|
||||
},
|
||||
{ emitEvent: true }
|
||||
);
|
||||
|
||||
// Clear the saved state
|
||||
this.e2eeStateBeforeRecording = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle recording form update with animation
|
||||
if (previouslyEnabled && !willBeEnabled) {
|
||||
this.isAnimatingOut = true;
|
||||
// Wait for the animation to finish before updating the form
|
||||
|
||||
@ -13,6 +13,55 @@
|
||||
<form [formGroup]="configForm" class="config-form">
|
||||
<!-- Config Cards Grid -->
|
||||
<div class="config-grid">
|
||||
<!-- End-to-End Encryption Card -->
|
||||
<mat-card class="config-card e2ee-card">
|
||||
<mat-card-content>
|
||||
<div class="card-header">
|
||||
<div class="icon-title-group">
|
||||
<mat-icon class="feature-icon">lock</mat-icon>
|
||||
<div class="title-group">
|
||||
<h4 class="card-title">End-to-End Encryption</h4>
|
||||
<p class="card-description">
|
||||
Add an extra layer of security to your meetings. No one outside the meeting, not
|
||||
even anyone with server access, can see or hear the conversation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mat-slide-toggle
|
||||
[checked]="e2eeEnabled"
|
||||
(change)="onE2EEToggleChange($event)"
|
||||
color="primary"
|
||||
class="feature-toggle"
|
||||
>
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
<!-- E2EE Info Section -->
|
||||
@if (e2eeEnabled) {
|
||||
<div class="e2ee-info-section fade-in">
|
||||
<div class="warning-card">
|
||||
<mat-icon class="warning-icon">warning</mat-icon>
|
||||
<div class="warning-content">
|
||||
<p class="warning-title">Restrictions</p>
|
||||
<ul class="restrictions-list">
|
||||
<li>
|
||||
All participants must use the same encryption key to see and hear each
|
||||
other.
|
||||
<span class="restriction-detail">
|
||||
Participants who enter an incorrect key will not receive an error
|
||||
message but will be unable to see or hear others.
|
||||
</span>
|
||||
</li>
|
||||
<li>Recording is unavailable while encryption is enabled.</li>
|
||||
<li>Chat messages are not protected by end-to-end encryption.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<!-- Chat Settings Card -->
|
||||
<mat-card class="config-card">
|
||||
<mat-card-content>
|
||||
|
||||
@ -65,6 +65,12 @@
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: default;
|
||||
|
||||
&.e2ee-card {
|
||||
.feature-icon {
|
||||
color: var(--ov-meet-color-info) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--ov-meet-border-primary);
|
||||
box-shadow: var(--ov-meet-shadow-md);
|
||||
@ -118,6 +124,78 @@
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.e2ee-info-section {
|
||||
margin-top: var(--ov-meet-spacing-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ov-meet-spacing-sm);
|
||||
animation: slideIn 0.3s ease-out;
|
||||
|
||||
.warning-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--ov-meet-spacing-sm);
|
||||
padding: var(--ov-meet-spacing-md);
|
||||
border-radius: var(--ov-meet-radius-sm);
|
||||
}
|
||||
|
||||
.warning-card {
|
||||
background: rgba(255, 152, 0, 0.08);
|
||||
border: 1px solid rgba(255, 152, 0, 0.2);
|
||||
|
||||
.warning-icon {
|
||||
@include design-tokens.ov-icon(md);
|
||||
color: var(--ov-meet-color-warning);
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.warning-content {
|
||||
flex: 1;
|
||||
|
||||
.warning-title {
|
||||
margin: 0 0 var(--ov-meet-spacing-xs) 0;
|
||||
font-size: var(--ov-meet-font-size-sm);
|
||||
font-weight: var(--ov-meet-font-weight-semibold);
|
||||
color: var(--ov-meet-color-warning);
|
||||
}
|
||||
|
||||
.restrictions-list {
|
||||
margin: 0;
|
||||
padding-left: var(--ov-meet-spacing-md);
|
||||
font-size: var(--ov-meet-font-size-sm);
|
||||
line-height: var(--ov-meet-line-height-relaxed);
|
||||
color: var(--ov-meet-text-primary);
|
||||
|
||||
li {
|
||||
margin-bottom: var(--ov-meet-spacing-xs);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.restriction-detail {
|
||||
display: block;
|
||||
color: var(--ov-meet-text-secondary);
|
||||
margin-top: var(--ov-meet-spacing-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,8 @@ export class RoomConfigComponent implements OnDestroy {
|
||||
configForm: FormGroup;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
// Store the previous recording state before E2EE disables it
|
||||
private recordingStateBeforeE2EE: string | null = null;
|
||||
|
||||
constructor(private wizardService: RoomWizardStateService) {
|
||||
const currentStep = this.wizardService.currentStep();
|
||||
@ -32,13 +34,18 @@ export class RoomConfigComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
private saveFormData(formValue: any): void {
|
||||
const isE2EEEnabled = formValue.e2eeEnabled ?? false;
|
||||
|
||||
const stepData: any = {
|
||||
config: {
|
||||
chat: {
|
||||
enabled: formValue.chatEnabled
|
||||
enabled: formValue.chatEnabled ?? false
|
||||
},
|
||||
virtualBackground: {
|
||||
enabled: formValue.virtualBackgroundsEnabled
|
||||
enabled: formValue.virtualBackgroundsEnabled ?? false
|
||||
},
|
||||
e2ee: {
|
||||
enabled: isE2EEEnabled
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -46,6 +53,41 @@ export class RoomConfigComponent implements OnDestroy {
|
||||
this.wizardService.updateStepData('config', stepData);
|
||||
}
|
||||
|
||||
onE2EEToggleChange(event: any): void {
|
||||
const isEnabled = event.checked;
|
||||
this.configForm.patchValue({
|
||||
e2eeEnabled: isEnabled
|
||||
});
|
||||
|
||||
const recordingStep = this.wizardService.steps().find(step => step.id === 'recording');
|
||||
if (!recordingStep) return;
|
||||
|
||||
if (isEnabled) {
|
||||
// Save the current recording state before disabling it
|
||||
const currentRecordingValue = recordingStep.formGroup.get('recordingEnabled')?.value;
|
||||
|
||||
// Only save if it's not already 'disabled' (to preserve user's original choice)
|
||||
if (currentRecordingValue !== 'disabled') {
|
||||
this.recordingStateBeforeE2EE = currentRecordingValue;
|
||||
}
|
||||
|
||||
// Disable recording automatically
|
||||
recordingStep.formGroup.patchValue({
|
||||
recordingEnabled: 'disabled'
|
||||
}, { emitEvent: true });
|
||||
} else {
|
||||
// Restore the previous recording state when E2EE is disabled
|
||||
if (this.recordingStateBeforeE2EE !== null) {
|
||||
recordingStep.formGroup.patchValue({
|
||||
recordingEnabled: this.recordingStateBeforeE2EE
|
||||
}, { emitEvent: true });
|
||||
|
||||
// Clear the saved state
|
||||
this.recordingStateBeforeE2EE = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onChatToggleChange(event: any): void {
|
||||
const isEnabled = event.checked;
|
||||
this.configForm.patchValue({ chatEnabled: isEnabled });
|
||||
@ -61,6 +103,10 @@ export class RoomConfigComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
get virtualBackgroundsEnabled(): boolean {
|
||||
return this.configForm.value.virtualBackgroundsEnabled || false;
|
||||
return this.configForm.value.virtualBackgroundEnabled ?? false;
|
||||
}
|
||||
|
||||
get e2eeEnabled(): boolean {
|
||||
return this.configForm.value.e2eeEnabled ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
[prejoinDisplayParticipantName]="false"
|
||||
[videoEnabled]="features().videoEnabled"
|
||||
[audioEnabled]="features().audioEnabled"
|
||||
[e2eeKey]="e2eeKey"
|
||||
[toolbarRoomName]="roomName"
|
||||
[toolbarCameraButton]="features().showCamera"
|
||||
[toolbarMicrophoneButton]="features().showMicrophone"
|
||||
|
||||
@ -143,14 +143,9 @@ export class MeetingComponent implements OnInit {
|
||||
protected lobbyInputs = computed(() => {
|
||||
if (!this.lobbyState) return {};
|
||||
return this.pluginManager.getLobbyInputs(
|
||||
this.roomName,
|
||||
`${this.hostname}/room/${this.roomId}`,
|
||||
this.lobbyState.roomClosed,
|
||||
this.lobbyState.showRecordingCard,
|
||||
!this.lobbyState.roomClosed && this.features().canModerateRoom,
|
||||
this.lobbyState.showBackButton,
|
||||
this.lobbyState.backButtonText,
|
||||
this.lobbyState.participantForm,
|
||||
this.lobbyState,
|
||||
this.hostname,
|
||||
this.features().canModerateRoom,
|
||||
() => this.submitAccessMeeting(),
|
||||
() => this.lobbyService.goToRecordings(),
|
||||
() => this.lobbyService.goBack(),
|
||||
@ -183,6 +178,9 @@ export class MeetingComponent implements OnInit {
|
||||
get participantName(): string {
|
||||
return this.lobbyService.participantName;
|
||||
}
|
||||
get e2eeKey(): string {
|
||||
return this.lobbyService.e2eeKey;
|
||||
}
|
||||
|
||||
get participantToken(): string {
|
||||
return this.lobbyState!.participantToken;
|
||||
|
||||
@ -36,8 +36,10 @@ export class MeetingLobbyService {
|
||||
showRecordingCard: false,
|
||||
showBackButton: true,
|
||||
backButtonText: 'Back',
|
||||
isE2EEEnabled: false,
|
||||
participantForm: new FormGroup({
|
||||
name: new FormControl('', [Validators.required])
|
||||
name: new FormControl('', [Validators.required]),
|
||||
e2eeKey: new FormControl('')
|
||||
}),
|
||||
participantToken: ''
|
||||
};
|
||||
@ -70,6 +72,14 @@ export class MeetingLobbyService {
|
||||
return value.name.trim();
|
||||
}
|
||||
|
||||
get e2eeKey(): string {
|
||||
const { valid, value } = this.state.participantForm;
|
||||
if (!valid || !value.e2eeKey?.trim()) {
|
||||
return '';
|
||||
}
|
||||
return value.e2eeKey.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the lobby state by fetching room data and configuring UI
|
||||
*/
|
||||
@ -78,6 +88,13 @@ export class MeetingLobbyService {
|
||||
this.state.roomSecret = this.roomService.getRoomSecret();
|
||||
this.state.room = await this.roomService.getRoom(this.state.roomId);
|
||||
this.state.roomClosed = this.state.room.status === MeetRoomStatus.CLOSED;
|
||||
this.state.isE2EEEnabled = this.state.room.config.e2ee?.enabled || false;
|
||||
|
||||
// If E2EE is enabled, require e2eeKey
|
||||
if (this.state.isE2EEEnabled) {
|
||||
this.state.participantForm.get('e2eeKey')?.setValidators([Validators.required]);
|
||||
this.state.participantForm.get('e2eeKey')?.updateValueAndValidity();
|
||||
}
|
||||
|
||||
await this.setBackButtonText();
|
||||
await this.checkForRecordings();
|
||||
@ -86,7 +103,6 @@ export class MeetingLobbyService {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles the back button click event and navigates accordingly
|
||||
* If in embedded mode, it closes the WebComponentManagerService
|
||||
@ -134,6 +150,12 @@ export class MeetingLobbyService {
|
||||
throw new Error('Participant form is invalid');
|
||||
}
|
||||
|
||||
// For E2EE rooms, validate passkey
|
||||
if (this.state.isE2EEEnabled && !this.e2eeKey) {
|
||||
console.warn('E2EE key is required for encrypted rooms.');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.generateParticipantToken();
|
||||
await this.addParticipantNameToUrl();
|
||||
await this.roomService.loadRoomConfig(this.state.roomId);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Injectable, Optional, Inject } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { CustomParticipantModel } from '../../models';
|
||||
import { CustomParticipantModel, LobbyState } from '../../models';
|
||||
import { MeetingActionHandler, MEETING_ACTION_HANDLER_TOKEN, ParticipantControls } from '../../customization';
|
||||
import { ParticipantService } from '../participant.service';
|
||||
|
||||
@ -27,11 +26,7 @@ export class MeetingPluginManagerService {
|
||||
/**
|
||||
* Prepares inputs for the toolbar additional buttons plugin
|
||||
*/
|
||||
getToolbarAdditionalButtonsInputs(
|
||||
canModerateRoom: boolean,
|
||||
isMobile: boolean,
|
||||
onCopyLink: () => void
|
||||
) {
|
||||
getToolbarAdditionalButtonsInputs(canModerateRoom: boolean, isMobile: boolean, onCopyLink: () => void) {
|
||||
return {
|
||||
showCopyLinkButton: canModerateRoom,
|
||||
showLeaveMenu: false,
|
||||
@ -61,11 +56,7 @@ export class MeetingPluginManagerService {
|
||||
/**
|
||||
* Prepares inputs for the participant panel "after local participant" plugin
|
||||
*/
|
||||
getParticipantPanelAfterLocalInputs(
|
||||
canModerateRoom: boolean,
|
||||
meetingUrl: string,
|
||||
onCopyLink: () => void
|
||||
) {
|
||||
getParticipantPanelAfterLocalInputs(canModerateRoom: boolean, meetingUrl: string, onCopyLink: () => void) {
|
||||
return {
|
||||
showShareLink: canModerateRoom,
|
||||
meetingUrl,
|
||||
@ -76,11 +67,7 @@ export class MeetingPluginManagerService {
|
||||
/**
|
||||
* Prepares inputs for the layout additional elements plugin
|
||||
*/
|
||||
getLayoutAdditionalElementsInputs(
|
||||
showOverlay: boolean,
|
||||
meetingUrl: string,
|
||||
onCopyLink: () => void
|
||||
) {
|
||||
getLayoutAdditionalElementsInputs(showOverlay: boolean, meetingUrl: string, onCopyLink: () => void) {
|
||||
return {
|
||||
showOverlay,
|
||||
meetingUrl,
|
||||
@ -118,27 +105,36 @@ export class MeetingPluginManagerService {
|
||||
* Prepares inputs for the lobby plugin
|
||||
*/
|
||||
getLobbyInputs(
|
||||
roomName: string,
|
||||
meetingUrl: string,
|
||||
roomClosed: boolean,
|
||||
showRecordingCard: boolean,
|
||||
showShareLink: boolean,
|
||||
showBackButton: boolean,
|
||||
backButtonText: string,
|
||||
participantForm: FormGroup,
|
||||
lobbyState: LobbyState,
|
||||
hostname: string,
|
||||
canModerateRoom: boolean,
|
||||
onFormSubmit: () => void,
|
||||
onViewRecordings: () => void,
|
||||
onBack: () => void,
|
||||
onCopyLink: () => void
|
||||
) {
|
||||
const {
|
||||
room,
|
||||
roomId,
|
||||
roomClosed,
|
||||
showRecordingCard,
|
||||
showBackButton,
|
||||
backButtonText,
|
||||
isE2EEEnabled,
|
||||
participantForm
|
||||
} = lobbyState;
|
||||
const meetingUrl = `${hostname}/room/${roomId}`;
|
||||
const showShareLink = !roomClosed && canModerateRoom;
|
||||
|
||||
return {
|
||||
roomName,
|
||||
roomName: room?.roomName || 'Room',
|
||||
meetingUrl,
|
||||
roomClosed,
|
||||
showRecordingsCard: showRecordingCard,
|
||||
showRecordingCard,
|
||||
showShareLink,
|
||||
showBackButton,
|
||||
backButtonText,
|
||||
isE2EEEnabled,
|
||||
participantForm,
|
||||
formSubmittedFn: onFormSubmit,
|
||||
viewRecordingsClickedFn: onViewRecordings,
|
||||
@ -177,7 +173,8 @@ export class MeetingPluginManagerService {
|
||||
|
||||
// Calculate if current moderator can revoke the moderator role from the target participant
|
||||
// Only allow if target is not an original moderator
|
||||
const canRevokeModeratorRole = currentUserIsModerator && !isCurrentUser && participantIsModerator && !participantIsOriginalModerator;
|
||||
const canRevokeModeratorRole =
|
||||
currentUserIsModerator && !isCurrentUser && participantIsModerator && !participantIsOriginalModerator;
|
||||
|
||||
// Calculate if current moderator can kick the target participant
|
||||
// Only allow if target is not an original moderator
|
||||
|
||||
@ -16,7 +16,8 @@ const DEFAULT_CONFIG: MeetRoomConfig = {
|
||||
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
||||
},
|
||||
chat: { enabled: true },
|
||||
virtualBackground: { enabled: true }
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: false }
|
||||
};
|
||||
|
||||
/**
|
||||
@ -188,7 +189,8 @@ export class RoomWizardStateService {
|
||||
isVisible: true,
|
||||
formGroup: this.formBuilder.group({
|
||||
chatEnabled: initialRoomOptions.config!.chat.enabled,
|
||||
virtualBackgroundsEnabled: initialRoomOptions.config!.virtualBackground.enabled
|
||||
virtualBackgroundsEnabled: initialRoomOptions.config!.virtualBackground.enabled,
|
||||
e2eeEnabled: initialRoomOptions.config!.e2ee?.enabled ?? false
|
||||
})
|
||||
}
|
||||
];
|
||||
@ -259,9 +261,16 @@ export class RoomWizardStateService {
|
||||
...currentOptions.config?.virtualBackground,
|
||||
...stepData.config?.virtualBackground
|
||||
},
|
||||
e2ee: {
|
||||
...currentOptions.config?.e2ee,
|
||||
...stepData.config?.e2ee
|
||||
},
|
||||
recording: {
|
||||
...currentOptions.config?.recording,
|
||||
...stepData.config?.recording
|
||||
// If recording is explicitly set in stepData, use it
|
||||
...(stepData.config?.recording?.enabled !== undefined && {
|
||||
enabled: stepData.config.recording.enabled
|
||||
})
|
||||
}
|
||||
} as MeetRoomConfig
|
||||
};
|
||||
|
||||
@ -24,6 +24,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Animations
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
max-height: 0;
|
||||
margin: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
max-height: 150px;
|
||||
margin: var(--ov-meet-spacing-lg) 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInFromTop {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
"test:e2e-core-events": "playwright test tests/e2e/core/events.test.ts",
|
||||
"test:e2e-core-webhooks": "playwright test tests/e2e/core/webhooks.test.ts",
|
||||
"test:e2e-ui-features": "playwright test tests/e2e/ui-feature-config.test.ts",
|
||||
"test:e2e-e2ee-ui": "playwright test tests/e2e/e2ee-ui.test.ts",
|
||||
"test:e2e-recording-access": "playwright test tests/e2e/recording-access.test.ts",
|
||||
"lint": "eslint 'src/**/*.ts'"
|
||||
},
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
openParticipantsPanel,
|
||||
prepareForJoiningRoom,
|
||||
removeParticipantModerator,
|
||||
waitForElementInIframe
|
||||
waitForElementInIframe,
|
||||
} from '../../helpers/function-helpers.js';
|
||||
|
||||
let subscribedToAppErrors = false;
|
||||
@ -39,6 +39,16 @@ test.describe('Moderation Functionality Tests', () => {
|
||||
roomId = await createTestRoom('moderation-test-room');
|
||||
});
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
const tempContext = await browser.newContext();
|
||||
const tempPage = await tempContext.newPage();
|
||||
await deleteAllRooms(tempPage);
|
||||
await deleteAllRecordings(tempPage);
|
||||
|
||||
await tempContext.close();
|
||||
await tempPage.close();
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
if (!subscribedToAppErrors) {
|
||||
page.on('console', (msg) => {
|
||||
|
||||
412
meet-ce/frontend/webcomponent/tests/e2e/e2ee-ui.test.ts
Normal file
412
meet-ce/frontend/webcomponent/tests/e2e/e2ee-ui.test.ts
Normal file
@ -0,0 +1,412 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { MeetRecordingAccess } from '../../../../typings/src/room-config';
|
||||
import { MEET_TESTAPP_URL } from '../config';
|
||||
import {
|
||||
closeMoreOptionsMenu,
|
||||
countElementsInIframe,
|
||||
createTestRoom,
|
||||
deleteAllRecordings,
|
||||
deleteAllRooms,
|
||||
interactWithElementInIframe,
|
||||
joinRoomAs,
|
||||
leaveRoom,
|
||||
openMoreOptionsMenu,
|
||||
prepareForJoiningRoom,
|
||||
updateRoomConfig,
|
||||
waitForElementInIframe
|
||||
} from '../helpers/function-helpers';
|
||||
|
||||
let subscribedToAppErrors = false;
|
||||
|
||||
test.describe('E2EE UI Tests', () => {
|
||||
let roomId: string;
|
||||
let participantName: string;
|
||||
|
||||
// ==========================================
|
||||
// SETUP & TEARDOWN
|
||||
// ==========================================
|
||||
|
||||
test.beforeAll(async () => {
|
||||
// Create a test room before all tests
|
||||
roomId = await createTestRoom('test-room-e2ee');
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
if (!subscribedToAppErrors) {
|
||||
page.on('console', (msg) => {
|
||||
const type = msg.type();
|
||||
const tag = type === 'error' ? 'ERROR' : type === 'warning' ? 'WARNING' : 'LOG';
|
||||
console.log('[' + tag + ']', msg.text());
|
||||
});
|
||||
subscribedToAppErrors = true;
|
||||
}
|
||||
|
||||
participantName = `P-${Math.random().toString(36).substring(2, 9)}`;
|
||||
});
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
const tempContext = await browser.newContext();
|
||||
const tempPage = await tempContext.newPage();
|
||||
await deleteAllRooms(tempPage);
|
||||
await deleteAllRecordings(tempPage);
|
||||
await tempContext.close();
|
||||
await tempPage.close();
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// E2EE LOBBY UI TESTS
|
||||
// ==========================================
|
||||
|
||||
test.describe('E2EE Lobby Elements', () => {
|
||||
test('should show E2EE key input and badge in lobby when E2EE is enabled', async ({ page }) => {
|
||||
// Enable E2EE
|
||||
await updateRoomConfig(roomId, {
|
||||
chat: { enabled: true },
|
||||
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: true }
|
||||
});
|
||||
|
||||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
||||
await page.click('#join-as-speaker');
|
||||
|
||||
const component = page.locator('openvidu-meet');
|
||||
await expect(component).toBeVisible();
|
||||
|
||||
// Wait for participant name input
|
||||
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
|
||||
|
||||
// Check that E2EE badge is visible
|
||||
const e2eeBadge = await waitForElementInIframe(page, '.encryption-badge', { state: 'visible' });
|
||||
await expect(e2eeBadge).toBeVisible();
|
||||
await expect(e2eeBadge).toContainText('end-to-end encrypted');
|
||||
|
||||
// Check that E2EE key input is visible
|
||||
const e2eeKeyInput = await waitForElementInIframe(page, '#participant-e2eekey-input', {
|
||||
state: 'visible'
|
||||
});
|
||||
await expect(e2eeKeyInput).toBeVisible();
|
||||
|
||||
// Check that the input has correct attributes
|
||||
await expect(e2eeKeyInput).toHaveAttribute('type', 'password');
|
||||
await expect(e2eeKeyInput).toHaveAttribute('required');
|
||||
});
|
||||
|
||||
test('should hide E2EE elements in lobby when E2EE is disabled', async ({ page }) => {
|
||||
// Disable E2EE
|
||||
await updateRoomConfig(roomId, {
|
||||
chat: { enabled: true },
|
||||
recording: { enabled: true, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: false }
|
||||
});
|
||||
|
||||
await page.goto(MEET_TESTAPP_URL);
|
||||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
||||
await page.click('#join-as-speaker');
|
||||
|
||||
const component = page.locator('openvidu-meet');
|
||||
await expect(component).toBeVisible();
|
||||
|
||||
// Wait for participant name input
|
||||
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
|
||||
|
||||
// Check that E2EE badge is hidden
|
||||
const e2eeBadge = await waitForElementInIframe(page, '.encryption-badge', { state: 'hidden' });
|
||||
await expect(e2eeBadge).toBeHidden();
|
||||
|
||||
// Check that E2EE key input is hidden
|
||||
const e2eeKeyInput = await waitForElementInIframe(page, '#participant-e2eekey-input', {
|
||||
state: 'hidden'
|
||||
});
|
||||
await expect(e2eeKeyInput).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// E2EE SESSION TESTS
|
||||
// ==========================================
|
||||
|
||||
test.describe('E2EE in Session', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
try {
|
||||
await leaveRoom(page);
|
||||
} catch (error) {
|
||||
// Ignore errors if already left
|
||||
}
|
||||
});
|
||||
|
||||
test('should allow participants to see and hear each other with correct E2EE key', async ({
|
||||
page,
|
||||
context
|
||||
}) => {
|
||||
// Enable E2EE
|
||||
await updateRoomConfig(roomId, {
|
||||
chat: { enabled: true },
|
||||
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: true }
|
||||
});
|
||||
|
||||
// Create a second page for participant 2
|
||||
const page2 = await context.newPage();
|
||||
|
||||
// Participant 1 joins with E2EE key
|
||||
await page.goto(MEET_TESTAPP_URL);
|
||||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
||||
await page.click('#join-as-speaker');
|
||||
|
||||
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
|
||||
await interactWithElementInIframe(page, '#participant-name-input', {
|
||||
action: 'fill',
|
||||
value: participantName
|
||||
});
|
||||
|
||||
// Fill E2EE key
|
||||
const e2eeKey = 'test-encryption-key-123';
|
||||
await interactWithElementInIframe(page, '#participant-e2eekey-input', {
|
||||
action: 'fill',
|
||||
value: e2eeKey
|
||||
});
|
||||
|
||||
await interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' });
|
||||
|
||||
// Wait for prejoin page and join
|
||||
await waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' });
|
||||
await interactWithElementInIframe(page, '#join-button', { action: 'click' });
|
||||
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
|
||||
|
||||
// Participant 2 joins with same E2EE key
|
||||
const participant2Name = `P2-${Math.random().toString(36).substring(2, 9)}`;
|
||||
await page2.goto(MEET_TESTAPP_URL);
|
||||
await prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId);
|
||||
await page2.click('#join-as-speaker');
|
||||
|
||||
await waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' });
|
||||
await interactWithElementInIframe(page2, '#participant-name-input', {
|
||||
action: 'fill',
|
||||
value: participant2Name
|
||||
});
|
||||
|
||||
// Fill same E2EE key
|
||||
await interactWithElementInIframe(page2, '#participant-e2eekey-input', {
|
||||
action: 'fill',
|
||||
value: e2eeKey
|
||||
});
|
||||
|
||||
await interactWithElementInIframe(page2, '#participant-name-submit', { action: 'click' });
|
||||
|
||||
// Wait for prejoin page and join
|
||||
await waitForElementInIframe(page2, 'ov-pre-join', { state: 'visible' });
|
||||
await interactWithElementInIframe(page2, '#join-button', { action: 'click' });
|
||||
await waitForElementInIframe(page2, 'ov-session', { state: 'visible' });
|
||||
|
||||
// Wait a bit for media to flow
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check that both participants can see each other's video elements
|
||||
const videoCount1 = await countElementsInIframe(page, '.OV_video-element');
|
||||
expect(videoCount1).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const videoCount2 = await countElementsInIframe(page2, '.OV_video-element');
|
||||
expect(videoCount2).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// Check that no encryption error poster is shown
|
||||
const encryptionError1 = await waitForElementInIframe(page, '.encryption-error-poster', {
|
||||
state: 'hidden'
|
||||
});
|
||||
await expect(encryptionError1).toBeHidden();
|
||||
|
||||
const encryptionError2 = await waitForElementInIframe(page2, '.encryption-error-poster', {
|
||||
state: 'hidden'
|
||||
});
|
||||
await expect(encryptionError2).toBeHidden();
|
||||
|
||||
// Cleanup participant 2
|
||||
await leaveRoom(page2);
|
||||
await page2.close();
|
||||
});
|
||||
|
||||
test('should show encryption error poster when using wrong E2EE key', async ({ page, context }) => {
|
||||
// Enable E2EE
|
||||
await updateRoomConfig(roomId, {
|
||||
chat: { enabled: true },
|
||||
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: true }
|
||||
});
|
||||
|
||||
// Create a second page for participant 2
|
||||
const page2 = await context.newPage();
|
||||
|
||||
// Participant 1 joins with E2EE key
|
||||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
||||
await page.click('#join-as-speaker');
|
||||
|
||||
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
|
||||
await interactWithElementInIframe(page, '#participant-name-input', {
|
||||
action: 'fill',
|
||||
value: participantName
|
||||
});
|
||||
|
||||
// Fill E2EE key
|
||||
const e2eeKey1 = 'correct-key-abc';
|
||||
await interactWithElementInIframe(page, '#participant-e2eekey-input', {
|
||||
action: 'fill',
|
||||
value: e2eeKey1
|
||||
});
|
||||
|
||||
await interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' });
|
||||
|
||||
// Wait for prejoin page and join
|
||||
await waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' });
|
||||
await interactWithElementInIframe(page, '#join-button', { action: 'click' });
|
||||
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
|
||||
|
||||
// Participant 2 joins with DIFFERENT E2EE key
|
||||
const participant2Name = `P2-${Math.random().toString(36).substring(2, 9)}`;
|
||||
await prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId);
|
||||
await page2.click('#join-as-speaker');
|
||||
|
||||
await waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' });
|
||||
await interactWithElementInIframe(page2, '#participant-name-input', {
|
||||
action: 'fill',
|
||||
value: participant2Name
|
||||
});
|
||||
|
||||
// Fill DIFFERENT E2EE key
|
||||
const e2eeKey2 = 'wrong-key-xyz';
|
||||
await interactWithElementInIframe(page2, '#participant-e2eekey-input', {
|
||||
action: 'fill',
|
||||
value: e2eeKey2
|
||||
});
|
||||
|
||||
await interactWithElementInIframe(page2, '#participant-name-submit', { action: 'click' });
|
||||
|
||||
// Wait for prejoin page and join
|
||||
await waitForElementInIframe(page2, 'ov-pre-join', { state: 'visible' });
|
||||
await interactWithElementInIframe(page2, '#join-button', { action: 'click' });
|
||||
await waitForElementInIframe(page2, 'ov-session', { state: 'visible' });
|
||||
|
||||
// Wait for encryption error to be detected
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Check that encryption error poster is shown on both sides
|
||||
// Each participant should see an encryption error for the other's video
|
||||
const videoPosterCount = await countElementsInIframe(page, '.encryption-error-poster');
|
||||
|
||||
//! FIXME: Temporarily expecting 2 posters due to audio and video streams (needs to be fixed in ov-components)
|
||||
expect(videoPosterCount).toBe(2);
|
||||
|
||||
const videoPosterCount2 = await countElementsInIframe(page2, '.encryption-error-poster');
|
||||
//! FIXME: Temporarily expecting 2 posters due to audio and video streams (needs to be fixed in ov-components)
|
||||
expect(videoPosterCount2).toBe(2);
|
||||
|
||||
// Add additional participant with correct key to verify they can see/hear each other
|
||||
const page3 = await context.newPage();
|
||||
const participant3Name = `P3-${Math.random().toString(36).substring(2, 9)}`;
|
||||
await prepareForJoiningRoom(page3, MEET_TESTAPP_URL, roomId);
|
||||
await page3.click('#join-as-speaker');
|
||||
|
||||
await waitForElementInIframe(page3, '#participant-name-input', { state: 'visible' });
|
||||
await interactWithElementInIframe(page3, '#participant-name-input', {
|
||||
action: 'fill',
|
||||
value: participant3Name
|
||||
});
|
||||
|
||||
// Fill CORRECT E2EE key
|
||||
await interactWithElementInIframe(page3, '#participant-e2eekey-input', {
|
||||
action: 'fill',
|
||||
value: e2eeKey1
|
||||
});
|
||||
|
||||
await interactWithElementInIframe(page3, '#participant-name-submit', { action: 'click' });
|
||||
|
||||
// Wait for prejoin page and join
|
||||
await waitForElementInIframe(page3, 'ov-pre-join', { state: 'visible' });
|
||||
await interactWithElementInIframe(page3, '#join-button', { action: 'click' });
|
||||
await waitForElementInIframe(page3, 'ov-session', { state: 'visible' });
|
||||
|
||||
// Wait a bit for media to flow
|
||||
await page3.waitForTimeout(2000);
|
||||
|
||||
// Check that participant 3 can see participant 1's video
|
||||
const videoCount3 = await countElementsInIframe(page3, '.OV_video-element');
|
||||
expect(videoCount3).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const videoPosterCount3 = await countElementsInIframe(page3, '.encryption-error-poster');
|
||||
//! FIXME: Temporarily expecting 2 posters due to audio and video streams (needs to be fixed in ov-components)
|
||||
expect(videoPosterCount3).toBe(2);
|
||||
|
||||
// Cleanup participant 2
|
||||
await Promise.all([leaveRoom(page2), leaveRoom(page3)]);
|
||||
await Promise.all([page2.close(), page3.close()]);
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// E2EE AND RECORDING INTERACTION TESTS
|
||||
// ==========================================
|
||||
|
||||
test.describe('E2EE and Recording', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
try {
|
||||
await leaveRoom(page, 'moderator');
|
||||
} catch (error) {
|
||||
// Ignore errors if already left
|
||||
}
|
||||
});
|
||||
|
||||
test('should hide recording button when E2EE is enabled', async ({ page }) => {
|
||||
// Enable E2EE (which should auto-disable recording)
|
||||
await updateRoomConfig(roomId, {
|
||||
chat: { enabled: true },
|
||||
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
|
||||
virtualBackground: { enabled: true },
|
||||
e2ee: { enabled: true }
|
||||
});
|
||||
|
||||
await page.goto(MEET_TESTAPP_URL);
|
||||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
||||
|
||||
// Join as moderator to access recording controls
|
||||
await page.click('#join-as-moderator');
|
||||
const component = page.locator('openvidu-meet');
|
||||
await expect(component).toBeVisible();
|
||||
|
||||
// Fill participant name
|
||||
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
|
||||
await interactWithElementInIframe(page, '#participant-name-input', {
|
||||
action: 'fill',
|
||||
value: participantName
|
||||
});
|
||||
|
||||
// Fill E2EE key
|
||||
const e2eeKey = 'test-key-recording';
|
||||
await interactWithElementInIframe(page, '#participant-e2eekey-input', {
|
||||
action: 'fill',
|
||||
value: e2eeKey
|
||||
});
|
||||
|
||||
await interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' });
|
||||
|
||||
// Wait for prejoin page and join
|
||||
await waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' });
|
||||
await interactWithElementInIframe(page, '#join-button', { action: 'click' });
|
||||
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
|
||||
|
||||
// Open more options menu
|
||||
await openMoreOptionsMenu(page);
|
||||
|
||||
// Check that recording button is not visible
|
||||
const recordingButton = await waitForElementInIframe(page, '#recording-btn', { state: 'hidden' });
|
||||
await expect(recordingButton).toBeHidden();
|
||||
|
||||
await closeMoreOptionsMenu(page);
|
||||
|
||||
// Also check that recording activities panel is not available
|
||||
const activitiesButton = await waitForElementInIframe(page, '#activities-panel-btn', { state: 'hidden' });
|
||||
await expect(activitiesButton).toBeHidden();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -58,6 +58,24 @@ export async function waitForElementInIframe(
|
||||
return elementLocator;
|
||||
}
|
||||
|
||||
export async function countElementsInIframe(
|
||||
page: Page,
|
||||
elementSelector: string,
|
||||
options: {
|
||||
componentSelector?: string;
|
||||
iframeSelector?: string;
|
||||
timeout?: number;
|
||||
state?: 'attached' | 'visible';
|
||||
} = {}
|
||||
): Promise<number> {
|
||||
const { componentSelector = 'openvidu-meet', iframeSelector = 'iframe' } = options;
|
||||
|
||||
const frameLocator = await getIframeInShadowDom(page, componentSelector, iframeSelector);
|
||||
const elements = frameLocator.locator(elementSelector);
|
||||
|
||||
return await elements.count();
|
||||
}
|
||||
|
||||
// Interact with an element inside an iframe within Shadow DOM
|
||||
export async function interactWithElementInIframe(
|
||||
page: Page,
|
||||
|
||||
@ -5,6 +5,7 @@ export interface MeetRoomConfig {
|
||||
chat: MeetChatConfig;
|
||||
recording: MeetRecordingConfig;
|
||||
virtualBackground: MeetVirtualBackgroundConfig;
|
||||
e2ee?: MeetE2EEConfig;
|
||||
// appearance: MeetAppearanceConfig;
|
||||
}
|
||||
|
||||
@ -30,6 +31,10 @@ export interface MeetVirtualBackgroundConfig {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface MeetE2EEConfig {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface MeetAppearanceConfig {
|
||||
themes: MeetRoomTheme[];
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
"description": "OpenVidu Meet Monorepo",
|
||||
"scripts": {
|
||||
"build": "pnpm run build:typings && pnpm run build:backend && pnpm run build:shared-components && pnpm run build:frontend && pnpm run build:webcomponent",
|
||||
"clean": "pnpm --filter @openvidu-meet/backend run clean && pnpm --filter @openvidu-meet/frontend run clean",
|
||||
"build:frontend": "pnpm --filter @openvidu-meet/frontend run build ${BASE_HREF:-/}",
|
||||
"build:backend": "pnpm --filter @openvidu-meet/backend run build",
|
||||
"build:webcomponent": "pnpm --filter openvidu-meet-webcomponent run build",
|
||||
|
||||
@ -320,6 +320,51 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- E2EE Config -->
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button
|
||||
class="accordion-button collapsed"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#e2eeCollapse"
|
||||
aria-expanded="false"
|
||||
aria-controls="e2eeCollapse"
|
||||
data-testid="e2ee-config-toggle"
|
||||
>
|
||||
End-to-End Encryption (E2EE) Settings
|
||||
</button>
|
||||
</h2>
|
||||
<div
|
||||
id="e2eeCollapse"
|
||||
class="accordion-collapse collapse"
|
||||
data-bs-parent="#configAccordion"
|
||||
>
|
||||
<div class="accordion-body">
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="config.e2ee.enabled"
|
||||
id="e2ee-enabled"
|
||||
class="form-check-input"
|
||||
data-testid="e2ee-enabled-checkbox"
|
||||
/>
|
||||
<label
|
||||
for="e2ee-enabled"
|
||||
class="form-check-label"
|
||||
>
|
||||
Enable End-to-End Encryption
|
||||
</label>
|
||||
</div>
|
||||
<div class="alert alert-info mt-2" role="alert">
|
||||
<small>
|
||||
<strong>Note:</strong> When E2EE is enabled, recording will be automatically disabled for security reasons.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -170,6 +170,9 @@ const processFormConfig = (body: any): any => {
|
||||
},
|
||||
virtualBackground: {
|
||||
enabled: body['config.virtualBackground.enabled'] === 'on'
|
||||
},
|
||||
e2ee: {
|
||||
enabled: body['config.e2ee.enabled'] === 'on'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user