diff --git a/.github/workflows/backend-integration-test.yaml b/.github/workflows/backend-integration-test.yaml index 47ce8eb8..48082f60 100644 --- a/.github/workflows/backend-integration-test.yaml +++ b/.github/workflows/backend-integration-test.yaml @@ -38,7 +38,7 @@ jobs: fail-fast: false matrix: include: - - test-name: 'Room Management API Tests (Rooms, Meetings, Participants)' + - test-name: 'Room Management API Tests (Rooms, Meetings)' test-script: 'test:integration-backend-room-management' - test-name: 'Webhook Tests' test-script: 'test:integration-backend-webhooks' diff --git a/meet-ce/backend/openapi/components/headers/set-cookie-access-token.yaml b/meet-ce/backend/openapi/components/headers/set-cookie-access-token.yaml deleted file mode 100644 index 4afa3636..00000000 --- a/meet-ce/backend/openapi/components/headers/set-cookie-access-token.yaml +++ /dev/null @@ -1,6 +0,0 @@ -description: > - The cookie containing the access token. - This cookie is used to authenticate the user in subsequent requests. -schema: - type: string -example: 'OvMeetAccessToken=token_123456; Path=/; HttpOnly; SameSite=Strict' diff --git a/meet-ce/backend/openapi/components/headers/set-cookie-participant-token.yaml b/meet-ce/backend/openapi/components/headers/set-cookie-participant-token.yaml deleted file mode 100644 index 7b3ab7c0..00000000 --- a/meet-ce/backend/openapi/components/headers/set-cookie-participant-token.yaml +++ /dev/null @@ -1,6 +0,0 @@ -description: > - The cookie containing the participant token. - This cookie is used to authenticate the participant in the room. -schema: - type: string -example: 'OvMeetParticipantToken=token_123456; Path=/; HttpOnly; SameSite=Strict' diff --git a/meet-ce/backend/openapi/components/headers/set-cookie-recording-token.yaml b/meet-ce/backend/openapi/components/headers/set-cookie-recording-token.yaml deleted file mode 100644 index 65e236ae..00000000 --- a/meet-ce/backend/openapi/components/headers/set-cookie-recording-token.yaml +++ /dev/null @@ -1,6 +0,0 @@ -description: > - The cookie containing the recording token. - This cookie is used to access the recordings in a room. -schema: - type: string -example: 'OvMeetRecordingToken=token_123456; Path=/; HttpOnly; SameSite=Strict' diff --git a/meet-ce/backend/openapi/components/headers/set-cookie-refresh-token.yaml b/meet-ce/backend/openapi/components/headers/set-cookie-refresh-token.yaml deleted file mode 100644 index fc19c5e7..00000000 --- a/meet-ce/backend/openapi/components/headers/set-cookie-refresh-token.yaml +++ /dev/null @@ -1,6 +0,0 @@ -description: > - The cookie containing the refresh token. - This cookie is used to refresh the access token when it expires. -schema: - type: string -example: 'OvMeetRefreshToken=token_123456; Path=/meet/internal-api/v1/auth; HttpOnly; SameSite=Strict' diff --git a/meet-ce/backend/openapi/components/parameters/internal/participant-identity.yaml b/meet-ce/backend/openapi/components/parameters/internal/participant-identity.yaml deleted file mode 100644 index b10b346b..00000000 --- a/meet-ce/backend/openapi/components/parameters/internal/participant-identity.yaml +++ /dev/null @@ -1,7 +0,0 @@ -name: participantIdentity -in: path -required: true -description: The identity of the participant. -schema: - type: string -example: 'Alice' diff --git a/meet-ce/backend/openapi/components/parameters/internal/secret.yaml b/meet-ce/backend/openapi/components/parameters/internal/secret.yaml index fd7b7780..c5633f28 100644 --- a/meet-ce/backend/openapi/components/parameters/internal/secret.yaml +++ b/meet-ce/backend/openapi/components/parameters/internal/secret.yaml @@ -1,6 +1,6 @@ name: secret in: path required: true -description: The secret value from the room URL used to connect to the room. +description: The secret value from the room URL used to access the room. schema: type: string diff --git a/meet-ce/backend/openapi/components/parameters/internal/x-participant-role.yaml b/meet-ce/backend/openapi/components/parameters/internal/x-participant-role.yaml deleted file mode 100644 index a4dbd5d7..00000000 --- a/meet-ce/backend/openapi/components/parameters/internal/x-participant-role.yaml +++ /dev/null @@ -1,12 +0,0 @@ -name: x-participant-role -in: header -description: | - The role of the participant in the meeting. It can be one of the following values: - - `moderator`: Can manage the room and its participants. - - `speaker`: Can publish media streams to the room. - - This is required to distinguish roles when multiple are present in the participant token -required: true -schema: - type: string - enum: ['moderator', 'speaker'] diff --git a/meet-ce/backend/openapi/components/parameters/recording-status.yaml b/meet-ce/backend/openapi/components/parameters/recording-status.yaml index ee469ce6..8a79debc 100644 --- a/meet-ce/backend/openapi/components/parameters/recording-status.yaml +++ b/meet-ce/backend/openapi/components/parameters/recording-status.yaml @@ -1,12 +1,7 @@ name: status in: query required: false -description: | - Filter recordings by their status. - - You can provide multiple statuses as a comma-separated list (e.g., `status=active,failed`). - - > ⚠️ **Note:** Using this filter may impact performance for large datasets. +description: Filter recordings by their status. schema: type: string enum: diff --git a/meet-ce/backend/openapi/components/requestBodies/internal/participant-token-request.yaml b/meet-ce/backend/openapi/components/requestBodies/internal/participant-token-request.yaml deleted file mode 100644 index cbbcf79f..00000000 --- a/meet-ce/backend/openapi/components/requestBodies/internal/participant-token-request.yaml +++ /dev/null @@ -1,6 +0,0 @@ -description: Participant details -required: true -content: - application/json: - schema: - $ref: '../../schemas/internal/meet-participant-options.yaml' diff --git a/meet-ce/backend/openapi/components/requestBodies/internal/room-member-token-request.yaml b/meet-ce/backend/openapi/components/requestBodies/internal/room-member-token-request.yaml new file mode 100644 index 00000000..da7506ac --- /dev/null +++ b/meet-ce/backend/openapi/components/requestBodies/internal/room-member-token-request.yaml @@ -0,0 +1,6 @@ +description: Room member token options +required: true +content: + application/json: + schema: + $ref: '../../schemas/internal/room-member-token-options.yaml' diff --git a/meet-ce/backend/openapi/components/requestBodies/update-room-status-request.yaml b/meet-ce/backend/openapi/components/requestBodies/update-room-status-request.yaml new file mode 100644 index 00000000..71318931 --- /dev/null +++ b/meet-ce/backend/openapi/components/requestBodies/update-room-status-request.yaml @@ -0,0 +1,17 @@ +description: New room status +content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: + - open + - active_meeting + - closed + example: closed + description: | + The new status of the room. Options are: + - open: The room will be open for new participants to join. + - closed: The room will be closed to new participants. diff --git a/meet-ce/backend/openapi/components/responses/internal/error-invalid-participant-role.yaml b/meet-ce/backend/openapi/components/responses/internal/error-invalid-participant-role.yaml deleted file mode 100644 index f8007dcf..00000000 --- a/meet-ce/backend/openapi/components/responses/internal/error-invalid-participant-role.yaml +++ /dev/null @@ -1,8 +0,0 @@ -description: Invalid participant role provided -content: - application/json: - schema: - $ref: '../../schemas/error.yaml' - example: - error: Participant Error - message: 'No valid participant role provided' diff --git a/meet-ce/backend/openapi/components/responses/internal/error-participant-already-exists.yaml b/meet-ce/backend/openapi/components/responses/internal/error-participant-already-exists.yaml deleted file mode 100644 index e8559ab7..00000000 --- a/meet-ce/backend/openapi/components/responses/internal/error-participant-already-exists.yaml +++ /dev/null @@ -1,8 +0,0 @@ -description: Conflict — The participant already exists in the room -content: - application/json: - schema: - $ref: '../../schemas/error.yaml' - example: - error: 'Participant Error' - message: 'Participant "Alice" already exists in room "room_123"' diff --git a/meet-ce/backend/openapi/components/responses/internal/error-room-metadata-not-found.yaml b/meet-ce/backend/openapi/components/responses/internal/error-room-metadata-not-found.yaml deleted file mode 100644 index c8cfb972..00000000 --- a/meet-ce/backend/openapi/components/responses/internal/error-room-metadata-not-found.yaml +++ /dev/null @@ -1,8 +0,0 @@ -description: Room metadata not found -content: - application/json: - schema: - $ref: '../../schemas/error.yaml' - example: - error: 'Room Error' - message: 'Room metadata for "room_123" not found. Room "room_123" does not exist or has no recordings associated' diff --git a/meet-ce/backend/openapi/components/responses/internal/success-generate-participant-token.yaml b/meet-ce/backend/openapi/components/responses/internal/success-generate-participant-token.yaml deleted file mode 100644 index 27c06945..00000000 --- a/meet-ce/backend/openapi/components/responses/internal/success-generate-participant-token.yaml +++ /dev/null @@ -1,13 +0,0 @@ -description: Successfully generated the participant token -# headers: -# Set-Cookie: -# $ref: '../../headers/set-cookie-participant-token.yaml' -content: - application/json: - schema: - type: object - properties: - token: - type: string - description: > - The token to authenticate the participant. diff --git a/meet-ce/backend/openapi/components/responses/internal/success-generate-recording-token.yaml b/meet-ce/backend/openapi/components/responses/internal/success-generate-recording-token.yaml deleted file mode 100644 index efdf519b..00000000 --- a/meet-ce/backend/openapi/components/responses/internal/success-generate-recording-token.yaml +++ /dev/null @@ -1,13 +0,0 @@ -description: Successfully generated the recording token -# headers: -# Set-Cookie: -# $ref: '../../headers/set-cookie-recording-token.yaml' -content: - application/json: - schema: - type: object - properties: - token: - type: string - description: > - The token to access the recordings in the specified OpenVidu Meet room. diff --git a/meet-ce/backend/openapi/components/responses/internal/success-generate-room-member-token.yaml b/meet-ce/backend/openapi/components/responses/internal/success-generate-room-member-token.yaml new file mode 100644 index 00000000..f9f77eef --- /dev/null +++ b/meet-ce/backend/openapi/components/responses/internal/success-generate-room-member-token.yaml @@ -0,0 +1,10 @@ +description: Successfully generated the room member token +content: + application/json: + schema: + type: object + properties: + token: + type: string + description: > + The token to authenticate the user to access the room and its resources. diff --git a/meet-ce/backend/openapi/components/responses/internal/success-get-profile.yaml b/meet-ce/backend/openapi/components/responses/internal/success-get-profile.yaml index d35009b0..1ee87ee6 100644 --- a/meet-ce/backend/openapi/components/responses/internal/success-get-profile.yaml +++ b/meet-ce/backend/openapi/components/responses/internal/success-get-profile.yaml @@ -2,4 +2,4 @@ description: Successfully retrieved user profile content: application/json: schema: - $ref: '../../schemas/internal/user.yaml' + $ref: '../../schemas/internal/meet-user.yaml' diff --git a/meet-ce/backend/openapi/components/responses/internal/success-get-room-role.yaml b/meet-ce/backend/openapi/components/responses/internal/success-get-room-member-role.yaml similarity index 61% rename from meet-ce/backend/openapi/components/responses/internal/success-get-room-role.yaml rename to meet-ce/backend/openapi/components/responses/internal/success-get-room-member-role.yaml index fafa7049..19e125b9 100644 --- a/meet-ce/backend/openapi/components/responses/internal/success-get-room-role.yaml +++ b/meet-ce/backend/openapi/components/responses/internal/success-get-room-member-role.yaml @@ -2,4 +2,4 @@ description: Successfully retrieved the room role and associated permissions content: application/json: schema: - $ref: '../../schemas/internal/meet-room-role-permissions.yaml' + $ref: '../../schemas/internal/room-member-role-permissions.yaml' diff --git a/meet-ce/backend/openapi/components/responses/internal/success-get-room-roles.yaml b/meet-ce/backend/openapi/components/responses/internal/success-get-room-member-roles.yaml similarity index 80% rename from meet-ce/backend/openapi/components/responses/internal/success-get-room-roles.yaml rename to meet-ce/backend/openapi/components/responses/internal/success-get-room-member-roles.yaml index dc6211ab..eef4ea5e 100644 --- a/meet-ce/backend/openapi/components/responses/internal/success-get-room-roles.yaml +++ b/meet-ce/backend/openapi/components/responses/internal/success-get-room-member-roles.yaml @@ -4,7 +4,7 @@ content: schema: type: array items: - $ref: '../../schemas/internal/meet-room-role-permissions.yaml' + $ref: '../../schemas/internal/room-member-role-permissions.yaml' example: - role: 'moderator' permissions: @@ -17,6 +17,8 @@ content: canUpdateOwnMetadata: true openvidu: canRecord: true + canRetrieveRecordings: true + canDeleteRecordings: true canChat: true canChangeVirtualBackground: true - role: 'speaker' @@ -30,5 +32,7 @@ content: canUpdateOwnMetadata: true openvidu: canRecord: false + canRetrieveRecordings: true + canDeleteRecordings: false canChat: true canChangeVirtualBackground: true diff --git a/meet-ce/backend/openapi/components/responses/internal/success-kick-participant.yaml b/meet-ce/backend/openapi/components/responses/internal/success-kick-participant.yaml index b51067ee..576790ab 100644 --- a/meet-ce/backend/openapi/components/responses/internal/success-kick-participant.yaml +++ b/meet-ce/backend/openapi/components/responses/internal/success-kick-participant.yaml @@ -6,4 +6,4 @@ content: properties: message: type: string - example: Participant 'Alice' kicked successfully from room 'room-123' + example: Participant 'Alice' kicked successfully from meeting in room 'room-123' diff --git a/meet-ce/backend/openapi/components/responses/internal/success-refresh-token.yaml b/meet-ce/backend/openapi/components/responses/internal/success-refresh-token.yaml index 68cab317..007f837b 100644 --- a/meet-ce/backend/openapi/components/responses/internal/success-refresh-token.yaml +++ b/meet-ce/backend/openapi/components/responses/internal/success-refresh-token.yaml @@ -1,7 +1,4 @@ description: Successfully refreshed the access token -# headers: -# Set-Cookie: -# $ref: '../../headers/set-cookie-access-token.yaml' content: application/json: schema: diff --git a/meet-ce/backend/openapi/components/responses/internal/success-user-login.yaml b/meet-ce/backend/openapi/components/responses/internal/success-user-login.yaml index 8691b9f7..a31f56f9 100644 --- a/meet-ce/backend/openapi/components/responses/internal/success-user-login.yaml +++ b/meet-ce/backend/openapi/components/responses/internal/success-user-login.yaml @@ -1,9 +1,4 @@ description: Successfully logged in -# headers: -# Set-Cookie: -# $ref: '../../headers/set-cookie-access-token.yaml' -# Set-Cookie*: -# $ref: '../../headers/set-cookie-refresh-token.yaml' content: application/json: schema: diff --git a/meet-ce/backend/openapi/components/responses/internal/success-user-logout.yaml b/meet-ce/backend/openapi/components/responses/internal/success-user-logout.yaml index 8000c945..e77c5245 100644 --- a/meet-ce/backend/openapi/components/responses/internal/success-user-logout.yaml +++ b/meet-ce/backend/openapi/components/responses/internal/success-user-logout.yaml @@ -1,11 +1,4 @@ description: Successfully logged out -# headers: -# Set-Cookie: -# description: > -# Clears the access and refresh token cookie. -# schema: -# type: string -# example: 'OvMeetAccessToken=; Path=/; HttpOnly; SameSite=Strict' content: application/json: schema: diff --git a/meet-ce/backend/openapi/components/responses/success-room-schedule-closure.yaml b/meet-ce/backend/openapi/components/responses/success-room-schedule-closure.yaml new file mode 100644 index 00000000..4e396a49 --- /dev/null +++ b/meet-ce/backend/openapi/components/responses/success-room-schedule-closure.yaml @@ -0,0 +1,10 @@ +description: Success response for scheduling room closure +content: + application/json: + schema: + type: object + properties: + message: + type: string + example: + message: Room 'room-123' scheduled to be closed when the meeting ends diff --git a/meet-ce/backend/openapi/components/responses/success-rooms-marked-for-deletion.yaml b/meet-ce/backend/openapi/components/responses/success-rooms-marked-for-deletion.yaml deleted file mode 100644 index 36509b11..00000000 --- a/meet-ce/backend/openapi/components/responses/success-rooms-marked-for-deletion.yaml +++ /dev/null @@ -1,12 +0,0 @@ -description: > - All specified rooms were marked for deletion (due to active participants) - and will be removed once all participants leave. -content: - application/json: - schema: - type: object - properties: - message: - type: string - example: - message: Rooms 'room-123, room-456' marked for deletion diff --git a/meet-ce/backend/openapi/components/responses/success-update-room-status.yaml b/meet-ce/backend/openapi/components/responses/success-update-room-status.yaml new file mode 100644 index 00000000..e73a0da3 --- /dev/null +++ b/meet-ce/backend/openapi/components/responses/success-update-room-status.yaml @@ -0,0 +1,10 @@ +description: Success response for updating room status +content: + application/json: + schema: + type: object + properties: + message: + type: string + example: + message: Room 'room-123' closed successfully diff --git a/meet-ce/backend/openapi/components/schemas/internal/meet-participant-options.yaml b/meet-ce/backend/openapi/components/schemas/internal/meet-participant-options.yaml deleted file mode 100644 index 034376c5..00000000 --- a/meet-ce/backend/openapi/components/schemas/internal/meet-participant-options.yaml +++ /dev/null @@ -1,21 +0,0 @@ -type: object -required: - - roomId - - secret -properties: - roomId: - type: string - description: The unique identifier of the room where the participant will join. - example: 'room-123' - secret: - type: string - description: The secret token from the room Url - example: 'abc123456' - participantName: - type: string - description: The name of the participant. - example: 'Alice' - participantIdentity: - type: string - description: The unique identity of the participant. - example: 'Alice' diff --git a/meet-ce/backend/openapi/components/schemas/internal/user.yaml b/meet-ce/backend/openapi/components/schemas/internal/meet-user.yaml similarity index 100% rename from meet-ce/backend/openapi/components/schemas/internal/user.yaml rename to meet-ce/backend/openapi/components/schemas/internal/meet-user.yaml diff --git a/meet-ce/backend/openapi/components/schemas/internal/meet-room-role-permissions.yaml b/meet-ce/backend/openapi/components/schemas/internal/room-member-role-permissions.yaml similarity index 67% rename from meet-ce/backend/openapi/components/schemas/internal/meet-room-role-permissions.yaml rename to meet-ce/backend/openapi/components/schemas/internal/room-member-role-permissions.yaml index 06a47b7a..68032014 100644 --- a/meet-ce/backend/openapi/components/schemas/internal/meet-room-role-permissions.yaml +++ b/meet-ce/backend/openapi/components/schemas/internal/room-member-role-permissions.yaml @@ -4,10 +4,10 @@ properties: type: string enum: ['moderator', 'speaker'] description: | - A role that a participant can have in a room. - The role determines the permissions of the participant in the room. - - `moderator`: Can manage the room and its participants. - - `speaker`: Can publish media streams to the room. + A role that a user can have as a member of a room. + The role determines the permissions of the user in the room. + - `moderator`: Can manage the room resources and meeting participants. + - `speaker`: Can publish media streams to the meeting. example: 'moderator' permissions: type: object @@ -50,15 +50,25 @@ properties: canRecord: type: boolean description: > - Indicates whether the participant can record the room. + Indicates whether the user can record a meeting in the room. + example: true + canRetrieveRecordings: + type: boolean + description: > + Indicates whether the user can retrieve and play recordings of meetings in the room. + example: true + canDeleteRecordings: + type: boolean + description: > + Indicates whether the user can delete recordings of meetings in the room. example: true canChat: type: boolean description: > - Indicates whether the participant can send and receive chat messages in the room. + Indicates whether the user can send and receive chat messages in the room. example: true canChangeVirtualBackground: type: boolean description: > - Indicates whether the participant can change their own virtual background. + Indicates whether the user can change their own virtual background. example: true diff --git a/meet-ce/backend/openapi/components/schemas/internal/room-member-token-options.yaml b/meet-ce/backend/openapi/components/schemas/internal/room-member-token-options.yaml new file mode 100644 index 00000000..9567edfc --- /dev/null +++ b/meet-ce/backend/openapi/components/schemas/internal/room-member-token-options.yaml @@ -0,0 +1,20 @@ +type: object +required: + - secret +properties: + secret: + type: string + description: A secret key for room access. Determines the member's role. + example: 'abc123456' + grantJoinMeetingPermission: + type: boolean + description: Whether to grant permission to join the meeting. If true, participantName must be provided. + example: true + participantName: + type: string + description: The name of the participant when joining the meeting. Required if `grantJoinMeetingPermission` is true and this is a new token (not a refresh). + example: 'Alice' + participantIdentity: + type: string + description: The identity of the participant in the meeting. Required when refreshing an existing token with meeting permissions. + example: 'Alice' diff --git a/meet-ce/backend/openapi/components/schemas/internal/rooms-appearance-config.yaml b/meet-ce/backend/openapi/components/schemas/internal/rooms-appearance-config.yaml index 437e3bd1..5acec768 100644 --- a/meet-ce/backend/openapi/components/schemas/internal/rooms-appearance-config.yaml +++ b/meet-ce/backend/openapi/components/schemas/internal/rooms-appearance-config.yaml @@ -26,6 +26,10 @@ MeetRoomTheme: minLength: 1 maxLength: 50 example: "Default Theme" + enabled: + type: boolean + description: Whether the theme is enabled + example: true baseTheme: $ref: '#/MeetRoomThemeMode' description: Base theme mode (light or dark) @@ -51,6 +55,7 @@ MeetRoomTheme: example: "#CCCCCC" required: - name + - enabled - baseTheme MeetRoomThemeMode: diff --git a/meet-ce/backend/openapi/components/schemas/meet-room-config.yaml b/meet-ce/backend/openapi/components/schemas/meet-room-config.yaml index 34012fe0..860d7401 100644 --- a/meet-ce/backend/openapi/components/schemas/meet-room-config.yaml +++ b/meet-ce/backend/openapi/components/schemas/meet-room-config.yaml @@ -58,6 +58,6 @@ MeetE2EEConfig: default: false example: false description: > - If true, the room will have End-to-End Encryption (E2EE) enabled.
- This ensures that the media streams are encrypted from the sender to the receiver, providing enhanced privacy and security for the participants.
- **Enabling E2EE will disable the recording feature for the room**. + If true, the room will have End-to-End Encryption (E2EE) enabled.
+ This ensures that the media streams are encrypted from the sender to the receiver, providing enhanced privacy and security for the participants.
+ **Enabling E2EE will disable the recording feature for the room**. diff --git a/meet-ce/backend/openapi/components/schemas/meet-room.yaml b/meet-ce/backend/openapi/components/schemas/meet-room.yaml index b5db64ae..217bf919 100644 --- a/meet-ce/backend/openapi/components/schemas/meet-room.yaml +++ b/meet-ce/backend/openapi/components/schemas/meet-room.yaml @@ -71,14 +71,14 @@ properties: type: string example: 'http://localhost:6080/room/room-123?secret=123456' description: > - The URL for the moderator participants to join the room. The moderator role has special permissions to manage the - room and participants. + The URL for moderator room members to access the room. The moderator role has special permissions to manage the + room resources and meeting participants. speakerUrl: type: string example: 'http://localhost:6080/room/room-123?secret=654321' description: > - The URL for the speaker participants to join the room. The speaker role has permissions to publish audio and - video streams to the room. + The URL for speaker room members to access the room. The speaker role has permissions to publish audio and + video streams to the meeting. status: type: string enum: diff --git a/meet-ce/backend/openapi/components/security.yaml b/meet-ce/backend/openapi/components/security.yaml index 0273da7a..2d4a4346 100644 --- a/meet-ce/backend/openapi/components/security.yaml +++ b/meet-ce/backend/openapi/components/security.yaml @@ -4,51 +4,21 @@ apiKeyHeader: in: header description: > The API key to authenticate the request. This key is required to access the OpenVidu Meet API. -# accessTokenCookie: -# type: apiKey -# name: OvMeetAccessToken -# in: cookie -# description: > -# The JWT token to authenticate the request in case of consuming the API from the OpenVidu Meet frontend. accessTokenHeader: type: apiKey name: Authorization in: header description: > The JWT token to authenticate the request in case of consuming the API from the OpenVidu Meet frontend. -# refreshTokenCookie: -# type: apiKey -# name: OvMeetRefreshToken -# in: cookie -# description: > -# The JWT token to refresh the access token when it expires. refreshTokenHeader: type: apiKey name: X-Refresh-Token in: header description: > The JWT token to refresh the access token when it expires. -# participantTokenCookie: -# type: apiKey -# name: OvMeetParticipantToken -# in: cookie -# description: > -# The JWT token to authenticate the participant when entering the room. -participantTokenHeader: +roomMemberTokenHeader: type: apiKey - name: X-Participant-Token + name: X-Room-Member-Token in: header description: > - The JWT token to authenticate the participant when entering the room. -# recordingTokenCookie: -# type: apiKey -# name: OvMeetRecordingToken -# in: cookie -# description: > -# The JWT token containing permissions to access the recordings in a room. -recordingTokenHeader: - type: apiKey - name: X-Recording-Token - in: header - description: > - The JWT token containing permissions to access the recordings in a room. + The JWT token to authenticate a room member when accessing room and its resources. diff --git a/meet-ce/backend/openapi/openvidu-meet-api.yaml b/meet-ce/backend/openapi/openvidu-meet-api.yaml index a1113131..1507e9a8 100644 --- a/meet-ce/backend/openapi/openvidu-meet-api.yaml +++ b/meet-ce/backend/openapi/openvidu-meet-api.yaml @@ -15,6 +15,8 @@ paths: $ref: './paths/rooms.yaml#/~1rooms~1{roomId}' /rooms/{roomId}/config: $ref: './paths/rooms.yaml#/~1rooms~1{roomId}~1config' + /rooms/{roomId}/status: + $ref: './paths/rooms.yaml#/~1rooms~1{roomId}~1status' /recordings: $ref: './paths/recordings.yaml#/~1recordings' /recordings/download: @@ -37,7 +39,7 @@ components: $ref: './components/schemas/meet-room-config.yaml#/MeetRoomConfig' MeetRecording: $ref: components/schemas/meet-recording.yaml - Error: - $ref: components/schemas/error.yaml MeetWebhookEvent: $ref: components/schemas/meet-webhook.yaml#/MeetWebhookEvent + Error: + $ref: components/schemas/error.yaml diff --git a/meet-ce/backend/openapi/openvidu-meet-internal-api.yaml b/meet-ce/backend/openapi/openvidu-meet-internal-api.yaml index 506d328e..bc70a08d 100644 --- a/meet-ce/backend/openapi/openvidu-meet-internal-api.yaml +++ b/meet-ce/backend/openapi/openvidu-meet-internal-api.yaml @@ -28,8 +28,8 @@ paths: $ref: './paths/internal/meet-global-config.yaml#/~1config~1security' /config/rooms/appearance: $ref: './paths/internal/meet-global-config.yaml#/~1config~1rooms~1appearance' - /rooms/{roomId}/recording-token: - $ref: './paths/internal/rooms.yaml#/~1rooms~1{roomId}~1recording-token' + /rooms/{roomId}/token: + $ref: './paths/internal/rooms.yaml#/~1rooms~1{roomId}~1token' /rooms/{roomId}/roles: $ref: './paths/internal/rooms.yaml#/~1rooms~1{roomId}~1roles' /rooms/{roomId}/roles/{secret}: @@ -38,10 +38,6 @@ paths: $ref: './paths/internal/recordings.yaml#/~1recordings' /recordings/{recordingId}/stop: $ref: './paths/internal/recordings.yaml#/~1recordings~1{recordingId}~1stop' - /participants/token: - $ref: './paths/internal/participants.yaml#/~1participants~1token' - /participants/token/refresh: - $ref: './paths/internal/participants.yaml#/~1participants~1token~1refresh' /meetings/{roomId}: $ref: './paths/internal/meetings.yaml#/~1meetings~1{roomId}' /meetings/{roomId}/participants/{participantIdentity}: @@ -55,8 +51,10 @@ components: securitySchemes: $ref: components/security.yaml schemas: - User: - $ref: components/schemas/internal/user.yaml + MeetApiKey: + $ref: components/schemas/internal/meet-api-key.yaml + MeetUser: + $ref: components/schemas/internal/meet-user.yaml WebhooksConfig: $ref: components/schemas/internal/webhooks-config.yaml SecurityConfig: @@ -67,8 +65,8 @@ components: $ref: components/schemas/meet-room-options.yaml MeetRoomConfig: $ref: components/schemas/meet-room-config.yaml#/MeetRoomConfig - MeetRoomRoleAndPermissions: - $ref: components/schemas/internal/meet-room-role-permissions.yaml + MeetRoomMemberRoleAndPermissions: + $ref: components/schemas/internal/room-member-role-permissions.yaml MeetAnalytics: $ref: components/schemas/internal/meet-analytics.yaml MeetRecording: diff --git a/meet-ce/backend/openapi/paths/internal/meetings.yaml b/meet-ce/backend/openapi/paths/internal/meetings.yaml index c0957d98..84b0a365 100644 --- a/meet-ce/backend/openapi/paths/internal/meetings.yaml +++ b/meet-ce/backend/openapi/paths/internal/meetings.yaml @@ -9,15 +9,12 @@ tags: - Internal API - Meetings security: - - participantTokenHeader: [] + - roomMemberTokenHeader: [] parameters: - $ref: '../../components/parameters/room-id-path.yaml' - - $ref: '../../components/parameters/internal/x-participant-role.yaml' responses: '200': $ref: '../../components/responses/internal/success-end-meeting.yaml' - '400': - $ref: '../../components/responses/internal/error-invalid-participant-role.yaml' '401': $ref: '../../components/responses/unauthorized-error.yaml' '403': @@ -35,16 +32,12 @@ tags: - Internal API - Meetings security: - - participantTokenHeader: [] + - roomMemberTokenHeader: [] parameters: - $ref: '../../components/parameters/room-id-path.yaml' - - $ref: '../../components/parameters/internal/participant-identity.yaml' - - $ref: '../../components/parameters/internal/x-participant-role.yaml' responses: '200': $ref: '../../components/responses/internal/success-kick-participant.yaml' - '400': - $ref: '../../components/responses/internal/error-invalid-participant-role.yaml' '401': $ref: '../../components/responses/unauthorized-error.yaml' '403': @@ -62,18 +55,14 @@ tags: - Internal API - Meetings security: - - participantTokenHeader: [] + - roomMemberTokenHeader: [] parameters: - $ref: '../../components/parameters/room-id-path.yaml' - - $ref: '../../components/parameters/internal/participant-identity.yaml' - - $ref: '../../components/parameters/internal/x-participant-role.yaml' requestBody: $ref: '../../components/requestBodies/internal/update-participant-role-request.yaml' responses: '200': $ref: '../../components/responses/internal/success-update-participant-role.yaml' - '400': - $ref: '../../components/responses/internal/error-invalid-participant-role.yaml' '401': $ref: '../../components/responses/unauthorized-error.yaml' '403': diff --git a/meet-ce/backend/openapi/paths/internal/participants.yaml b/meet-ce/backend/openapi/paths/internal/participants.yaml deleted file mode 100644 index bb9122b0..00000000 --- a/meet-ce/backend/openapi/paths/internal/participants.yaml +++ /dev/null @@ -1,58 +0,0 @@ -/participants/token: - post: - operationId: generateParticipantToken - summary: Generate token for participant - description: > - Generates a token for a participant to join an OpenVidu Meet room. - tags: - - Internal API - Participant - security: - - accessTokenHeader: [] - requestBody: - $ref: '../../components/requestBodies/internal/participant-token-request.yaml' - responses: - '200': - $ref: '../../components/responses/internal/success-generate-participant-token.yaml' - '400': - $ref: '../../components/responses/internal/error-invalid-room-secret.yaml' - '401': - $ref: '../../components/responses/unauthorized-error.yaml' - '403': - $ref: '../../components/responses/forbidden-error.yaml' - '404': - $ref: '../../components/responses/error-room-not-found.yaml' - '409': - $ref: '../../components/responses/internal/error-room-closed.yaml' - '422': - $ref: '../../components/responses/validation-error.yaml' - '500': - $ref: '../../components/responses/internal-server-error.yaml' -/participants/token/refresh: - post: - operationId: refreshParticipantToken - summary: Refresh token for participant - description: > - Refresh a token for a participant in an OpenVidu Meet room. - tags: - - Internal API - Participant - security: - - accessTokenHeader: [] - requestBody: - $ref: '../../components/requestBodies/internal/participant-token-request.yaml' - responses: - '200': - $ref: '../../components/responses/internal/success-generate-participant-token.yaml' - '400': - $ref: '../../components/responses/internal/error-invalid-room-secret.yaml' - '401': - $ref: '../../components/responses/unauthorized-error.yaml' - '403': - $ref: '../../components/responses/forbidden-error.yaml' - '404': - $ref: '../../components/responses/internal/error-room-participant-not-found.yaml' - '409': - $ref: '../../components/responses/internal/error-room-closed.yaml' - '422': - $ref: '../../components/responses/validation-error.yaml' - '500': - $ref: '../../components/responses/internal-server-error.yaml' diff --git a/meet-ce/backend/openapi/paths/internal/recordings.yaml b/meet-ce/backend/openapi/paths/internal/recordings.yaml index 06091045..ba902b3a 100644 --- a/meet-ce/backend/openapi/paths/internal/recordings.yaml +++ b/meet-ce/backend/openapi/paths/internal/recordings.yaml @@ -7,16 +7,12 @@ tags: - Internal API - Recordings security: - - participantTokenHeader: [] - parameters: - - $ref: '../../components/parameters/internal/x-participant-role.yaml' + - roomMemberTokenHeader: [] requestBody: $ref: '../../components/requestBodies/internal/start-recording-request.yaml' responses: '201': $ref: '../../components/responses/internal/success-start-recording.yaml' - '400': - $ref: '../../components/responses/internal/error-invalid-participant-role.yaml' '401': $ref: '../../components/responses/unauthorized-error.yaml' '403': @@ -42,15 +38,12 @@ tags: - Internal API - Recordings security: - - participantTokenHeader: [] + - roomMemberTokenHeader: [] parameters: - $ref: '../../components/parameters/recording-id.yaml' - - $ref: '../../components/parameters/internal/x-participant-role.yaml' responses: '202': $ref: '../../components/responses/internal/success-stop-recording.yaml' - '400': - $ref: '../../components/responses/internal/error-invalid-participant-role.yaml' '401': $ref: '../../components/responses/unauthorized-error.yaml' '403': diff --git a/meet-ce/backend/openapi/paths/internal/rooms.yaml b/meet-ce/backend/openapi/paths/internal/rooms.yaml index 4e8aeb1b..2959e1d3 100644 --- a/meet-ce/backend/openapi/paths/internal/rooms.yaml +++ b/meet-ce/backend/openapi/paths/internal/rooms.yaml @@ -1,10 +1,9 @@ -/rooms/{roomId}/recording-token: +/rooms/{roomId}/token: post: - operationId: generateRecordingToken - summary: Generate recording token + operationId: generateRoomMemberToken + summary: Generate room member token description: > - Generates a token with recording permissions for a specified OpenVidu Meet room. - This token can be used to access the recordings in a room. + Generates a token for a user to access an OpenVidu Meet room and its resources. tags: - Internal API - Rooms security: @@ -12,10 +11,10 @@ parameters: - $ref: '../../components/parameters/room-id-path.yaml' requestBody: - $ref: '../../components/requestBodies/internal/recording-token-request.yaml' + $ref: '../../components/requestBodies/internal/room-member-token-request.yaml' responses: '200': - $ref: '../../components/responses/internal/success-generate-recording-token.yaml' + $ref: '../../components/responses/internal/success-generate-room-member-token.yaml' '400': $ref: '../../components/responses/internal/error-invalid-room-secret.yaml' '401': @@ -23,24 +22,26 @@ '403': $ref: '../../components/responses/forbidden-error.yaml' '404': - $ref: '../../components/responses/internal/error-room-metadata-not-found.yaml' + $ref: '../../components/responses/internal/error-room-participant-not-found.yaml' + '409': + $ref: '../../components/responses/internal/error-room-closed.yaml' '422': $ref: '../../components/responses/validation-error.yaml' '500': $ref: '../../components/responses/internal-server-error.yaml' /rooms/{roomId}/roles: get: - operationId: getRoomRolesAndPermissions - summary: Get room roles and permissions + operationId: getRoomMemberRolesAndPermissions + summary: Get room member roles and permissions description: > - Retrieves the roles and associated permissions that a participant can have in a specified OpenVidu Meet room. + Retrieves the roles and associated permissions that a user can have as a member of a specified OpenVidu Meet room. tags: - Internal API - Rooms parameters: - $ref: '../../components/parameters/room-id-path.yaml' responses: '200': - $ref: '../../components/responses/internal/success-get-room-roles.yaml' + $ref: '../../components/responses/internal/success-get-room-member-roles.yaml' '404': $ref: '../../components/responses/error-room-not-found.yaml' '422': @@ -52,10 +53,10 @@ operationId: getRoomRoleAndPermissions summary: Get room role and permissions description: | - Retrieves the role and associated permissions that a participant will have in a specified OpenVidu Meet room - when using the URL thant contains the given secret value. + Retrieves the role and associated permissions that a user will have as a member of a specified OpenVidu Meet room + when using the URL that contains the given secret value. - This endpoint is useful for checking the participant's role and permissions before joining the room. + This endpoint is useful for checking the user's role and permissions before accessing the room. tags: - Internal API - Rooms parameters: @@ -63,7 +64,7 @@ - $ref: '../../components/parameters/internal/secret.yaml' responses: '200': - $ref: '../../components/responses/internal/success-get-room-role.yaml' + $ref: '../../components/responses/internal/success-get-room-member-role.yaml' '400': $ref: '../../components/responses/internal/error-invalid-room-secret.yaml' '404': diff --git a/meet-ce/backend/openapi/paths/recordings.yaml b/meet-ce/backend/openapi/paths/recordings.yaml index 327f9ef6..bd2fb88c 100644 --- a/meet-ce/backend/openapi/paths/recordings.yaml +++ b/meet-ce/backend/openapi/paths/recordings.yaml @@ -6,14 +6,14 @@ Retrieves a paginated list of all recordings available in the system. You can apply filters to narrow down the results based on specific criteria. - > **Note:** If this endpoint is called using the `recordingTokenHeader` authentication method, + > **Note:** If this endpoint is called using the `roomMemberTokenHeader` authentication method, > the `roomId` filter will be ignored and only recordings associated with the room included in the token will be returned. tags: - OpenVidu Meet - Recordings security: - apiKeyHeader: [] - accessTokenHeader: [] - - recordingTokenHeader: [] + - roomMemberTokenHeader: [] parameters: # - $ref: '../components/parameters/recording-status.yaml' - $ref: '../components/parameters/recording-fields.yaml' @@ -39,7 +39,7 @@ description: | Deletes multiple recordings at once with the specified recording IDs. - > **Note:** If this endpoint is called using the `recordingTokenHeader` authentication method, + > **Note:** If this endpoint is called using the `roomMemberTokenHeader` authentication method, > all specified recordings must belong to the same room included in the token. > If a recording does not belong to that room, it will not be deleted. tags: @@ -47,7 +47,7 @@ security: - apiKeyHeader: [] - accessTokenHeader: [] - - recordingTokenHeader: [] + - roomMemberTokenHeader: [] parameters: - $ref: '../components/parameters/recording-ids.yaml' responses: @@ -72,7 +72,7 @@ Downloads multiple recordings as a ZIP file with the specified recording IDs. The ZIP file will contain all recordings in MP4 format. - > **Note:** If this endpoint is called using the `recordingTokenHeader` authentication method, + > **Note:** If this endpoint is called using the `roomMemberTokenHeader` authentication method, > all specified recordings must belong to the same room included in the token. > If a recording does not belong to that room, it will not be included in the ZIP file. tags: @@ -80,7 +80,7 @@ security: - apiKeyHeader: [] - accessTokenHeader: [] - - recordingTokenHeader: [] + - roomMemberTokenHeader: [] parameters: - $ref: '../components/parameters/recording-ids.yaml' responses: @@ -121,7 +121,7 @@ security: - apiKeyHeader: [] - accessTokenHeader: [] - - recordingTokenHeader: [] + - roomMemberTokenHeader: [] parameters: - $ref: '../components/parameters/recording-id.yaml' - $ref: '../components/parameters/recording-secret.yaml' @@ -153,7 +153,7 @@ security: - apiKeyHeader: [] - accessTokenHeader: [] - - recordingTokenHeader: [] + - roomMemberTokenHeader: [] parameters: - $ref: '../components/parameters/recording-id.yaml' responses: @@ -186,7 +186,7 @@ security: - apiKeyHeader: [] - accessTokenHeader: [] - - recordingTokenHeader: [] + - roomMemberTokenHeader: [] parameters: - $ref: '../components/parameters/recording-id.yaml' - $ref: '../components/parameters/recording-secret.yaml' @@ -260,7 +260,7 @@ security: - apiKeyHeader: [] - accessTokenHeader: [] - - recordingTokenHeader: [] + - roomMemberTokenHeader: [] parameters: - $ref: '../components/parameters/recording-id.yaml' # - $ref: '../components/parameters/private-access.yaml' diff --git a/meet-ce/backend/openapi/paths/rooms.yaml b/meet-ce/backend/openapi/paths/rooms.yaml index 5f6f3a2b..f428728b 100644 --- a/meet-ce/backend/openapi/paths/rooms.yaml +++ b/meet-ce/backend/openapi/paths/rooms.yaml @@ -94,16 +94,13 @@ security: - apiKeyHeader: [] - accessTokenHeader: [] - - participantTokenHeader: [] + - roomMemberTokenHeader: [] parameters: - $ref: '../components/parameters/room-id-path.yaml' - $ref: '../components/parameters/room-fields.yaml' - - $ref: '../components/parameters/internal/x-participant-role.yaml' responses: '200': $ref: '../components/responses/success-get-room.yaml' - '400': - $ref: '../components/responses/internal/error-invalid-participant-role.yaml' '401': $ref: '../components/responses/unauthorized-error.yaml' '403': @@ -162,15 +159,12 @@ security: - apiKeyHeader: [] - accessTokenHeader: [] - - participantTokenHeader: [] + - roomMemberTokenHeader: [] parameters: - $ref: '../components/parameters/room-id-path.yaml' - - $ref: '../components/parameters/internal/x-participant-role.yaml' responses: '200': $ref: '../components/responses/success-get-room-config.yaml' - '400': - $ref: '../components/responses/internal/error-invalid-participant-role.yaml' '401': $ref: '../components/responses/unauthorized-error.yaml' '403': @@ -210,3 +204,34 @@ $ref: '../components/responses/validation-error.yaml' '500': $ref: '../components/responses/internal-server-error.yaml' +/rooms/{roomId}/status: + put: + operationId: updateRoomStatus + summary: Update room status + description: > + Updates the status of an OpenVidu Meet room with the specified room ID. + This can be used to open or close the room for new participants. + tags: + - OpenVidu Meet - Rooms + security: + - apiKeyHeader: [] + - accessTokenHeader: [] + parameters: + - $ref: '../components/parameters/room-id-path.yaml' + requestBody: + $ref: '../components/requestBodies/update-room-status-request.yaml' + responses: + '200': + $ref: '../components/responses/success-update-room-status.yaml' + '202': + $ref: '../components/responses/success-room-schedule-closure.yaml' + '401': + $ref: '../components/responses/unauthorized-error.yaml' + '403': + $ref: '../components/responses/forbidden-error.yaml' + '404': + $ref: '../components/responses/error-room-not-found.yaml' + '422': + $ref: '../components/responses/validation-error.yaml' + '500': + $ref: '../components/responses/internal-server-error.yaml' diff --git a/meet-ce/backend/openapi/tags/tags.yaml b/meet-ce/backend/openapi/tags/tags.yaml index 05a0af65..9c520434 100644 --- a/meet-ce/backend/openapi/tags/tags.yaml +++ b/meet-ce/backend/openapi/tags/tags.yaml @@ -14,8 +14,6 @@ description: Operations related to managing global config in OpenVidu Meet - name: Internal API - Rooms description: Operations related to managing OpenVidu Meet rooms -- name: Internal API - Participant - description: Operations related to managing participants in OpenVidu Meet rooms - name: Internal API - Meetings description: Operations related to managing meetings in OpenVidu Meet rooms - name: Internal API - Recordings diff --git a/meet-ce/backend/package.json b/meet-ce/backend/package.json index 9abe5877..be523370 100644 --- a/meet-ce/backend/package.json +++ b/meet-ce/backend/package.json @@ -35,10 +35,9 @@ "start:dev": "NODE_ENV=development concurrently -k -n server,typecheck -c cyan,yellow \"pnpm tsx watch --clear-screen=false --include src ./src/server.ts\" \"pnpm run dev:typecheck\"", "dev:typecheck": "node ../../scripts/dev/backend-type-checker.mjs", "package:build": "pnpm run build:prod && pnpm pack", - "test:integration-room-management": "node --experimental-vm-modules ../../node_modules/jest/bin/jest.js --runInBand --forceExit --testPathPattern \"tests/integration/api/(rooms|meetings|participants)\" --ci --reporters=default --reporters=jest-junit", + "test:integration-room-management": "node --experimental-vm-modules ../../node_modules/jest/bin/jest.js --runInBand --forceExit --testPathPattern \"tests/integration/api/(rooms|meetings)\" --ci --reporters=default --reporters=jest-junit", "test:integration-rooms": "node --experimental-vm-modules ../../node_modules/jest/bin/jest.js --runInBand --forceExit --testPathPattern 'tests/integration/api/rooms' --ci --reporters=default --reporters=jest-junit", "test:integration-meetings": "node --experimental-vm-modules ../../node_modules/.bin/jest --runInBand --forceExit --testPathPattern \"tests/integration/api/meetings\" --ci --reporters=default --reporters=jest-junit", - "test:integration-participants": "node --experimental-vm-modules ../../node_modules/.bin/jest --runInBand --forceExit --testPathPattern \"tests/integration/api/participants\" --ci --reporters=default --reporters=jest-junit", "test:integration-webhooks": "node --experimental-vm-modules ../../node_modules/.bin/jest --runInBand --forceExit --testPathPattern \"tests/integration/webhooks\" --ci --reporters=default --reporters=jest-junit", "test:integration-auth-security": "node --experimental-vm-modules ../../node_modules/.bin/jest --runInBand --forceExit --testPathPattern \"tests/integration/api/(security|auth|api-keys|users)\" --ci --reporters=default --reporters=jest-junit", "test:integration-security": "node --experimental-vm-modules ../../node_modules/.bin/jest --runInBand --forceExit --testPathPattern \"tests/integration/api/security\" --ci --reporters=default --reporters=jest-junit", diff --git a/meet-ce/backend/src/config/@types/express/index.d.ts b/meet-ce/backend/src/config/@types/express/index.d.ts deleted file mode 100644 index 570aac2c..00000000 --- a/meet-ce/backend/src/config/@types/express/index.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ParticipantRole, User } from '@openvidu-meet/typings'; -import { ClaimGrants } from 'livekit-server-sdk'; - -// Override the Express Request type to include a session object with user and token properties -// This will allow controllers to access the user and token information from the request object in a type-safe manner -declare module 'express' { - interface Request { - session?: { - user?: User; - tokenClaims?: ClaimGrants; - participantRole?: ParticipantRole; - }; - } -} diff --git a/meet-ce/backend/src/config/dependency-injector.config.ts b/meet-ce/backend/src/config/dependency-injector.config.ts index f07f637a..31976864 100644 --- a/meet-ce/backend/src/config/dependency-injector.config.ts +++ b/meet-ce/backend/src/config/dependency-injector.config.ts @@ -14,13 +14,13 @@ import { ABSStorageProvider, AnalyticsService, ApiKeyService, + BaseUrlService, BlobStorageService, DistributedEventService, FrontendEventService, GCSService, GCSStorageProvider, GlobalConfigService, - HttpContextService, LegacyStorageService, LiveKitService, LivekitWebhookService, @@ -30,9 +30,10 @@ import { MutexService, OpenViduWebhookService, ParticipantNameService, - ParticipantService, RecordingService, RedisService, + RequestSessionService, + RoomMemberService, RoomService, S3KeyBuilder, S3Service, @@ -69,7 +70,10 @@ export const registerDependencies = () => { container.bind(DistributedEventService).toSelf().inSingletonScope(); container.bind(MutexService).toSelf().inSingletonScope(); container.bind(TaskSchedulerService).toSelf().inSingletonScope(); - container.bind(HttpContextService).toSelf().inSingletonScope(); + container.bind(BaseUrlService).toSelf().inSingletonScope(); + // RequestSessionService uses AsyncLocalStorage for request isolation + // It's a singleton but provides per-request data isolation automatically + container.bind(RequestSessionService).toSelf().inSingletonScope(); container.bind(MongoDBService).toSelf().inSingletonScope(); container.bind(BaseRepository).toSelf().inSingletonScope(); @@ -97,7 +101,7 @@ export const registerDependencies = () => { container.bind(RecordingService).toSelf().inSingletonScope(); container.bind(RoomService).toSelf().inSingletonScope(); container.bind(ParticipantNameService).toSelf().inSingletonScope(); - container.bind(ParticipantService).toSelf().inSingletonScope(); + container.bind(RoomMemberService).toSelf().inSingletonScope(); container.bind(OpenViduWebhookService).toSelf().inSingletonScope(); container.bind(LivekitWebhookService).toSelf().inSingletonScope(); container.bind(AnalyticsService).toSelf().inSingletonScope(); diff --git a/meet-ce/backend/src/config/internal-config.ts b/meet-ce/backend/src/config/internal-config.ts index 16ef7788..b33b1a0f 100644 --- a/meet-ce/backend/src/config/internal-config.ts +++ b/meet-ce/backend/src/config/internal-config.ts @@ -5,25 +5,16 @@ export const INTERNAL_CONFIG = { API_BASE_PATH_V1: '/api/v1', INTERNAL_API_BASE_PATH_V1: '/internal-api/v1', - // Cookie names - ACCESS_TOKEN_COOKIE_NAME: 'OvMeetAccessToken', - REFRESH_TOKEN_COOKIE_NAME: 'OvMeetRefreshToken', - PARTICIPANT_TOKEN_COOKIE_NAME: 'OvMeetParticipantToken', - RECORDING_TOKEN_COOKIE_NAME: 'OvMeetRecordingToken', - // Headers names API_KEY_HEADER: 'x-api-key', ACCESS_TOKEN_HEADER: 'authorization', REFRESH_TOKEN_HEADER: 'x-refresh-token', - PARTICIPANT_TOKEN_HEADER: 'x-participant-token', - PARTICIPANT_ROLE_HEADER: 'x-participant-role', - RECORDING_TOKEN_HEADER: 'x-recording-token', + ROOM_MEMBER_TOKEN_HEADER: 'x-room-member-token', // Token expiration times ACCESS_TOKEN_EXPIRATION: '2h', REFRESH_TOKEN_EXPIRATION: '1d', - PARTICIPANT_TOKEN_EXPIRATION: '2h', - RECORDING_TOKEN_EXPIRATION: '2h', + ROOM_MEMBER_TOKEN_EXPIRATION: '2h', // Participant name reservations PARTICIPANT_MAX_CONCURRENT_NAME_REQUESTS: '20', // Maximum number of request by the same name at the same time allowed diff --git a/meet-ce/backend/src/controllers/auth.controller.ts b/meet-ce/backend/src/controllers/auth.controller.ts index f0a442e9..c66a8137 100644 --- a/meet-ce/backend/src/controllers/auth.controller.ts +++ b/meet-ce/backend/src/controllers/auth.controller.ts @@ -1,8 +1,6 @@ -import { AuthTransportMode } from '@openvidu-meet/typings'; import { Request, Response } from 'express'; import { ClaimGrants } from 'livekit-server-sdk'; import { container } from '../config/index.js'; -import { INTERNAL_CONFIG } from '../config/internal-config.js'; import { errorInvalidCredentials, errorInvalidRefreshToken, @@ -12,7 +10,7 @@ import { rejectRequestFromMeetError } from '../models/error.model.js'; import { LoggerService, TokenService, UserService } from '../services/index.js'; -import { getAuthTransportMode, getCookieOptions, getRefreshToken } from '../utils/index.js'; +import { getRefreshToken } from '../utils/index.js'; export const login = async (req: Request, res: Response) => { const logger = container.get(LoggerService); @@ -34,49 +32,19 @@ export const login = async (req: Request, res: Response) => { const refreshToken = await tokenService.generateRefreshToken(user); logger.info(`Login succeeded for user '${username}'`); - const transportMode = await getAuthTransportMode(); - - if (transportMode === AuthTransportMode.HEADER) { - // Send tokens in response body for header mode - return res.status(200).json({ - message: `User '${username}' logged in successfully`, - accessToken, - refreshToken - }); - } else { - // Send tokens as cookies for cookie mode - res.cookie( - INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME, - accessToken, - getCookieOptions('/', INTERNAL_CONFIG.ACCESS_TOKEN_EXPIRATION) - ); - res.cookie( - INTERNAL_CONFIG.REFRESH_TOKEN_COOKIE_NAME, - refreshToken, - getCookieOptions( - `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/auth`, - INTERNAL_CONFIG.REFRESH_TOKEN_EXPIRATION - ) - ); - return res.status(200).json({ message: `User '${username}' logged in successfully` }); - } + return res.status(200).json({ + message: `User '${username}' logged in successfully`, + accessToken, + refreshToken + }); } catch (error) { handleError(res, error, 'generating access and refresh tokens'); } }; export const logout = async (_req: Request, res: Response) => { - const transportMode = await getAuthTransportMode(); - - if (transportMode === AuthTransportMode.COOKIE) { - // Clear cookies only in cookie mode - res.clearCookie(INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME); - res.clearCookie(INTERNAL_CONFIG.REFRESH_TOKEN_COOKIE_NAME, { - path: `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/auth` - }); - } - - // In header mode, the client is responsible for clearing localStorage + // The client is responsible for clearing tokens from localStorage, + // so just respond with success return res.status(200).json({ message: 'Logout successful' }); }; @@ -118,23 +86,10 @@ export const refreshToken = async (req: Request, res: Response) => { const accessToken = await tokenService.generateAccessToken(user); logger.info(`Access token refreshed for user '${username}'`); - const transportMode = await getAuthTransportMode(); - - if (transportMode === AuthTransportMode.HEADER) { - // Send access token in response body for header mode - return res.status(200).json({ - message: `Access token for user '${username}' successfully refreshed`, - accessToken - }); - } else { - // Send access token as cookie for cookie mode - res.cookie( - INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME, - accessToken, - getCookieOptions('/', INTERNAL_CONFIG.ACCESS_TOKEN_EXPIRATION) - ); - return res.status(200).json({ message: `Access token for user '${username}' successfully refreshed` }); - } + return res.status(200).json({ + message: `Access token for user '${username}' successfully refreshed`, + accessToken + }); } catch (error) { handleError(res, error, 'refreshing token'); } diff --git a/meet-ce/backend/src/controllers/index.ts b/meet-ce/backend/src/controllers/index.ts index 2c3ed80e..3feea422 100644 --- a/meet-ce/backend/src/controllers/index.ts +++ b/meet-ce/backend/src/controllers/index.ts @@ -3,7 +3,6 @@ export * from './api-key.controller.js'; export * from './user.controller.js'; export * from './room.controller.js'; export * from './meeting.controller.js'; -export * from './participant.controller.js'; export * from './recording.controller.js'; export * from './livekit-webhook.controller.js'; export * from './analytics.controller.js'; diff --git a/meet-ce/backend/src/controllers/meeting.controller.ts b/meet-ce/backend/src/controllers/meeting.controller.ts index 4e1f9c16..50bbf854 100644 --- a/meet-ce/backend/src/controllers/meeting.controller.ts +++ b/meet-ce/backend/src/controllers/meeting.controller.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import { container } from '../config/index.js'; import { handleError } from '../models/error.model.js'; -import { LiveKitService, LoggerService, RoomService } from '../services/index.js'; +import { LiveKitService, LoggerService, RoomMemberService, RoomService } from '../services/index.js'; export const endMeeting = async (req: Request, res: Response) => { const logger = container.get(LoggerService); @@ -26,3 +26,42 @@ export const endMeeting = async (req: Request, res: Response) => { handleError(res, error, `ending meeting from room '${roomId}'`); } }; + +export const updateParticipantRole = async (req: Request, res: Response) => { + const logger = container.get(LoggerService); + const roomMemberService = container.get(RoomMemberService); + const { roomId, participantIdentity } = req.params; + const { role } = req.body; + + try { + logger.verbose(`Changing role of participant '${participantIdentity}' in room '${roomId}' to '${role}'`); + await roomMemberService.updateParticipantRole(roomId, participantIdentity, role); + res.status(200).json({ message: `Participant '${participantIdentity}' role updated to '${role}'` }); + } catch (error) { + handleError(res, error, `changing role for participant '${participantIdentity}' in room '${roomId}'`); + } +}; + +export const kickParticipantFromMeeting = async (req: Request, res: Response) => { + const logger = container.get(LoggerService); + const roomService = container.get(RoomService); + const roomMemberService = container.get(RoomMemberService); + const { roomId, participantIdentity } = req.params; + + // Check if the room exists + try { + await roomService.getMeetRoom(roomId); + } catch (error) { + return handleError(res, error, `getting room '${roomId}'`); + } + + try { + logger.verbose(`Kicking participant '${participantIdentity}' from room '${roomId}'`); + await roomMemberService.kickParticipantFromMeeting(roomId, participantIdentity); + res.status(200).json({ + message: `Participant '${participantIdentity}' kicked successfully from meeting in room '${roomId}'` + }); + } catch (error) { + handleError(res, error, `kicking participant '${participantIdentity}' from meeting in room '${roomId}'`); + } +}; diff --git a/meet-ce/backend/src/controllers/participant.controller.ts b/meet-ce/backend/src/controllers/participant.controller.ts deleted file mode 100644 index 8ab97ee5..00000000 --- a/meet-ce/backend/src/controllers/participant.controller.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { - AuthTransportMode, - OpenViduMeetPermissions, - ParticipantOptions, - ParticipantRole -} from '@openvidu-meet/typings'; -import { Request, Response } from 'express'; -import { container } from '../config/index.js'; -import { INTERNAL_CONFIG } from '../config/internal-config.js'; -import { - errorInvalidParticipantToken, - errorParticipantTokenNotPresent, - handleError, - rejectRequestFromMeetError -} from '../models/error.model.js'; -import { LoggerService, ParticipantService, RoomService, TokenService } from '../services/index.js'; -import { getAuthTransportMode, getCookieOptions, getParticipantToken } from '../utils/index.js'; - -export const generateParticipantToken = async (req: Request, res: Response) => { - const logger = container.get(LoggerService); - const participantService = container.get(ParticipantService); - const tokenService = container.get(TokenService); - - const participantOptions: ParticipantOptions = req.body; - const { roomId } = participantOptions; - - // Check if there is a previous token (only for cookie mode) - const previousToken = req.cookies[INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME]; - let currentRoles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[] = []; - - if (previousToken) { - // If there is a previous token, extract the roles from it - // and use them to generate the new token, aggregating the new role to the current ones - // This logic is only used in cookie mode to allow multiple roles across tabs - logger.verbose('Previous participant token found. Extracting roles'); - - try { - const claims = tokenService.getClaimsIgnoringExpiration(previousToken); - const metadata = participantService.parseMetadata(claims.metadata || '{}'); - currentRoles = metadata.roles; - } catch (error) { - logger.verbose('Error extracting roles from previous token:', error); - } - } - - try { - logger.verbose(`Generating participant token for room '${roomId}'`); - const token = await participantService.generateOrRefreshParticipantToken(participantOptions, currentRoles); - - const authTransportMode = await getAuthTransportMode(); - - // Send participant token as cookie for cookie mode - if (authTransportMode === AuthTransportMode.COOKIE) { - res.cookie(INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME, token, getCookieOptions('/')); - } - - return res.status(200).json({ token }); - } catch (error) { - handleError(res, error, `generating participant token for room '${roomId}'`); - } -}; - -export const refreshParticipantToken = async (req: Request, res: Response) => { - const logger = container.get(LoggerService); - const tokenService = container.get(TokenService); - const participantService = container.get(ParticipantService); - - // Check if there is a previous token - const previousToken = await getParticipantToken(req); - - if (!previousToken) { - logger.verbose('No previous participant token found. Cannot refresh.'); - const error = errorParticipantTokenNotPresent(); - return rejectRequestFromMeetError(res, error); - } - - // Extract roles from the previous token - let currentRoles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[] = []; - - try { - const claims = tokenService.getClaimsIgnoringExpiration(previousToken); - const metadata = participantService.parseMetadata(claims.metadata || '{}'); - currentRoles = metadata.roles; - } catch (err) { - logger.verbose('Error extracting roles from previous token:', err); - const error = errorInvalidParticipantToken(); - return rejectRequestFromMeetError(res, error); - } - - const participantOptions: ParticipantOptions = req.body; - const { roomId } = participantOptions; - - try { - logger.verbose(`Refreshing participant token for room '${roomId}'`); - const token = await participantService.generateOrRefreshParticipantToken( - participantOptions, - currentRoles, - true - ); - - const authTransportMode = await getAuthTransportMode(); - - // Send participant token as cookie for cookie mode - if (authTransportMode === AuthTransportMode.COOKIE) { - res.cookie(INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME, token, getCookieOptions('/')); - } - - return res.status(200).json({ token }); - } catch (error) { - handleError(res, error, `refreshing participant token for room '${roomId}'`); - } -}; - -export const updateParticipantRole = async (req: Request, res: Response) => { - const logger = container.get(LoggerService); - const participantService = container.get(ParticipantService); - const { roomId, participantIdentity } = req.params; - const { role } = req.body; - - try { - logger.verbose(`Changing role of participant '${participantIdentity}' in room '${roomId}' to '${role}'`); - await participantService.updateParticipantRole(roomId, participantIdentity, role); - res.status(200).json({ message: `Participant '${participantIdentity}' role updated to '${role}'` }); - } catch (error) { - handleError(res, error, `changing role for participant '${participantIdentity}' in room '${roomId}'`); - } -}; - -export const kickParticipant = async (req: Request, res: Response) => { - const logger = container.get(LoggerService); - const roomService = container.get(RoomService); - const participantService = container.get(ParticipantService); - const { roomId, participantIdentity } = req.params; - - // Check if the room exists - try { - await roomService.getMeetRoom(roomId); - } catch (error) { - return handleError(res, error, `getting room '${roomId}'`); - } - - try { - logger.verbose(`Kicking participant '${participantIdentity}' from room '${roomId}'`); - await participantService.kickParticipant(roomId, participantIdentity); - res.status(200).json({ - message: `Participant '${participantIdentity}' kicked successfully from room '${roomId}'` - }); - } catch (error) { - handleError(res, error, `kicking participant '${participantIdentity}' from room '${roomId}'`); - } -}; diff --git a/meet-ce/backend/src/controllers/recording.controller.ts b/meet-ce/backend/src/controllers/recording.controller.ts index 2c9cd874..d5c45a8f 100644 --- a/meet-ce/backend/src/controllers/recording.controller.ts +++ b/meet-ce/backend/src/controllers/recording.controller.ts @@ -12,7 +12,7 @@ import { rejectRequestFromMeetError } from '../models/error.model.js'; import { RecordingRepository } from '../repositories/index.js'; -import { LoggerService, RecordingService } from '../services/index.js'; +import { LoggerService, RecordingService, RequestSessionService } from '../services/index.js'; import { getBaseUrl } from '../utils/index.js'; export const startRecording = async (req: Request, res: Response) => { @@ -37,13 +37,13 @@ export const startRecording = async (req: Request, res: Response) => { export const getRecordings = async (req: Request, res: Response) => { const logger = container.get(LoggerService); const recordingService = container.get(RecordingService); + const requestSessionService = container.get(RequestSessionService); const queryParams = req.query; - // If recording token is present, retrieve only recordings for the room associated with the token - const payload = req.session?.tokenClaims; + // If room member token is present, retrieve only recordings for the room associated with the token + const roomId = requestSessionService.getRoomIdFromToken(); - if (payload && payload.video) { - const roomId = payload.video.room; + if (roomId) { queryParams.roomId = roomId; logger.info(`Getting recordings for room '${roomId}'`); } else { @@ -70,20 +70,17 @@ export const getRecordings = async (req: Request, res: Response) => { export const bulkDeleteRecordings = async (req: Request, res: Response) => { const logger = container.get(LoggerService); const recordingService = container.get(RecordingService); + const requestSessionService = container.get(RequestSessionService); const { recordingIds } = req.query; - // If recording token is present, delete only recordings for the room associated with the token - const payload = req.session?.tokenClaims; - let roomId: string | undefined; - - if (payload && payload.video) { - roomId = payload.video.room; - } - logger.info(`Deleting recordings: ${recordingIds}`); try { const recordingIdsArray = (recordingIds as string).split(','); + + // If room member token is present, delete only recordings for the room associated with the token + const roomId = requestSessionService.getRoomIdFromToken(); + const { deleted, failed } = await recordingService.bulkDeleteRecordings(recordingIdsArray, roomId); // All recordings were successfully deleted @@ -257,21 +254,17 @@ export const getRecordingUrl = async (req: Request, res: Response) => { export const downloadRecordingsZip = async (req: Request, res: Response) => { const logger = container.get(LoggerService); const recordingService = container.get(RecordingService); + const requestSessionService = container.get(RequestSessionService); const recordingIds = req.query.recordingIds as string; const recordingIdsArray = (recordingIds as string).split(','); - // If recording token is present, download only recordings for the room associated with the token - const payload = req.session?.tokenClaims; - let roomId: string | undefined; - - if (payload && payload.video) { - roomId = payload.video.room; - } - // Filter recording IDs if a room ID is provided let validRecordingIds = recordingIdsArray; + // If room member token is present, download only recordings for the room associated with the token + const roomId = requestSessionService.getRoomIdFromToken(); + if (roomId) { validRecordingIds = recordingIdsArray.filter((recordingId) => { const { roomId: recRoomId } = RecordingHelper.extractInfoFromRecordingId(recordingId); diff --git a/meet-ce/backend/src/controllers/room.controller.ts b/meet-ce/backend/src/controllers/room.controller.ts index 0a638bb8..be8eeb81 100644 --- a/meet-ce/backend/src/controllers/room.controller.ts +++ b/meet-ce/backend/src/controllers/room.controller.ts @@ -1,19 +1,19 @@ import { - AuthTransportMode, MeetRoomDeletionPolicyWithMeeting, MeetRoomDeletionPolicyWithRecordings, MeetRoomDeletionSuccessCode, MeetRoomFilters, - MeetRoomOptions, - MeetRoomRoleAndPermissions, - ParticipantRole + MeetRoomMemberRole, + MeetRoomMemberRoleAndPermissions, + MeetRoomMemberTokenOptions, + MeetRoomOptions } from '@openvidu-meet/typings'; import { Request, Response } from 'express'; import { container } from '../config/index.js'; import { INTERNAL_CONFIG } from '../config/internal-config.js'; import { handleError } from '../models/error.model.js'; -import { LoggerService, ParticipantService, RoomService } from '../services/index.js'; -import { getAuthTransportMode, getBaseUrl, getCookieOptions } from '../utils/index.js'; +import { LoggerService, RoomMemberService, RoomService } from '../services/index.js'; +import { getBaseUrl } from '../utils/index.js'; export const createRoom = async (req: Request, res: Response) => { const logger = container.get(LoggerService); @@ -52,13 +52,12 @@ export const getRoom = async (req: Request, res: Response) => { const { roomId } = req.params; const fields = req.query.fields as string | undefined; - const role = req.session?.participantRole; try { logger.verbose(`Getting room '${roomId}'`); const roomService = container.get(RoomService); - const room = await roomService.getMeetRoom(roomId, fields, role); + const room = await roomService.getMeetRoom(roomId, fields); return res.status(200).json(room); } catch (error) { @@ -183,37 +182,26 @@ export const updateRoomStatus = async (req: Request, res: Response) => { } }; -export const generateRecordingToken = async (req: Request, res: Response) => { +export const generateRoomMemberToken = async (req: Request, res: Response) => { const logger = container.get(LoggerService); - const roomService = container.get(RoomService); - const { roomId } = req.params; - const { secret } = req.body; + const roomMemberTokenService = container.get(RoomMemberService); - logger.verbose(`Generating recording token for room '${roomId}'`); + const { roomId } = req.params; + const tokenOptions: MeetRoomMemberTokenOptions = req.body; try { - const token = await roomService.generateRecordingToken(roomId, secret); - const authTransportMode = await getAuthTransportMode(); - - // Send recording token as cookie for cookie mode - if (authTransportMode === AuthTransportMode.COOKIE) { - res.cookie( - INTERNAL_CONFIG.RECORDING_TOKEN_COOKIE_NAME, - token, - getCookieOptions('/', INTERNAL_CONFIG.RECORDING_TOKEN_EXPIRATION) - ); - } - + logger.verbose(`Generating room member token for room '${roomId}'`); + const token = await roomMemberTokenService.generateOrRefreshRoomMemberToken(roomId, tokenOptions); return res.status(200).json({ token }); } catch (error) { - handleError(res, error, `generating recording token for room '${roomId}'`); + handleError(res, error, `generating room member token for room '${roomId}'`); } }; -export const getRoomRolesAndPermissions = async (req: Request, res: Response) => { +export const getRoomMemberRolesAndPermissions = async (req: Request, res: Response) => { const logger = container.get(LoggerService); const roomService = container.get(RoomService); - const participantService = container.get(ParticipantService); + const roomMemberService = container.get(RoomMemberService); const { roomId } = req.params; @@ -224,41 +212,42 @@ export const getRoomRolesAndPermissions = async (req: Request, res: Response) => return handleError(res, error, `getting room '${roomId}'`); } - logger.verbose(`Getting roles and associated permissions for room '${roomId}'`); - const moderatorPermissions = participantService.getParticipantPermissions(roomId, ParticipantRole.MODERATOR); - const speakerPermissions = participantService.getParticipantPermissions(roomId, ParticipantRole.SPEAKER); + logger.verbose(`Getting room member roles and associated permissions for room '${roomId}'`); + const moderatorPermissions = await roomMemberService.getRoomMemberPermissions(roomId, MeetRoomMemberRole.MODERATOR); + const speakerPermissions = await roomMemberService.getRoomMemberPermissions(roomId, MeetRoomMemberRole.SPEAKER); - const rolesAndPermissions = [ + const rolesAndPermissions: MeetRoomMemberRoleAndPermissions[] = [ { - role: ParticipantRole.MODERATOR, + role: MeetRoomMemberRole.MODERATOR, permissions: moderatorPermissions }, { - role: ParticipantRole.SPEAKER, + role: MeetRoomMemberRole.SPEAKER, permissions: speakerPermissions } ]; res.status(200).json(rolesAndPermissions); }; -export const getRoomRoleAndPermissions = async (req: Request, res: Response) => { +export const getRoomMemberRoleAndPermissions = async (req: Request, res: Response) => { const logger = container.get(LoggerService); - const roomService = container.get(RoomService); - const participantService = container.get(ParticipantService); + const roomMemberService = container.get(RoomMemberService); const { roomId, secret } = req.params; try { - logger.verbose(`Getting room role and associated permissions for room '${roomId}' and secret '${secret}'`); + logger.verbose( + `Getting room member role and associated permissions for room '${roomId}' and secret '${secret}'` + ); - const role = await roomService.getRoomRoleBySecret(roomId, secret); - const permissions = participantService.getParticipantPermissions(roomId, role); - const roleAndPermissions: MeetRoomRoleAndPermissions = { + const role = await roomMemberService.getRoomMemberRoleBySecret(roomId, secret); + const permissions = await roomMemberService.getRoomMemberPermissions(roomId, role); + const roleAndPermissions: MeetRoomMemberRoleAndPermissions = { role, permissions }; return res.status(200).json(roleAndPermissions); } catch (error) { - handleError(res, error, `getting room role and permissions for room '${roomId}' and secret '${secret}'`); + handleError(res, error, `getting room member role and permissions for room '${roomId}' and secret '${secret}'`); } }; diff --git a/meet-ce/backend/src/controllers/user.controller.ts b/meet-ce/backend/src/controllers/user.controller.ts index 38355975..64f18e4e 100644 --- a/meet-ce/backend/src/controllers/user.controller.ts +++ b/meet-ce/backend/src/controllers/user.controller.ts @@ -1,10 +1,11 @@ import { Request, Response } from 'express'; import { container } from '../config/index.js'; import { errorUnauthorized, handleError, rejectRequestFromMeetError } from '../models/error.model.js'; -import { UserService } from '../services/index.js'; +import { RequestSessionService, UserService } from '../services/index.js'; export const getProfile = (req: Request, res: Response) => { - const user = req.session?.user; + const requestSessionService = container.get(RequestSessionService); + const user = requestSessionService.getUser(); if (!user) { const error = errorUnauthorized(); @@ -17,7 +18,8 @@ export const getProfile = (req: Request, res: Response) => { }; export const changePassword = async (req: Request, res: Response) => { - const user = req.session?.user; + const requestSessionService = container.get(RequestSessionService); + const user = requestSessionService.getUser(); if (!user) { const error = errorUnauthorized(); diff --git a/meet-ce/backend/src/middlewares/auth.middleware.ts b/meet-ce/backend/src/middlewares/auth.middleware.ts index 64d16087..4489738a 100644 --- a/meet-ce/backend/src/middlewares/auth.middleware.ts +++ b/meet-ce/backend/src/middlewares/auth.middleware.ts @@ -1,4 +1,4 @@ -import { MeetTokenMetadata, OpenViduMeetPermissions, ParticipantRole, User, UserRole } from '@openvidu-meet/typings'; +import { LiveKitPermissions, MeetUser, MeetUserRole } from '@openvidu-meet/typings'; import { NextFunction, Request, RequestHandler, Response } from 'express'; import rateLimit from 'express-rate-limit'; import { ClaimGrants } from 'livekit-server-sdk'; @@ -8,9 +8,7 @@ import { INTERNAL_CONFIG } from '../config/internal-config.js'; import { errorInsufficientPermissions, errorInvalidApiKey, - errorInvalidParticipantRole, - errorInvalidParticipantToken, - errorInvalidRecordingToken, + errorInvalidRoomMemberToken, errorInvalidToken, errorInvalidTokenSubject, errorUnauthorized, @@ -20,12 +18,12 @@ import { import { ApiKeyService, LoggerService, - ParticipantService, - RoomService, + RequestSessionService, + RoomMemberService, TokenService, UserService } from '../services/index.js'; -import { getAccessToken, getParticipantToken, getRecordingToken } from '../utils/index.js'; +import { getAccessToken, getRoomMemberToken } from '../utils/index.js'; /** * Interface for authentication validators. @@ -82,15 +80,15 @@ export const withAuth = (...validators: AuthValidator[]): RequestHandler => { * * @param roles One or more roles that are allowed to access the resource */ -export const tokenAndRoleValidator = (...roles: UserRole[]): AuthValidator => { +export const tokenAndRoleValidator = (...roles: MeetUserRole[]): AuthValidator => { return { async isPresent(req: Request): Promise { - const token = await getAccessToken(req); + const token = getAccessToken(req); return !!token; }, async validate(req: Request): Promise { - const token = await getAccessToken(req); + const token = getAccessToken(req); if (!token) { throw errorUnauthorized(); @@ -120,104 +118,61 @@ export const tokenAndRoleValidator = (...roles: UserRole[]): AuthValidator => { throw errorInsufficientPermissions(); } - req.session = req.session || {}; - req.session.user = user; + const requestSessionService = container.get(RequestSessionService); + requestSessionService.setUser(user); } }; }; /** - * Participant token validator for room access. - * Validates participant tokens and checks role permissions. + * Room member token validator for room access. + * Validates room member tokens and checks role permissions. */ -export const participantTokenValidator: AuthValidator = { +export const roomMemberTokenValidator: AuthValidator = { async isPresent(req: Request): Promise { - const token = await getParticipantToken(req); + const token = getRoomMemberToken(req); return !!token; }, async validate(req: Request): Promise { - const token = await getParticipantToken(req); - await validateTokenAndSetSession(req, token); + const token = getRoomMemberToken(req); - // Check if the participant role is provided in the request headers - // This is required to distinguish roles when multiple are present in the token - const participantRole = req.headers[INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER]; - const allRoles = [ParticipantRole.MODERATOR, ParticipantRole.SPEAKER]; - - if (!participantRole || !allRoles.includes(participantRole as ParticipantRole)) { - throw errorInvalidParticipantRole(); + if (!token) { + throw errorUnauthorized(); } - // Check that the specified role is present in the token claims - let metadata: MeetTokenMetadata; + let tokenMetadata: string | undefined; + let livekitPermissions: LiveKitPermissions | undefined; try { - const participantService = container.get(ParticipantService); - metadata = participantService.parseMetadata(req.session?.tokenClaims?.metadata || '{}'); + const tokenService = container.get(TokenService); + ({ metadata: tokenMetadata, video: livekitPermissions } = await tokenService.verifyToken(token)); + + if (!tokenMetadata || !livekitPermissions) { + throw new Error('Missing required token claims'); + } } catch (error) { - const logger = container.get(LoggerService); - logger.error('Invalid participant token:', error); - throw errorInvalidParticipantToken(); + throw errorInvalidToken(); } - const roles = metadata.roles; - const hasRole = roles.some( - (r: { role: ParticipantRole; permissions: OpenViduMeetPermissions }) => r.role === participantRole - ); + const requestSessionService = container.get(RequestSessionService); - if (!hasRole) { - throw errorInsufficientPermissions(); - } - - // Set the participant role in the session - req.session!.participantRole = participantRole as ParticipantRole; - } -}; - -/** - * Recording token validator for recording access. - * Validates recording tokens with specific metadata. - */ -export const recordingTokenValidator: AuthValidator = { - async isPresent(req: Request): Promise { - const token = await getRecordingToken(req); - return !!token; - }, - - async validate(req: Request): Promise { - const token = await getRecordingToken(req); - await validateTokenAndSetSession(req, token); - - // Validate the recording token metadata + // Validate the room member token metadata and extract role and permissions try { - const roomService = container.get(RoomService); - roomService.parseRecordingTokenMetadata(req.session?.tokenClaims?.metadata || '{}'); + const roomMemberService = container.get(RoomMemberService); + const { role, permissions: meetPermissions } = + roomMemberService.parseRoomMemberTokenMetadata(tokenMetadata); + + requestSessionService.setRoomMemberTokenInfo(role, meetPermissions, livekitPermissions); } catch (error) { const logger = container.get(LoggerService); - logger.error('Invalid recording token:', error); - throw errorInvalidRecordingToken(); + logger.error('Invalid room member token:', error); + throw errorInvalidRoomMemberToken(); } - } -}; -const validateTokenAndSetSession = async (req: Request, token: string | undefined) => { - if (!token) { - throw errorUnauthorized(); - } - - const tokenService = container.get(TokenService); - let payload: ClaimGrants; - - try { - payload = await tokenService.verifyToken(token); + // Set authenticated user if present, otherwise anonymous const user = await getAuthenticatedUserOrAnonymous(req); - - req.session = req.session || {}; - req.session.tokenClaims = payload; - req.session.user = user; - } catch (error) { - throw errorInvalidToken(); + requestSessionService.setUser(user); } }; @@ -248,8 +203,8 @@ export const apiKeyValidator: AuthValidator = { const userService = container.get(UserService); const apiUser = userService.getApiUser(); - req.session = req.session || {}; - req.session.user = apiUser; + const requestSessionService = container.get(RequestSessionService); + requestSessionService.setUser(apiUser); } }; @@ -266,18 +221,18 @@ export const allowAnonymous: AuthValidator = { async validate(req: Request): Promise { const user = await getAuthenticatedUserOrAnonymous(req); - req.session = req.session || {}; - req.session.user = user; + const requestSessionService = container.get(RequestSessionService); + requestSessionService.setUser(user); } }; // Return the authenticated user if available, otherwise return an anonymous user -const getAuthenticatedUserOrAnonymous = async (req: Request): Promise => { +const getAuthenticatedUserOrAnonymous = async (req: Request): Promise => { const userService = container.get(UserService); - let user: User | null = null; + let user: MeetUser | null = null; // Check if there is a user already authenticated - const token = await getAccessToken(req); + const token = getAccessToken(req); if (token) { try { diff --git a/meet-ce/backend/src/middlewares/base-url.middleware.ts b/meet-ce/backend/src/middlewares/base-url.middleware.ts new file mode 100644 index 00000000..ec3c0167 --- /dev/null +++ b/meet-ce/backend/src/middlewares/base-url.middleware.ts @@ -0,0 +1,14 @@ +import { NextFunction, Request, Response } from 'express'; +import { container } from '../config/dependency-injector.config.js'; +import { BaseUrlService } from '../services/index.js'; + +export const setBaseUrlMiddleware = (req: Request, _res: Response, next: NextFunction) => { + if (req.path === '/livekit/webhook') { + // Skip setting base URL for LiveKit webhooks + return next(); + } + + const baseUrlService = container.get(BaseUrlService); + baseUrlService.setBaseUrlFromRequest(req); + next(); +}; diff --git a/meet-ce/backend/src/middlewares/http-context.middleware.ts b/meet-ce/backend/src/middlewares/http-context.middleware.ts deleted file mode 100644 index 3fd29fca..00000000 --- a/meet-ce/backend/src/middlewares/http-context.middleware.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NextFunction, Request, Response } from 'express'; -import { container } from '../config/dependency-injector.config.js'; -import { HttpContextService } from '../services/index.js'; - -export const httpContextMiddleware = (req: Request, _res: Response, next: NextFunction) => { - if (req.path === '/livekit/webhook') { - // Skip setting context for LiveKit webhooks - return next(); - } - - const httpContextService = container.get(HttpContextService); - httpContextService.setContext(req); - next(); -}; diff --git a/meet-ce/backend/src/middlewares/index.ts b/meet-ce/backend/src/middlewares/index.ts index bc510633..9244b7bf 100644 --- a/meet-ce/backend/src/middlewares/index.ts +++ b/meet-ce/backend/src/middlewares/index.ts @@ -1,5 +1,6 @@ export * from './content-type.middleware.js'; -export * from './http-context.middleware.js'; +export * from './base-url.middleware.js'; +export * from './request-context.middleware.js'; export * from './auth.middleware.js'; export * from './room.middleware.js'; export * from './participant.middleware.js'; diff --git a/meet-ce/backend/src/middlewares/participant.middleware.ts b/meet-ce/backend/src/middlewares/participant.middleware.ts index 5175bacd..e0c894e4 100644 --- a/meet-ce/backend/src/middlewares/participant.middleware.ts +++ b/meet-ce/backend/src/middlewares/participant.middleware.ts @@ -1,28 +1,30 @@ -import { AuthMode, ParticipantOptions, ParticipantRole, UserRole } from '@openvidu-meet/typings'; +import { AuthMode, MeetRoomMemberRole, MeetRoomMemberTokenOptions, MeetUserRole } from '@openvidu-meet/typings'; import { NextFunction, Request, Response } from 'express'; import { container } from '../config/index.js'; import { errorInsufficientPermissions, handleError, rejectRequestFromMeetError } from '../models/error.model.js'; -import { GlobalConfigService, RoomService } from '../services/index.js'; +import { GlobalConfigService, RequestSessionService, RoomMemberService } from '../services/index.js'; import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middleware.js'; /** - * Middleware to configure authentication based on participant role and authentication mode for entering a room. + * Middleware to configure authentication for generating token to access room and its resources + * based on room member role and authentication mode. * - * - If the authentication mode is MODERATORS_ONLY and the participant role is MODERATOR, configure user authentication. + * - If the authentication mode is MODERATORS_ONLY and the room member role is MODERATOR, configure user authentication. * - If the authentication mode is ALL_USERS, configure user authentication. * - Otherwise, allow anonymous access. */ -export const configureParticipantTokenAuth = async (req: Request, res: Response, next: NextFunction) => { +export const configureRoomMemberTokenAuth = async (req: Request, res: Response, next: NextFunction) => { const configService = container.get(GlobalConfigService); - const roomService = container.get(RoomService); + const roomMemberService = container.get(RoomMemberService); - let role: ParticipantRole; + let role: MeetRoomMemberRole; try { - const { roomId, secret } = req.body as ParticipantOptions; - role = await roomService.getRoomRoleBySecret(roomId, secret); + const { roomId } = req.params; + const { secret } = req.body as MeetRoomMemberTokenOptions; + role = await roomMemberService.getRoomMemberRoleBySecret(roomId, secret); } catch (error) { - return handleError(res, error, 'getting room role by secret'); + return handleError(res, error, 'getting room member role by secret'); } let authModeToAccessRoom: AuthMode; @@ -40,11 +42,11 @@ export const configureParticipantTokenAuth = async (req: Request, res: Response, authValidators.push(allowAnonymous); } else { const isModeratorsOnlyMode = - authModeToAccessRoom === AuthMode.MODERATORS_ONLY && role === ParticipantRole.MODERATOR; + authModeToAccessRoom === AuthMode.MODERATORS_ONLY && role === MeetRoomMemberRole.MODERATOR; const isAllUsersMode = authModeToAccessRoom === AuthMode.ALL_USERS; if (isModeratorsOnlyMode || isAllUsersMode) { - authValidators.push(tokenAndRoleValidator(UserRole.USER)); + authValidators.push(tokenAndRoleValidator(MeetUserRole.USER)); } else { authValidators.push(allowAnonymous); } @@ -55,17 +57,17 @@ export const configureParticipantTokenAuth = async (req: Request, res: Response, export const withModeratorPermissions = async (req: Request, res: Response, next: NextFunction) => { const { roomId } = req.params; - const payload = req.session?.tokenClaims; - const role = req.session?.participantRole; - if (!payload || !role) { + const requestSessionService = container.get(RequestSessionService); + const tokenRoomId = requestSessionService.getRoomIdFromToken(); + const role = requestSessionService.getRoomMemberRole(); + + if (!tokenRoomId || !role) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } - const sameRoom = payload.video?.room === roomId; - - if (!sameRoom || role !== ParticipantRole.MODERATOR) { + if (tokenRoomId !== roomId || role !== MeetRoomMemberRole.MODERATOR) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } @@ -75,16 +77,11 @@ export const withModeratorPermissions = async (req: Request, res: Response, next export const checkParticipantFromSameRoom = async (req: Request, res: Response, next: NextFunction) => { const { roomId } = req.params; - const payload = req.session?.tokenClaims; - if (!payload) { - const error = errorInsufficientPermissions(); - return rejectRequestFromMeetError(res, error); - } + const requestSessionService = container.get(RequestSessionService); + const tokenRoomId = requestSessionService.getRoomIdFromToken(); - const sameRoom = payload.video?.room === roomId; - - if (!sameRoom) { + if (!tokenRoomId || tokenRoomId !== roomId) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } diff --git a/meet-ce/backend/src/middlewares/recording.middleware.ts b/meet-ce/backend/src/middlewares/recording.middleware.ts index 074ed91d..13e48469 100644 --- a/meet-ce/backend/src/middlewares/recording.middleware.ts +++ b/meet-ce/backend/src/middlewares/recording.middleware.ts @@ -1,4 +1,4 @@ -import { MeetRoom, UserRole } from '@openvidu-meet/typings'; +import { MeetRoom, MeetUserRole } from '@openvidu-meet/typings'; import { NextFunction, Request, Response } from 'express'; import { container } from '../config/index.js'; import { RecordingHelper } from '../helpers/index.js'; @@ -11,11 +11,11 @@ import { rejectRequestFromMeetError } from '../models/error.model.js'; import { RecordingRepository } from '../repositories/index.js'; -import { LoggerService, ParticipantService, RoomService } from '../services/index.js'; +import { LoggerService, RequestSessionService, RoomService } from '../services/index.js'; import { allowAnonymous, apiKeyValidator, - recordingTokenValidator, + roomMemberTokenValidator, tokenAndRoleValidator, withAuth } from './auth.middleware.js'; @@ -42,22 +42,17 @@ export const withRecordingEnabled = async (req: Request, res: Response, next: Ne export const withCanRecordPermission = async (req: Request, res: Response, next: NextFunction) => { const roomId = extractRoomIdFromRequest(req); - const payload = req.session?.tokenClaims; - const role = req.session?.participantRole; - if (!payload || !role) { + const requestSessionService = container.get(RequestSessionService); + const tokenRoomId = requestSessionService.getRoomIdFromToken(); + const permissions = requestSessionService.getRoomMemberMeetPermissions(); + + if (!tokenRoomId || !permissions) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } - const participantService = container.get(ParticipantService); - const metadata = participantService.parseMetadata(payload.metadata || '{}'); - - const sameRoom = payload.video?.room === roomId; - const permissions = metadata.roles.find((r) => r.role === role)?.permissions; - const canRecord = permissions?.canRecord; - - if (!sameRoom || !canRecord) { + if (tokenRoomId !== roomId || !permissions.canRecord) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } @@ -67,7 +62,9 @@ export const withCanRecordPermission = async (req: Request, res: Response, next: export const withCanRetrieveRecordingsPermission = async (req: Request, res: Response, next: NextFunction) => { const roomId = extractRoomIdFromRequest(req); - const payload = req.session?.tokenClaims; + + 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: @@ -77,17 +74,20 @@ export const withCanRetrieveRecordingsPermission = async (req: Request, res: Res * - The user is anonymous and is using the public access secret. * - The user is using the private access secret and is authenticated. */ - if (!payload) { + if (!tokenRoomId) { return next(); } - const roomService = container.get(RoomService); - const metadata = roomService.parseRecordingTokenMetadata(payload.metadata || '{}'); + const permissions = requestSessionService.getRoomMemberMeetPermissions(); - const sameRoom = roomId ? payload.video?.room === roomId : true; - const canRetrieveRecordings = metadata.recordingPermissions.canRetrieveRecordings; + if (!permissions) { + const error = errorInsufficientPermissions(); + return rejectRequestFromMeetError(res, error); + } - if (!sameRoom || !canRetrieveRecordings) { + const sameRoom = roomId ? tokenRoomId === roomId : true; + + if (!sameRoom || !permissions.canRetrieveRecordings) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } @@ -97,21 +97,26 @@ export const withCanRetrieveRecordingsPermission = async (req: Request, res: Res export const withCanDeleteRecordingsPermission = async (req: Request, res: Response, next: NextFunction) => { const roomId = extractRoomIdFromRequest(req); - const payload = req.session?.tokenClaims; + + const requestSessionService = container.get(RequestSessionService); + const tokenRoomId = requestSessionService.getRoomIdFromToken(); // If there is no token, the user is admin or it is invoked using the API key // In this case, the user is allowed to access the resource - if (!payload) { + if (!tokenRoomId) { return next(); } - const roomService = container.get(RoomService); - const metadata = roomService.parseRecordingTokenMetadata(payload.metadata || '{}'); + const permissions = requestSessionService.getRoomMemberMeetPermissions(); - const sameRoom = roomId ? payload.video?.room === roomId : true; - const canDeleteRecordings = metadata.recordingPermissions.canDeleteRecordings; + if (!permissions) { + const error = errorInsufficientPermissions(); + return rejectRequestFromMeetError(res, error); + } - if (!sameRoom || !canDeleteRecordings) { + const sameRoom = roomId ? tokenRoomId === roomId : true; + + if (!sameRoom || !permissions.canDeleteRecordings) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } @@ -123,7 +128,7 @@ export const withCanDeleteRecordingsPermission = async (req: Request, res: Respo * Middleware to configure authentication for retrieving recording based on the provided secret. * * - If a valid secret is provided in the query, access is granted according to the secret type. - * - If no secret is provided, the default authentication logic is applied, i.e., API key, admin and recording token access. + * - If no secret is provided, the default authentication logic is applied, i.e., API key, admin and room member token access. */ export const configureRecordingAuth = async (req: Request, res: Response, next: NextFunction) => { const secret = req.query.secret as string; @@ -151,7 +156,7 @@ export const configureRecordingAuth = async (req: Request, res: Response, next: break; case recordingSecrets.privateAccessSecret: // Private access secret requires authentication with user role - authValidators.push(tokenAndRoleValidator(UserRole.USER)); + authValidators.push(tokenAndRoleValidator(MeetUserRole.USER)); break; default: // Invalid secret provided @@ -165,8 +170,8 @@ export const configureRecordingAuth = async (req: Request, res: Response, next: } // If no secret is provided, we proceed with the default authentication logic. - // This will allow API key, admin and recording token access. - const authValidators = [apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), recordingTokenValidator]; + // This will allow API key, admin and room member token access. + const authValidators = [apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN), roomMemberTokenValidator]; return withAuth(...authValidators)(req, res, next); }; diff --git a/meet-ce/backend/src/middlewares/request-context.middleware.ts b/meet-ce/backend/src/middlewares/request-context.middleware.ts new file mode 100644 index 00000000..e66d4ff7 --- /dev/null +++ b/meet-ce/backend/src/middlewares/request-context.middleware.ts @@ -0,0 +1,28 @@ +import { NextFunction, Request, Response } from 'express'; +import { container } from '../config/index.js'; +import { RequestSessionService } from '../services/index.js'; + +/** + * Middleware that initializes the AsyncLocalStorage context for each HTTP request. + * + * This middleware MUST be registered before any other middleware or route handler + * that needs to access the RequestSessionService. It creates an isolated context + * for the entire request lifecycle, ensuring that data stored in RequestSessionService + * is unique to each request and doesn't leak between concurrent requests. + * + * How it works: + * 1. Gets the singleton RequestSessionService from the container + * 2. Calls requestSessionService.run() which creates a new AsyncLocalStorage context + * 3. All subsequent code (middlewares, controllers) executed within this context + * will have access to the same isolated storage + * 4. The context is automatically cleaned up when the request completes + */ +export const initRequestContext = (_req: Request, _res: Response, next: NextFunction) => { + const requestSessionService = container.get(RequestSessionService); + + // Wrap the rest of the request handling in the AsyncLocalStorage context + // All subsequent middlewares and route handlers will execute within this context + requestSessionService.run(() => { + next(); + }); +}; diff --git a/meet-ce/backend/src/middlewares/request-validators/config-validator.middleware.ts b/meet-ce/backend/src/middlewares/request-validators/config-validator.middleware.ts index 7fc2d8be..915b15d0 100644 --- a/meet-ce/backend/src/middlewares/request-validators/config-validator.middleware.ts +++ b/meet-ce/backend/src/middlewares/request-validators/config-validator.middleware.ts @@ -1,7 +1,6 @@ import { AuthenticationConfig, AuthMode, - AuthTransportMode, AuthType, SecurityConfig, SingleUserAuth, @@ -40,8 +39,6 @@ const WebhookTestSchema = z.object({ .regex(/^https?:\/\//, { message: 'URL must start with http:// or https://' }) }); -const AuthTransportModeSchema: z.ZodType = z.nativeEnum(AuthTransportMode); - const AuthModeSchema: z.ZodType = z.nativeEnum(AuthMode); const AuthTypeSchema: z.ZodType = z.nativeEnum(AuthType); @@ -54,7 +51,6 @@ const ValidAuthMethodSchema: z.ZodType = SingleUserAuthSchema; const AuthenticationConfigSchema: z.ZodType = z.object({ authMethod: ValidAuthMethodSchema, - authTransportMode: AuthTransportModeSchema, authModeToAccessRoom: AuthModeSchema }); diff --git a/meet-ce/backend/src/middlewares/request-validators/participant-validator.middleware.ts b/meet-ce/backend/src/middlewares/request-validators/participant-validator.middleware.ts index 7e6009a2..a9d5323b 100644 --- a/meet-ce/backend/src/middlewares/request-validators/participant-validator.middleware.ts +++ b/meet-ce/backend/src/middlewares/request-validators/participant-validator.middleware.ts @@ -1,53 +1,26 @@ -import { - MeetTokenMetadata, - OpenViduMeetPermissions, - ParticipantOptions, - ParticipantRole -} from '@openvidu-meet/typings'; +import { MeetPermissions, MeetRoomMemberRole, MeetRoomMemberTokenMetadata } from '@openvidu-meet/typings'; import { NextFunction, Request, Response } from 'express'; import { z } from 'zod'; import { rejectUnprocessableRequest } from '../../models/error.model.js'; -import { nonEmptySanitizedRoomId } from './room-validator.middleware.js'; - -const ParticipantTokenRequestSchema: z.ZodType = z.object({ - roomId: nonEmptySanitizedRoomId('roomId'), - secret: z.string().nonempty('Secret is required'), - participantName: z.string().optional(), - participantIdentity: z.string().optional() -}); const UpdateParticipantRequestSchema = z.object({ - role: z.nativeEnum(ParticipantRole) + role: z.nativeEnum(MeetRoomMemberRole) }); -const OpenViduMeetPermissionsSchema: z.ZodType = z.object({ +const MeetPermissionsSchema: z.ZodType = z.object({ canRecord: z.boolean(), + canRetrieveRecordings: z.boolean(), + canDeleteRecordings: z.boolean(), canChat: z.boolean(), canChangeVirtualBackground: z.boolean() }); -const MeetTokenMetadataSchema: z.ZodType = z.object({ +const RoomMemberTokenMetadataSchema: z.ZodType = z.object({ livekitUrl: z.string().url('LiveKit URL must be a valid URL'), - roles: z.array( - z.object({ - role: z.nativeEnum(ParticipantRole), - permissions: OpenViduMeetPermissionsSchema - }) - ), - selectedRole: z.nativeEnum(ParticipantRole) + role: z.nativeEnum(MeetRoomMemberRole), + permissions: MeetPermissionsSchema }); -export const validateParticipantTokenRequest = (req: Request, res: Response, next: NextFunction) => { - const { success, error, data } = ParticipantTokenRequestSchema.safeParse(req.body); - - if (!success) { - return rejectUnprocessableRequest(res, error); - } - - req.body = data; - next(); -}; - export const validateUpdateParticipantRequest = (req: Request, res: Response, next: NextFunction) => { const { success, error, data } = UpdateParticipantRequestSchema.safeParse(req.body); @@ -59,8 +32,8 @@ export const validateUpdateParticipantRequest = (req: Request, res: Response, ne next(); }; -export const validateMeetTokenMetadata = (metadata: unknown): MeetTokenMetadata => { - const { success, error, data } = MeetTokenMetadataSchema.safeParse(metadata); +export const validateRoomMemberTokenMetadata = (metadata: unknown): MeetRoomMemberTokenMetadata => { + const { success, error, data } = RoomMemberTokenMetadataSchema.safeParse(metadata); if (!success) { throw new Error(`Invalid metadata: ${error.message}`); diff --git a/meet-ce/backend/src/middlewares/request-validators/room-validator.middleware.ts b/meet-ce/backend/src/middlewares/request-validators/room-validator.middleware.ts index bd55d428..8ecf4973 100644 --- a/meet-ce/backend/src/middlewares/request-validators/room-validator.middleware.ts +++ b/meet-ce/backend/src/middlewares/request-validators/room-validator.middleware.ts @@ -9,13 +9,12 @@ import { MeetRoomDeletionPolicyWithMeeting, MeetRoomDeletionPolicyWithRecordings, MeetRoomFilters, + MeetRoomMemberTokenOptions, MeetRoomOptions, MeetRoomStatus, MeetRoomTheme, MeetRoomThemeMode, - MeetVirtualBackgroundConfig, - ParticipantRole, - RecordingPermissions + MeetVirtualBackgroundConfig } from '@openvidu-meet/typings'; import { NextFunction, Request, Response } from 'express'; import ms from 'ms'; @@ -274,22 +273,26 @@ const UpdateRoomConfigSchema = z.object({ }); const UpdateRoomStatusSchema = z.object({ - status: z.nativeEnum(MeetRoomStatus) + status: z.enum([MeetRoomStatus.OPEN, MeetRoomStatus.CLOSED]) }); -const RecordingTokenRequestSchema = z.object({ - secret: z.string().nonempty('Secret is required') -}); - -const RecordingPermissionsSchema: z.ZodType = z.object({ - canRetrieveRecordings: z.boolean(), - canDeleteRecordings: z.boolean() -}); - -const RecordingTokenMetadataSchema = z.object({ - role: z.nativeEnum(ParticipantRole), - recordingPermissions: RecordingPermissionsSchema -}); +const RoomMemberTokenRequestSchema: z.ZodType = z + .object({ + secret: z.string().nonempty('Secret is required'), + grantJoinMeetingPermission: z.boolean().optional().default(false), + participantName: z.string().optional(), + participantIdentity: z.string().optional() + }) + .refine( + (data) => { + // If grantJoinMeetingPermission is true, participantName must be provided + return !data.grantJoinMeetingPermission || data.participantName; + }, + { + message: 'participantName is required when grantJoinMeetingPermission is true', + path: ['participantName'] + } + ); export const withValidRoomOptions = (req: Request, res: Response, next: NextFunction) => { const { success, error, data } = RoomRequestOptionsSchema.safeParse(req.body); @@ -380,8 +383,8 @@ export const withValidRoomStatus = (req: Request, res: Response, next: NextFunct next(); }; -export const withValidRoomSecret = (req: Request, res: Response, next: NextFunction) => { - const { success, error, data } = RecordingTokenRequestSchema.safeParse(req.body); +export const withValidRoomMemberTokenRequest = (req: Request, res: Response, next: NextFunction) => { + const { success, error, data } = RoomMemberTokenRequestSchema.safeParse(req.body); if (!success) { return rejectUnprocessableRequest(res, error); @@ -390,13 +393,3 @@ export const withValidRoomSecret = (req: Request, res: Response, next: NextFunct req.body = data; next(); }; - -export const validateRecordingTokenMetadata = (metadata: unknown) => { - const { success, error, data } = RecordingTokenMetadataSchema.safeParse(metadata); - - if (!success) { - throw new Error(`Invalid metadata: ${error.message}`); - } - - return data; -}; diff --git a/meet-ce/backend/src/middlewares/room.middleware.ts b/meet-ce/backend/src/middlewares/room.middleware.ts index fe0ae18e..d242578e 100644 --- a/meet-ce/backend/src/middlewares/room.middleware.ts +++ b/meet-ce/backend/src/middlewares/room.middleware.ts @@ -1,13 +1,7 @@ -import { AuthMode, MeetRecordingAccess, ParticipantRole, UserRole } from '@openvidu-meet/typings'; import { NextFunction, Request, Response } from 'express'; import { container } from '../config/index.js'; -import { - errorInsufficientPermissions, - handleError, - rejectRequestFromMeetError -} from '../models/error.model.js'; -import { GlobalConfigService, RoomService } from '../services/index.js'; -import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middleware.js'; +import { errorInsufficientPermissions, rejectRequestFromMeetError } from '../models/error.model.js'; +import { RequestSessionService } from '../services/index.js'; /** * Middleware that configures authorization for accessing a specific room. @@ -18,80 +12,21 @@ import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middlewa */ export const configureRoomAuthorization = async (req: Request, res: Response, next: NextFunction) => { const roomId = req.params.roomId as string; - const payload = req.session?.tokenClaims; + + const requestSessionService = container.get(RequestSessionService); + const tokenRoomId = requestSessionService.getRoomIdFromToken(); // If there is no token, the user is admin or it is invoked using the API key // In this case, the user is allowed to access the resource - if (!payload) { + if (!tokenRoomId) { return next(); } - const sameRoom = payload.video?.room === roomId; - // If the user does not belong to the requested room, access is denied - if (!sameRoom) { + if (tokenRoomId !== roomId) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } return next(); }; - -/** - * Middleware to configure authentication based on participant role and authentication mode to access a room - * for generating a token for retrieving/deleting recordings. - * - * - If the authentication mode is MODERATORS_ONLY and the participant role is MODERATOR, configure user authentication. - * - If the authentication mode is ALL_USERS, configure user authentication. - * - Otherwise, allow anonymous access. - */ -export const configureRecordingTokenAuth = async (req: Request, res: Response, next: NextFunction) => { - const configService = container.get(GlobalConfigService); - const roomService = container.get(RoomService); - - let role: ParticipantRole; - - try { - const roomId = req.params.roomId; - const { secret } = req.body; - const room = await roomService.getMeetRoom(roomId); - - const recordingAccess = room.config.recording.allowAccessTo; - - if (!recordingAccess || recordingAccess === MeetRecordingAccess.ADMIN) { - // Deny request if the room is configured to allow access to recordings only for admins - throw errorInsufficientPermissions(); - } - - role = await roomService.getRoomRoleBySecret(roomId, secret); - } catch (error) { - return handleError(res, error, 'getting room role by secret'); - } - - let authModeToAccessRoom: AuthMode; - - try { - const { securityConfig } = await configService.getGlobalConfig(); - authModeToAccessRoom = securityConfig.authentication.authModeToAccessRoom; - } catch (error) { - return handleError(res, error, 'checking authentication config'); - } - - const authValidators = []; - - if (authModeToAccessRoom === AuthMode.NONE) { - authValidators.push(allowAnonymous); - } else { - const isModeratorsOnlyMode = - authModeToAccessRoom === AuthMode.MODERATORS_ONLY && role === ParticipantRole.MODERATOR; - const isAllUsersMode = authModeToAccessRoom === AuthMode.ALL_USERS; - - if (isModeratorsOnlyMode || isAllUsersMode) { - authValidators.push(tokenAndRoleValidator(UserRole.USER)); - } else { - authValidators.push(allowAnonymous); - } - } - - return withAuth(...authValidators)(req, res, next); -}; diff --git a/meet-ce/backend/src/models/error.model.ts b/meet-ce/backend/src/models/error.model.ts index b34a9de0..5b411c6f 100644 --- a/meet-ce/backend/src/models/error.model.ts +++ b/meet-ce/backend/src/models/error.model.ts @@ -167,10 +167,6 @@ export const errorRecordingsNotFromSameRoom = (roomId: string): OpenViduMeetErro ); }; -export const errorInvalidRecordingToken = (): OpenViduMeetError => { - return new OpenViduMeetError('Recording', 'Invalid recording token', 400); -}; - const isMatchingError = (error: OpenViduMeetError, originalError: OpenViduMeetError): boolean => { return ( error instanceof OpenViduMeetError && @@ -217,6 +213,14 @@ export const errorDeletingRoom = (errorCode: MeetRoomDeletionErrorCode, message: return new OpenViduMeetError(errorCode, message, 409); }; +export const errorInvalidRoomMemberToken = (): OpenViduMeetError => { + return new OpenViduMeetError('Room Error', 'Invalid room member token', 400); +}; + +export const errorInvalidRoomMemberRole = (): OpenViduMeetError => { + return new OpenViduMeetError('Room Error', 'No valid room member role provided', 400); +}; + // Participant errors export const errorParticipantNotFound = (participantIdentity: string, roomId: string): OpenViduMeetError => { @@ -227,22 +231,6 @@ export const errorParticipantNotFound = (participantIdentity: string, roomId: st ); }; -export const errorParticipantTokenNotPresent = (): OpenViduMeetError => { - return new OpenViduMeetError('Participant Error', 'No participant token provided', 400); -}; - -export const errorInvalidParticipantToken = (): OpenViduMeetError => { - return new OpenViduMeetError('Participant Error', 'Invalid participant token', 400); -}; - -export const errorInvalidParticipantRole = (): OpenViduMeetError => { - return new OpenViduMeetError('Participant Error', 'No valid participant role provided', 400); -}; - -export const errorParticipantIdentityNotProvided = (): OpenViduMeetError => { - return new OpenViduMeetError('Participant Error', 'No participant identity provided', 400); -}; - // Webhook errors export const errorInvalidWebhookUrl = (url: string, reason: string): OpenViduMeetError => { diff --git a/meet-ce/backend/src/repositories/schemas/global-config.schema.ts b/meet-ce/backend/src/repositories/schemas/global-config.schema.ts index 8110349c..ec81ff08 100644 --- a/meet-ce/backend/src/repositories/schemas/global-config.schema.ts +++ b/meet-ce/backend/src/repositories/schemas/global-config.schema.ts @@ -1,4 +1,4 @@ -import { AuthMode, AuthTransportMode, AuthType, GlobalConfig, MeetRoomThemeMode } from '@openvidu-meet/typings'; +import { AuthMode, AuthType, GlobalConfig, MeetRoomThemeMode } from '@openvidu-meet/typings'; import { Document, model, Schema } from 'mongoose'; /** @@ -31,11 +31,6 @@ const AuthenticationConfigSchema = new Schema( type: AuthMethodSchema, required: true }, - authTransportMode: { - type: String, - enum: Object.values(AuthTransportMode), - required: true - }, authModeToAccessRoom: { type: String, enum: Object.values(AuthMode), diff --git a/meet-ce/backend/src/repositories/schemas/user.schema.ts b/meet-ce/backend/src/repositories/schemas/user.schema.ts index 812c62ea..6e80cf35 100644 --- a/meet-ce/backend/src/repositories/schemas/user.schema.ts +++ b/meet-ce/backend/src/repositories/schemas/user.schema.ts @@ -1,11 +1,11 @@ -import { User, UserRole } from '@openvidu-meet/typings'; +import { MeetUser, MeetUserRole } from '@openvidu-meet/typings'; import { Document, model, Schema } from 'mongoose'; /** * Mongoose Document interface for User. * Extends the User interface with MongoDB Document functionality. */ -export interface MeetUserDocument extends User, Document {} +export interface MeetUserDocument extends MeetUser, Document {} /** * Mongoose schema for User entity. @@ -23,9 +23,9 @@ const MeetUserSchema = new Schema( }, roles: { type: [String], - enum: Object.values(UserRole), + enum: Object.values(MeetUserRole), required: true, - default: [UserRole.USER] + default: [MeetUserRole.USER] } }, { diff --git a/meet-ce/backend/src/repositories/user.repository.ts b/meet-ce/backend/src/repositories/user.repository.ts index afe8fb6b..32130607 100644 --- a/meet-ce/backend/src/repositories/user.repository.ts +++ b/meet-ce/backend/src/repositories/user.repository.ts @@ -1,17 +1,17 @@ -import { User } from '@openvidu-meet/typings'; +import { MeetUser } from '@openvidu-meet/typings'; import { inject, injectable } from 'inversify'; import { LoggerService } from '../services/logger.service.js'; import { BaseRepository } from './base.repository.js'; import { MeetUserDocument, MeetUserModel } from './schemas/user.schema.js'; /** - * Repository for managing User entities in MongoDB. + * Repository for managing MeetUser entities in MongoDB. * Provides CRUD operations and specialized queries for user data. * - * @template TUser - The domain type extending User (default: User) + * @template TUser - The domain type extending MeetUser (default: MeetUser) */ @injectable() -export class UserRepository extends BaseRepository { +export class UserRepository extends BaseRepository { constructor(@inject(LoggerService) logger: LoggerService) { super(logger, MeetUserModel); } diff --git a/meet-ce/backend/src/routes/analytics.routes.ts b/meet-ce/backend/src/routes/analytics.routes.ts index e77ebf00..11fb0b3d 100644 --- a/meet-ce/backend/src/routes/analytics.routes.ts +++ b/meet-ce/backend/src/routes/analytics.routes.ts @@ -1,4 +1,4 @@ -import { UserRole } from '@openvidu-meet/typings'; +import { MeetUserRole } from '@openvidu-meet/typings'; import bodyParser from 'body-parser'; import { Router } from 'express'; import * as analyticsCtrl from '../controllers/analytics.controller.js'; @@ -9,4 +9,4 @@ analyticsRouter.use(bodyParser.urlencoded({ extended: true })); analyticsRouter.use(bodyParser.json()); // Analytics Routes -analyticsRouter.get('/', withAuth(tokenAndRoleValidator(UserRole.ADMIN)), analyticsCtrl.getAnalytics); +analyticsRouter.get('/', withAuth(tokenAndRoleValidator(MeetUserRole.ADMIN)), analyticsCtrl.getAnalytics); diff --git a/meet-ce/backend/src/routes/api-key.routes.ts b/meet-ce/backend/src/routes/api-key.routes.ts index aa231ea5..d899df1e 100644 --- a/meet-ce/backend/src/routes/api-key.routes.ts +++ b/meet-ce/backend/src/routes/api-key.routes.ts @@ -1,4 +1,4 @@ -import { UserRole } from '@openvidu-meet/typings'; +import { MeetUserRole } from '@openvidu-meet/typings'; import bodyParser from 'body-parser'; import { Router } from 'express'; import * as apiKeyCtrl from '../controllers/api-key.controller.js'; @@ -9,6 +9,6 @@ apiKeyRouter.use(bodyParser.urlencoded({ extended: true })); apiKeyRouter.use(bodyParser.json()); // API Key Routes -apiKeyRouter.post('/', withAuth(tokenAndRoleValidator(UserRole.ADMIN)), apiKeyCtrl.createApiKey); -apiKeyRouter.get('/', withAuth(tokenAndRoleValidator(UserRole.ADMIN)), apiKeyCtrl.getApiKeys); -apiKeyRouter.delete('/', withAuth(tokenAndRoleValidator(UserRole.ADMIN)), apiKeyCtrl.deleteApiKeys); +apiKeyRouter.post('/', withAuth(tokenAndRoleValidator(MeetUserRole.ADMIN)), apiKeyCtrl.createApiKey); +apiKeyRouter.get('/', withAuth(tokenAndRoleValidator(MeetUserRole.ADMIN)), apiKeyCtrl.getApiKeys); +apiKeyRouter.delete('/', withAuth(tokenAndRoleValidator(MeetUserRole.ADMIN)), apiKeyCtrl.deleteApiKeys); diff --git a/meet-ce/backend/src/routes/global-config.routes.ts b/meet-ce/backend/src/routes/global-config.routes.ts index 5d116071..d3ead6d4 100644 --- a/meet-ce/backend/src/routes/global-config.routes.ts +++ b/meet-ce/backend/src/routes/global-config.routes.ts @@ -1,4 +1,4 @@ -import { UserRole } from '@openvidu-meet/typings'; +import { MeetUserRole } from '@openvidu-meet/typings'; import bodyParser from 'body-parser'; import { Router } from 'express'; import * as appearanceConfigCtrl from '../controllers/global-config/appearance-config.controller.js'; @@ -21,17 +21,17 @@ configRouter.use(bodyParser.json()); // Webhook config configRouter.put( '/webhooks', - withAuth(tokenAndRoleValidator(UserRole.ADMIN)), + withAuth(tokenAndRoleValidator(MeetUserRole.ADMIN)), validateWebhookConfig, webhookConfigCtrl.updateWebhookConfig ); -configRouter.get('/webhooks', withAuth(tokenAndRoleValidator(UserRole.ADMIN)), webhookConfigCtrl.getWebhookConfig); +configRouter.get('/webhooks', withAuth(tokenAndRoleValidator(MeetUserRole.ADMIN)), webhookConfigCtrl.getWebhookConfig); configRouter.post('/webhooks/test', withValidWebhookTestRequest, webhookConfigCtrl.testWebhook); // Security config configRouter.put( '/security', - withAuth(tokenAndRoleValidator(UserRole.ADMIN)), + withAuth(tokenAndRoleValidator(MeetUserRole.ADMIN)), validateSecurityConfig, securityConfigCtrl.updateSecurityConfig ); @@ -40,7 +40,7 @@ configRouter.get('/security', withAuth(allowAnonymous), securityConfigCtrl.getSe // Appearance config configRouter.put( '/rooms/appearance', - withAuth(tokenAndRoleValidator(UserRole.ADMIN)), + withAuth(tokenAndRoleValidator(MeetUserRole.ADMIN)), validateRoomsAppearanceConfig, appearanceConfigCtrl.updateRoomsAppearanceConfig ); diff --git a/meet-ce/backend/src/routes/index.ts b/meet-ce/backend/src/routes/index.ts index 8b2ed8d3..3f435d25 100644 --- a/meet-ce/backend/src/routes/index.ts +++ b/meet-ce/backend/src/routes/index.ts @@ -4,7 +4,6 @@ export * from './api-key.routes.js'; export * from './user.routes.js'; export * from './room.routes.js'; export * from './meeting.routes.js'; -export * from './participant.routes.js'; export * from './recording.routes.js'; export * from './livekit.routes.js'; export * from './analytics.routes.js'; diff --git a/meet-ce/backend/src/routes/meeting.routes.ts b/meet-ce/backend/src/routes/meeting.routes.ts index 16fe13c2..c7a591ed 100644 --- a/meet-ce/backend/src/routes/meeting.routes.ts +++ b/meet-ce/backend/src/routes/meeting.routes.ts @@ -1,9 +1,8 @@ import bodyParser from 'body-parser'; import { Router } from 'express'; import * as meetingCtrl from '../controllers/meeting.controller.js'; -import * as participantCtrl from '../controllers/participant.controller.js'; import { - participantTokenValidator, + roomMemberTokenValidator, validateUpdateParticipantRequest, withAuth, withModeratorPermissions, @@ -17,23 +16,23 @@ internalMeetingRouter.use(bodyParser.json()); // Internal Meetings Routes internalMeetingRouter.delete( '/:roomId', - withAuth(participantTokenValidator), + withAuth(roomMemberTokenValidator), withValidRoomId, withModeratorPermissions, meetingCtrl.endMeeting ); internalMeetingRouter.delete( '/:roomId/participants/:participantIdentity', - withAuth(participantTokenValidator), + withAuth(roomMemberTokenValidator), withValidRoomId, withModeratorPermissions, - participantCtrl.kickParticipant + meetingCtrl.kickParticipantFromMeeting ); internalMeetingRouter.put( '/:roomId/participants/:participantIdentity/role', - withAuth(participantTokenValidator), + withAuth(roomMemberTokenValidator), withValidRoomId, withModeratorPermissions, validateUpdateParticipantRequest, - participantCtrl.updateParticipantRole + meetingCtrl.updateParticipantRole ); diff --git a/meet-ce/backend/src/routes/participant.routes.ts b/meet-ce/backend/src/routes/participant.routes.ts deleted file mode 100644 index d4f659f1..00000000 --- a/meet-ce/backend/src/routes/participant.routes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import bodyParser from 'body-parser'; -import { Router } from 'express'; -import * as participantCtrl from '../controllers/participant.controller.js'; -import { configureParticipantTokenAuth, validateParticipantTokenRequest } from '../middlewares/index.js'; - -export const internalParticipantRouter: Router = Router(); -internalParticipantRouter.use(bodyParser.urlencoded({ extended: true })); -internalParticipantRouter.use(bodyParser.json()); - -// Internal Participant Routes -internalParticipantRouter.post( - '/token', - validateParticipantTokenRequest, - configureParticipantTokenAuth, - participantCtrl.generateParticipantToken -); -internalParticipantRouter.post( - '/token/refresh', - validateParticipantTokenRequest, - configureParticipantTokenAuth, - participantCtrl.refreshParticipantToken -); diff --git a/meet-ce/backend/src/routes/recording.routes.ts b/meet-ce/backend/src/routes/recording.routes.ts index 9202e5a8..ae9cb811 100644 --- a/meet-ce/backend/src/routes/recording.routes.ts +++ b/meet-ce/backend/src/routes/recording.routes.ts @@ -1,12 +1,11 @@ -import { UserRole } from '@openvidu-meet/typings'; +import { MeetUserRole } from '@openvidu-meet/typings'; import bodyParser from 'body-parser'; import { Router } from 'express'; import * as recordingCtrl from '../controllers/recording.controller.js'; import { apiKeyValidator, configureRecordingAuth, - participantTokenValidator, - recordingTokenValidator, + roomMemberTokenValidator, tokenAndRoleValidator, withAuth, withCanDeleteRecordingsPermission, @@ -29,21 +28,21 @@ recordingRouter.use(bodyParser.json()); // Recording Routes recordingRouter.get( '/', - withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), recordingTokenValidator), + withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN), roomMemberTokenValidator), withCanRetrieveRecordingsPermission, withValidRecordingFiltersRequest, recordingCtrl.getRecordings ); recordingRouter.delete( '/', - withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), recordingTokenValidator), + withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN), roomMemberTokenValidator), withValidMultipleRecordingIds, withCanDeleteRecordingsPermission, recordingCtrl.bulkDeleteRecordings ); recordingRouter.get( '/download', - withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), recordingTokenValidator), + withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN), roomMemberTokenValidator), withValidMultipleRecordingIds, withCanRetrieveRecordingsPermission, recordingCtrl.downloadRecordingsZip @@ -57,7 +56,7 @@ recordingRouter.get( ); recordingRouter.delete( '/:recordingId', - withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), recordingTokenValidator), + withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN), roomMemberTokenValidator), withValidRecordingId, withCanDeleteRecordingsPermission, recordingCtrl.deleteRecording @@ -71,7 +70,7 @@ recordingRouter.get( ); recordingRouter.get( '/:recordingId/url', - withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), recordingTokenValidator), + withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN), roomMemberTokenValidator), withValidGetRecordingUrlRequest, withCanRetrieveRecordingsPermission, recordingCtrl.getRecordingUrl @@ -86,7 +85,7 @@ internalRecordingRouter.post( '/', withValidStartRecordingRequest, withRecordingEnabled, - withAuth(participantTokenValidator), + withAuth(roomMemberTokenValidator), withCanRecordPermission, recordingCtrl.startRecording ); @@ -94,7 +93,7 @@ internalRecordingRouter.post( '/:recordingId/stop', withValidRecordingId, withRecordingEnabled, - withAuth(participantTokenValidator), + withAuth(roomMemberTokenValidator), withCanRecordPermission, recordingCtrl.stopRecording ); diff --git a/meet-ce/backend/src/routes/room.routes.ts b/meet-ce/backend/src/routes/room.routes.ts index cbb2d361..50d83986 100644 --- a/meet-ce/backend/src/routes/room.routes.ts +++ b/meet-ce/backend/src/routes/room.routes.ts @@ -1,13 +1,13 @@ -import { UserRole } from '@openvidu-meet/typings'; +import { MeetUserRole } from '@openvidu-meet/typings'; import bodyParser from 'body-parser'; import { Router } from 'express'; import * as roomCtrl from '../controllers/room.controller.js'; import { allowAnonymous, apiKeyValidator, - configureRecordingTokenAuth, configureRoomAuthorization, - participantTokenValidator, + configureRoomMemberTokenAuth, + roomMemberTokenValidator, tokenAndRoleValidator, withAuth, withValidRoomBulkDeleteRequest, @@ -15,8 +15,8 @@ import { withValidRoomDeleteRequest, withValidRoomFiltersRequest, withValidRoomId, + withValidRoomMemberTokenRequest, withValidRoomOptions, - withValidRoomSecret, withValidRoomStatus } from '../middlewares/index.js'; @@ -27,47 +27,47 @@ roomRouter.use(bodyParser.json()); // Room Routes roomRouter.post( '/', - withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), + withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN)), withValidRoomOptions, roomCtrl.createRoom ); roomRouter.get( '/', - withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), + withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN)), withValidRoomFiltersRequest, roomCtrl.getRooms ); roomRouter.delete( '/', - withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), + withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN)), withValidRoomBulkDeleteRequest, roomCtrl.bulkDeleteRooms ); roomRouter.get( '/:roomId', - withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), participantTokenValidator), + withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN), roomMemberTokenValidator), withValidRoomId, configureRoomAuthorization, roomCtrl.getRoom ); roomRouter.delete( '/:roomId', - withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), + withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN)), withValidRoomDeleteRequest, roomCtrl.deleteRoom ); roomRouter.get( '/:roomId/config', - withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), participantTokenValidator), + withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN), roomMemberTokenValidator), withValidRoomId, configureRoomAuthorization, roomCtrl.getRoomConfig ); roomRouter.put( '/:roomId/config', - withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), + withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN)), withValidRoomId, withValidRoomConfig, roomCtrl.updateRoomConfig @@ -75,7 +75,7 @@ roomRouter.put( roomRouter.put( '/:roomId/status', - withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), + withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN)), withValidRoomId, withValidRoomStatus, roomCtrl.updateRoomStatus @@ -87,21 +87,21 @@ internalRoomRouter.use(bodyParser.urlencoded({ extended: true })); internalRoomRouter.use(bodyParser.json()); internalRoomRouter.post( - '/:roomId/recording-token', - configureRecordingTokenAuth, + '/:roomId/token', withValidRoomId, - withValidRoomSecret, - roomCtrl.generateRecordingToken + withValidRoomMemberTokenRequest, + configureRoomMemberTokenAuth, + roomCtrl.generateRoomMemberToken ); internalRoomRouter.get( '/:roomId/roles', withAuth(allowAnonymous), withValidRoomId, - roomCtrl.getRoomRolesAndPermissions + roomCtrl.getRoomMemberRolesAndPermissions ); internalRoomRouter.get( '/:roomId/roles/:secret', withAuth(allowAnonymous), withValidRoomId, - roomCtrl.getRoomRoleAndPermissions + roomCtrl.getRoomMemberRoleAndPermissions ); diff --git a/meet-ce/backend/src/routes/user.routes.ts b/meet-ce/backend/src/routes/user.routes.ts index a3e86e3d..519f0a30 100644 --- a/meet-ce/backend/src/routes/user.routes.ts +++ b/meet-ce/backend/src/routes/user.routes.ts @@ -1,4 +1,4 @@ -import { UserRole } from '@openvidu-meet/typings'; +import { MeetUserRole } from '@openvidu-meet/typings'; import bodyParser from 'body-parser'; import { Router } from 'express'; import * as userCtrl from '../controllers/user.controller.js'; @@ -9,10 +9,10 @@ userRouter.use(bodyParser.urlencoded({ extended: true })); userRouter.use(bodyParser.json()); // Users Routes -userRouter.get('/profile', withAuth(tokenAndRoleValidator(UserRole.ADMIN, UserRole.USER)), userCtrl.getProfile); +userRouter.get('/profile', withAuth(tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER)), userCtrl.getProfile); userRouter.post( '/change-password', - withAuth(tokenAndRoleValidator(UserRole.ADMIN, UserRole.USER)), + withAuth(tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER)), validateChangePasswordRequest, userCtrl.changePassword ); diff --git a/meet-ce/backend/src/server.ts b/meet-ce/backend/src/server.ts index ec6b6865..a8cc5cfe 100644 --- a/meet-ce/backend/src/server.ts +++ b/meet-ce/backend/src/server.ts @@ -5,14 +5,13 @@ import express, { Express, Request, Response } from 'express'; import { initializeEagerServices, registerDependencies } from './config/index.js'; import { INTERNAL_CONFIG } from './config/internal-config.js'; import { MEET_EDITION, SERVER_CORS_ORIGIN, SERVER_PORT, logEnvVars } from './environment.js'; -import { httpContextMiddleware, jsonSyntaxErrorHandler } from './middlewares/index.js'; +import { initRequestContext, jsonSyntaxErrorHandler, setBaseUrlMiddleware } from './middlewares/index.js'; import { analyticsRouter, apiKeyRouter, authRouter, configRouter, internalMeetingRouter, - internalParticipantRouter, internalRecordingRouter, internalRoomRouter, livekitWebhookRouter, @@ -49,8 +48,13 @@ const createApp = () => { app.use(jsonSyntaxErrorHandler); app.use(cookieParser()); - // Middleware to set HTTP context - app.use(httpContextMiddleware); + // CRITICAL: Initialize request context FIRST + // This middleware creates an isolated AsyncLocalStorage context for each request + // Must be registered before any middleware that uses RequestSessionService + app.use(initRequestContext); + + // Middleware to set base URL for each request + app.use(setBaseUrlMiddleware); // Public API routes app.use(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/docs`, (_req: Request, res: Response) => @@ -72,7 +76,6 @@ 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}/participants`, internalParticipantRouter); app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings`, internalRecordingRouter); app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/config`, configRouter); app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/analytics`, analyticsRouter); diff --git a/meet-ce/backend/src/services/http-context.service.ts b/meet-ce/backend/src/services/base-url.service.ts similarity index 75% rename from meet-ce/backend/src/services/http-context.service.ts rename to meet-ce/backend/src/services/base-url.service.ts index 50b3aedd..e69ac012 100644 --- a/meet-ce/backend/src/services/http-context.service.ts +++ b/meet-ce/backend/src/services/base-url.service.ts @@ -3,7 +3,7 @@ import { injectable } from 'inversify'; import { SERVER_PORT } from '../environment.js'; @injectable() -export class HttpContextService { +export class BaseUrlService { private baseUrl: string; constructor() { @@ -11,9 +11,9 @@ export class HttpContextService { } /** - * Sets the current HTTP context from the request + * Sets the base URL from the request */ - setContext(req: Request): void { + setBaseUrlFromRequest(req: Request): void { const protocol = req.protocol; const host = req.get('host'); this.baseUrl = `${protocol}://${host}`; @@ -27,9 +27,9 @@ export class HttpContextService { } /** - * Clears the current context + * Clears the current base URL by resetting to default */ - clearContext(): void { + clearBaseUrl(): void { this.baseUrl = this.getDefaultBaseUrl(); } diff --git a/meet-ce/backend/src/services/frontend-event.service.ts b/meet-ce/backend/src/services/frontend-event.service.ts index 656c5e2d..5c0d7cf9 100644 --- a/meet-ce/backend/src/services/frontend-event.service.ts +++ b/meet-ce/backend/src/services/frontend-event.service.ts @@ -1,9 +1,9 @@ import { + MeetParticipantRoleUpdatedPayload, MeetRecordingInfo, MeetRoom, - ParticipantRole, - MeetParticipantRoleUpdatedPayload, MeetRoomConfigUpdatedPayload, + MeetRoomMemberRole, MeetSignalPayload, MeetSignalType } from '@openvidu-meet/typings'; @@ -97,7 +97,7 @@ export class FrontendEventService { async sendParticipantRoleUpdatedSignal( roomId: string, participantIdentity: string, - newRole: ParticipantRole, + newRole: MeetRoomMemberRole, secret: string ): Promise { this.logger.debug( diff --git a/meet-ce/backend/src/services/global-config.service.ts b/meet-ce/backend/src/services/global-config.service.ts index ad7fde7b..bc14e318 100644 --- a/meet-ce/backend/src/services/global-config.service.ts +++ b/meet-ce/backend/src/services/global-config.service.ts @@ -1,4 +1,4 @@ -import { AuthMode, AuthTransportMode, AuthType, GlobalConfig } from '@openvidu-meet/typings'; +import { AuthMode, AuthType, GlobalConfig } from '@openvidu-meet/typings'; import { inject, injectable } from 'inversify'; import { MEET_INITIAL_API_KEY, @@ -93,8 +93,7 @@ export class GlobalConfigService { authMethod: { type: AuthType.SINGLE_USER }, - authModeToAccessRoom: AuthMode.NONE, - authTransportMode: AuthTransportMode.HEADER + authModeToAccessRoom: AuthMode.NONE } }, roomsConfig: { diff --git a/meet-ce/backend/src/services/index.ts b/meet-ce/backend/src/services/index.ts index f834576c..2d571525 100644 --- a/meet-ce/backend/src/services/index.ts +++ b/meet-ce/backend/src/services/index.ts @@ -3,7 +3,8 @@ export * from './redis.service.js'; export * from './distributed-event.service.js'; export * from './mutex.service.js'; export * from './task-scheduler.service.js'; -export * from './http-context.service.js'; +export * from './base-url.service.js'; +export * from './request-session.service.js'; export * from './token.service.js'; export * from './user.service.js'; @@ -18,7 +19,7 @@ export * from './frontend-event.service.js'; export * from './recording.service.js'; export * from './room.service.js'; export * from './participant-name.service.js'; -export * from './participant.service.js'; +export * from './room-member.service.js'; export * from './openvidu-webhook.service.js'; export * from './livekit-webhook.service.js'; export * from './analytics.service.js'; diff --git a/meet-ce/backend/src/services/livekit-webhook.service.ts b/meet-ce/backend/src/services/livekit-webhook.service.ts index 20e300cd..3e46b2f3 100644 --- a/meet-ce/backend/src/services/livekit-webhook.service.ts +++ b/meet-ce/backend/src/services/livekit-webhook.service.ts @@ -13,8 +13,8 @@ import { LoggerService, MutexService, OpenViduWebhookService, - ParticipantService, RecordingService, + RoomMemberService, RoomService } from './index.js'; @@ -31,7 +31,7 @@ export class LivekitWebhookService { @inject(MutexService) protected mutexService: MutexService, @inject(DistributedEventService) protected distributedEventService: DistributedEventService, @inject(FrontendEventService) protected frontendEventService: FrontendEventService, - @inject(ParticipantService) protected participantService: ParticipantService, + @inject(RoomMemberService) protected roomMemberService: RoomMemberService, @inject(LoggerService) protected logger: LoggerService ) { this.webhookReceiver = new WebhookReceiver(LIVEKIT_API_KEY, LIVEKIT_API_SECRET); @@ -189,7 +189,7 @@ export class LivekitWebhookService { try { // Release the participant's reserved name - await this.participantService.releaseParticipantName(room.name, participant.name); + await this.roomMemberService.releaseParticipantName(room.name, participant.name); this.logger.verbose(`Released name for participant '${participant.name}' in room '${room.name}'`); } catch (error) { this.logger.error('Error releasing participant name on participant left:', error); @@ -280,7 +280,7 @@ export class LivekitWebhookService { this.openViduWebhookService.sendMeetingEndedWebhook(meetRoom); tasks.push( - this.participantService.cleanupParticipantNames(roomId), + this.roomMemberService.cleanupParticipantNames(roomId), this.recordingService.releaseRecordingLockIfNoEgress(roomId) ); await Promise.all(tasks); diff --git a/meet-ce/backend/src/services/livekit.service.ts b/meet-ce/backend/src/services/livekit.service.ts index e4d9338d..c378bece 100644 --- a/meet-ce/backend/src/services/livekit.service.ts +++ b/meet-ce/backend/src/services/livekit.service.ts @@ -182,22 +182,11 @@ export class LiveKitService { } } - async participantExists( - roomName: string, - participantNameOrIdentity: string, - participantField: 'name' | 'identity' = 'identity' - ): Promise { + async participantExists(roomName: string, participantIdentity: string): Promise { try { const participants: ParticipantInfo[] = await this.listRoomParticipants(roomName); return participants.some((participant) => { - let fieldValue = participant[participantField]; - - // If the field is empty or undefined, use identity as a fallback - if (!fieldValue && participantField === 'name') { - fieldValue = participant.identity; - } - - return fieldValue === participantNameOrIdentity; + return participant.identity === participantIdentity; }); } catch (error: any) { this.logger.error(error); diff --git a/meet-ce/backend/src/services/migration.service.ts b/meet-ce/backend/src/services/migration.service.ts index 5621bfdd..8c5e987a 100644 --- a/meet-ce/backend/src/services/migration.service.ts +++ b/meet-ce/backend/src/services/migration.service.ts @@ -1,4 +1,3 @@ -import { AuthTransportMode, GlobalConfig } from '@openvidu-meet/typings'; import { inject, injectable } from 'inversify'; import ms from 'ms'; import { MeetLock } from '../helpers/index.js'; @@ -135,11 +134,8 @@ export class MigrationService { return; } - // Add missing fields for backwards compatibility - const updatedConfig = this.addMissingFieldToGlobalConfig(legacyConfig); - // Save to MongoDB - await this.configRepository.create(updatedConfig); + await this.configRepository.create(legacyConfig); this.logger.info('Global config migrated successfully'); // Delete from legacy storage @@ -392,21 +388,4 @@ export class MigrationService { throw error; } } - - /** - * Adds authTransportMode field to existing global config if missing. - */ - protected addMissingFieldToGlobalConfig(config: GlobalConfig): GlobalConfig { - // Check if authTransportMode is missing - const authConfig = config.securityConfig.authentication; - - if (!('authTransportMode' in authConfig)) { - // Directly add the missing field to the existing object - Object.assign(config.securityConfig.authentication, { - authTransportMode: AuthTransportMode.HEADER - }); - } - - return config; - } } diff --git a/meet-ce/backend/src/services/participant.service.ts b/meet-ce/backend/src/services/participant.service.ts deleted file mode 100644 index 694f3e91..00000000 --- a/meet-ce/backend/src/services/participant.service.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { - MeetRoomStatus, - MeetTokenMetadata, - OpenViduMeetPermissions, - ParticipantOptions, - ParticipantPermissions, - ParticipantRole -} from '@openvidu-meet/typings'; -import { inject, injectable } from 'inversify'; -import { ParticipantInfo } from 'livekit-server-sdk'; -import { MeetRoomHelper } from '../helpers/room.helper.js'; -import { validateMeetTokenMetadata } from '../middlewares/index.js'; -import { - errorParticipantIdentityNotProvided, - errorParticipantNotFound, - errorRoomClosed -} from '../models/error.model.js'; -import { - FrontendEventService, - LiveKitService, - LoggerService, - ParticipantNameService, - RoomService, - TokenService -} from './index.js'; - -@injectable() -export class ParticipantService { - constructor( - @inject(LoggerService) protected logger: LoggerService, - @inject(RoomService) protected roomService: RoomService, - @inject(LiveKitService) protected livekitService: LiveKitService, - @inject(FrontendEventService) protected frontendEventService: FrontendEventService, - @inject(TokenService) protected tokenService: TokenService, - @inject(ParticipantNameService) protected participantNameService: ParticipantNameService - ) {} - - async generateOrRefreshParticipantToken( - participantOptions: ParticipantOptions, - currentRoles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[], - refresh = false - ): Promise { - const { roomId, secret, participantName, participantIdentity } = participantOptions; - let finalParticipantName = participantName; - let finalParticipantOptions: ParticipantOptions = participantOptions; - - if (participantName) { - // Check that room is open - const room = await this.roomService.getMeetRoom(roomId); - - if (room.status === MeetRoomStatus.CLOSED) { - throw errorRoomClosed(roomId); - } - - // Create the Livekit room if it doesn't exist - await this.roomService.createLivekitRoom(roomId); - - if (refresh) { - if (!participantIdentity) { - throw errorParticipantIdentityNotProvided(); - } - - this.logger.verbose(`Refreshing participant token for '${participantIdentity}' in room '${roomId}'`); - // Check if participant with same participantIdentity exists in the room - const participantExists = await this.participantExists(roomId, participantIdentity, 'identity'); - - if (!participantExists) { - this.logger.verbose(`Participant '${participantIdentity}' does not exist in room '${roomId}'`); - throw errorParticipantNotFound(participantIdentity, roomId); - } - } else { - this.logger.verbose(`Generating participant token for '${participantName}' in room '${roomId}'`); - - try { - // Reserve a unique name for the participant - finalParticipantName = await this.participantNameService.reserveUniqueName(roomId, participantName); - this.logger.verbose(`Reserved unique name '${finalParticipantName}' for room '${roomId}'`); - } catch (error) { - this.logger.error( - `Failed to reserve unique name '${participantName}' for room '${roomId}':`, - error - ); - throw error; - } - - // Update participantOptions with the final participant name - finalParticipantOptions = { - ...participantOptions, - participantName: finalParticipantName - }; - } - } - - const role = await this.roomService.getRoomRoleBySecret(roomId, secret); - const token = await this.generateParticipantToken(finalParticipantOptions, role, currentRoles); - this.logger.verbose( - `Participant token generated for room '${roomId}'` + - (finalParticipantName ? ` with name '${finalParticipantName}'` : '') - ); - return token; - } - - protected async generateParticipantToken( - participantOptions: ParticipantOptions, - role: ParticipantRole, - currentRoles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[] - ): Promise { - const { roomId, participantName } = participantOptions; - const permissions = this.getParticipantPermissions(roomId, role, !!participantName); - - if (!currentRoles.some((r) => r.role === role)) { - currentRoles.push({ role, permissions: permissions.openvidu }); - } - - return this.tokenService.generateParticipantToken(participantOptions, permissions.livekit, currentRoles, role); - } - - async getParticipant(roomId: string, participantIdentity: string): Promise { - this.logger.verbose(`Fetching participant '${participantIdentity}'`); - return this.livekitService.getParticipant(roomId, participantIdentity); - } - - async participantExists( - roomId: string, - participantNameOrIdentity: string, - participantField: 'name' | 'identity' = 'identity' - ): Promise { - this.logger.verbose(`Checking if participant '${participantNameOrIdentity}' exists in room '${roomId}'`); - return this.livekitService.participantExists(roomId, participantNameOrIdentity, participantField); - } - - async kickParticipant(roomId: string, participantIdentity: string): Promise { - this.logger.verbose(`Kicking participant '${participantIdentity}' from room '${roomId}'`); - return this.livekitService.deleteParticipant(roomId, participantIdentity); - } - - getParticipantPermissions(roomId: string, role: ParticipantRole, addJoinPermission = true): ParticipantPermissions { - switch (role) { - case ParticipantRole.MODERATOR: - return this.generateModeratorPermissions(roomId, addJoinPermission); - case ParticipantRole.SPEAKER: - return this.generateSpeakerPermissions(roomId, addJoinPermission); - default: - throw new Error(`Role ${role} not supported`); - } - } - - async updateParticipantRole(roomId: string, participantIdentity: string, newRole: ParticipantRole): Promise { - try { - const meetRoom = await this.roomService.getMeetRoom(roomId); - - const participant = await this.getParticipant(roomId, participantIdentity); - const metadata: MeetTokenMetadata = this.parseMetadata(participant.metadata); - - // Update selected role and roles array - metadata.selectedRole = newRole; - const currentRoles = metadata.roles; - - if (!currentRoles.some((r) => r.role === newRole)) { - const { openvidu } = this.getParticipantPermissions(roomId, newRole); - currentRoles.push({ role: newRole, permissions: openvidu }); - } - - await this.livekitService.updateParticipantMetadata(roomId, participantIdentity, JSON.stringify(metadata)); - - const { speakerSecret, moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(meetRoom); - const secret = newRole === ParticipantRole.MODERATOR ? moderatorSecret : speakerSecret; - await this.frontendEventService.sendParticipantRoleUpdatedSignal( - roomId, - participantIdentity, - newRole, - secret - ); - } catch (error) { - this.logger.error('Error updating participant role:', error); - throw error; - } - } - - parseMetadata(metadata: string): MeetTokenMetadata { - try { - const parsedMetadata = JSON.parse(metadata); - return validateMeetTokenMetadata(parsedMetadata); - } catch (error) { - this.logger.error('Failed to parse participant metadata:', error); - throw new Error('Invalid participant metadata format'); - } - } - - protected generateModeratorPermissions(roomId: string, addJoinPermission = true): ParticipantPermissions { - return { - livekit: { - roomJoin: addJoinPermission, - room: roomId, - canPublish: true, - canSubscribe: true, - canPublishData: true, - canUpdateOwnMetadata: true - }, - openvidu: { - canRecord: true, - canChat: true, - canChangeVirtualBackground: true - } - }; - } - - protected generateSpeakerPermissions(roomId: string, addJoinPermission = true): ParticipantPermissions { - return { - livekit: { - roomJoin: addJoinPermission, - room: roomId, - canPublish: true, - canSubscribe: true, - canPublishData: true, - canUpdateOwnMetadata: true - }, - openvidu: { - canRecord: false, - canChat: true, - canChangeVirtualBackground: true - } - }; - } - - /** - * Releases a participant's reserved name when they disconnect. - * This should be called when a participant leaves the room to free up the name. - * - * @param roomId - The room identifier - * @param participantName - The participant name to release - */ - async releaseParticipantName(roomId: string, participantName: string): Promise { - try { - await this.participantNameService.releaseName(roomId, participantName); - this.logger.verbose(`Released participant name '${participantName}' for room '${roomId}'`); - } catch (error) { - this.logger.warn(`Error releasing participant name '${participantName}' for room '${roomId}':`, error); - } - } - - /** - * Gets all currently reserved participant names in a room. - * Useful for debugging and monitoring. - * - * @param roomId - The room identifier - * @returns Promise - Array of reserved participant names - */ - async getReservedNames(roomId: string): Promise { - return await this.participantNameService.getReservedNames(roomId); - } - - /** - * Cleans up expired participant name reservations for a room. - * This can be called during room cleanup or periodically. - * - * @param roomId - The room identifier - */ - async cleanupParticipantNames(roomId: string): Promise { - await this.participantNameService.cleanupExpiredReservations(roomId); - } -} diff --git a/meet-ce/backend/src/services/recording.service.ts b/meet-ce/backend/src/services/recording.service.ts index 83b0ce93..329dcc65 100644 --- a/meet-ce/backend/src/services/recording.service.ts +++ b/meet-ce/backend/src/services/recording.service.ts @@ -567,12 +567,6 @@ export class RecordingService { if (!room) throw errorRoomNotFound(roomId); - //TODO: Check if the room has participants before starting the recording - //room.numParticipants === 0 ? throw errorNoParticipants(roomId); - const lkRoom = await this.livekitService.getRoom(roomId); - - if (!lkRoom) throw errorRoomNotFound(roomId); - const hasParticipants = await this.livekitService.roomHasParticipants(roomId); if (!hasParticipants) throw errorRoomHasNoParticipants(roomId); diff --git a/meet-ce/backend/src/services/request-session.service.ts b/meet-ce/backend/src/services/request-session.service.ts new file mode 100644 index 00000000..00b354d0 --- /dev/null +++ b/meet-ce/backend/src/services/request-session.service.ts @@ -0,0 +1,137 @@ +import { AsyncLocalStorage } from 'async_hooks'; +import { + LiveKitPermissions, + MeetPermissions, + MeetRoomMemberRole, + MeetRoomMemberRoleAndPermissions, + MeetUser +} from '@openvidu-meet/typings'; +import { injectable } from 'inversify'; + +/** + * Context stored per HTTP request using AsyncLocalStorage. + * This ensures that each concurrent request has its own isolated data. + */ +interface RequestContext { + user?: MeetUser; + roomMember?: MeetRoomMemberRoleAndPermissions; +} + +/** + * Service that manages request-scoped session data using Node.js AsyncLocalStorage. + * + * This service provides isolated storage for each HTTP request without needing to pass + * the request object around or use Inversify's request scope. It works by leveraging + * Node.js's async_hooks module which tracks asynchronous execution contexts. + * + * IMPORTANT: This service is designed to work with HTTP requests, but it's also safe + * to use in other contexts (schedulers, webhooks, background jobs). When used outside + * an HTTP request context, all getters return undefined and setters are ignored. + */ +@injectable() +export class RequestSessionService { + private asyncLocalStorage = new AsyncLocalStorage(); + private hasLoggedWarning = false; + + /** + * Initializes the request context. Must be called at the start of each HTTP request. + * This method creates an isolated storage context for the duration of the request. + * + * @param callback - The function to execute within the request context + * @returns The result of the callback + */ + run(callback: () => T): T { + return this.asyncLocalStorage.run({}, callback); + } + + /** + * Gets the current request context. + * Returns undefined if called outside of a request context (e.g., in schedulers, background jobs). + * Logs a warning the first time this happens to help with debugging. + */ + private getContext(): RequestContext | undefined { + const context = this.asyncLocalStorage.getStore(); + + if (!context && !this.hasLoggedWarning) { + console.warn( + 'RequestSessionService: No context found. ' + + 'This service is being used outside of an HTTP request context (e.g., scheduler, webhook, background job). ' + + 'All getters will return undefined and setters will be ignored. ' + + 'This is expected behavior for non-HTTP contexts.' + ); + this.hasLoggedWarning = true; + } + + return context; + } + + /** + * Sets the authenticated user in the current request context. + * If called outside a request context, this operation is silently ignored. + */ + setUser(user: MeetUser): void { + const context = this.getContext(); + + if (context) { + context.user = user; + } + } + + /** + * Gets the authenticated user from the current request context. + */ + getUser(): MeetUser | undefined { + return this.getContext()?.user; + } + + /** + * Sets the room member token information (role, permissions, and token claims) + * in the current request context. + * If called outside a request context, this operation is silently ignored. + */ + setRoomMemberTokenInfo( + role: MeetRoomMemberRole, + meetPermissions: MeetPermissions, + livekitPermissions: LiveKitPermissions + ): void { + const context = this.getContext(); + + if (context) { + context.roomMember = { + role, + permissions: { + meet: meetPermissions, + livekit: livekitPermissions + } + }; + } + } + + /** + * Gets the room member role from the current request context. + */ + getRoomMemberRole(): MeetRoomMemberRole | undefined { + return this.getContext()?.roomMember?.role; + } + + /** + * Gets the room member Meet permissions from the current request context. + */ + getRoomMemberMeetPermissions(): MeetPermissions | undefined { + return this.getContext()?.roomMember?.permissions.meet; + } + + /** + * Gets the room member LiveKit permissions from the current request context. + */ + getRoomMemberLivekitPermissions(): LiveKitPermissions | undefined { + return this.getContext()?.roomMember?.permissions.livekit; + } + + /** + * Gets the room ID from the token claims in the current request context. + */ + getRoomIdFromToken(): string | undefined { + return this.getContext()?.roomMember?.permissions.livekit.room; + } +} diff --git a/meet-ce/backend/src/services/room-member.service.ts b/meet-ce/backend/src/services/room-member.service.ts new file mode 100644 index 00000000..1d716311 --- /dev/null +++ b/meet-ce/backend/src/services/room-member.service.ts @@ -0,0 +1,359 @@ +import { + MeetRecordingAccess, + MeetRoomMemberPermissions, + MeetRoomMemberRole, + MeetRoomMemberTokenMetadata, + MeetRoomMemberTokenOptions, + MeetRoomStatus +} from '@openvidu-meet/typings'; +import { inject, injectable } from 'inversify'; +import { ParticipantInfo } from 'livekit-server-sdk'; +import { MeetRoomHelper } from '../helpers/room.helper.js'; +import { validateRoomMemberTokenMetadata } from '../middlewares/index.js'; +import { errorInvalidRoomSecret, errorParticipantNotFound, errorRoomClosed } from '../models/error.model.js'; +import { + FrontendEventService, + LiveKitService, + LoggerService, + ParticipantNameService, + RoomService, + TokenService +} from './index.js'; + +/** + * Service for managing room members and meeting participants. + */ +@injectable() +export class RoomMemberService { + constructor( + @inject(LoggerService) protected logger: LoggerService, + @inject(RoomService) protected roomService: RoomService, + @inject(ParticipantNameService) protected participantNameService: ParticipantNameService, + @inject(FrontendEventService) protected frontendEventService: FrontendEventService, + @inject(LiveKitService) protected livekitService: LiveKitService, + @inject(TokenService) protected tokenService: TokenService + ) {} + + /** + * Validates a secret against a room's moderator and speaker secrets and returns the corresponding role. + * + * @param roomId - The unique identifier of the room to check + * @param secret - The secret to validate against the room's moderator and speaker secrets + * @returns A promise that resolves to the room member role (MODERATOR or SPEAKER) if the secret is valid + * @throws Error if the moderator or speaker secrets cannot be extracted from their URLs + * @throws Error if the provided secret doesn't match any of the room's secrets (unauthorized) + */ + async getRoomMemberRoleBySecret(roomId: string, secret: string): Promise { + const room = await this.roomService.getMeetRoom(roomId); + const { moderatorSecret, speakerSecret } = MeetRoomHelper.extractSecretsFromRoom(room); + + switch (secret) { + case moderatorSecret: + return MeetRoomMemberRole.MODERATOR; + case speakerSecret: + return MeetRoomMemberRole.SPEAKER; + default: + throw errorInvalidRoomSecret(room.roomId, secret); + } + } + + /** + * Generates or refreshes a room member token. + * + * @param roomId - The room identifier + * @param tokenOptions - Options for token generation + * @returns A promise that resolves to the generated token + */ + async generateOrRefreshRoomMemberToken(roomId: string, tokenOptions: MeetRoomMemberTokenOptions): Promise { + const { secret, grantJoinMeetingPermission = false, participantName, participantIdentity } = tokenOptions; + + // Get room member role from secret + const role = await this.getRoomMemberRoleBySecret(roomId, secret); + + if (grantJoinMeetingPermission && participantName) { + return this.generateTokenWithJoinMeetingPermission(roomId, role, participantName, participantIdentity); + } else { + return this.generateTokenWithoutJoinMeetingPermission(roomId, role); + } + } + + /** + * Generates a token with join meeting permissions. + * Handles both new token generation and token refresh. + */ + protected async generateTokenWithJoinMeetingPermission( + roomId: string, + role: MeetRoomMemberRole, + participantName: string, + participantIdentity?: string + ): Promise { + // Check that room is open + const room = await this.roomService.getMeetRoom(roomId); + + if (room.status === MeetRoomStatus.CLOSED) { + throw errorRoomClosed(roomId); + } + + const isRefresh = !!participantIdentity; + + if (!isRefresh) { + // GENERATION MODE + this.logger.verbose( + `Generating room member token with join meeting permission for '${participantName}' in room '${roomId}'` + ); + + // Create the Livekit room if it doesn't exist + await this.roomService.createLivekitRoom(roomId); + + try { + // Reserve a unique name for the participant + participantName = await this.participantNameService.reserveUniqueName(roomId, participantName); + this.logger.verbose(`Reserved unique name '${participantName}' for room '${roomId}'`); + } catch (error) { + this.logger.error(`Failed to reserve unique name '${participantName}' for room '${roomId}':`, error); + throw error; + } + } else { + // REFRESH MODE + this.logger.verbose( + `Refreshing room member token for participant '${participantIdentity}' in room '${roomId}'` + ); + + // Check if participant exists in the room + const participantExists = await this.existsParticipantInMeeting(roomId, participantIdentity); + + if (!participantExists) { + this.logger.verbose(`Participant '${participantIdentity}' does not exist in room '${roomId}'`); + throw errorParticipantNotFound(participantIdentity, roomId); + } + } + + // Get participant permissions (with join meeting) + const permissions = await this.getRoomMemberPermissions(roomId, role, true); + + // Generate token with participant name + return this.tokenService.generateRoomMemberToken(role, permissions, participantName); + } + + /** + * Generates a token without join meeting permission. + * This token only provides access to other room resources (recordings, etc.) + */ + protected async generateTokenWithoutJoinMeetingPermission( + roomId: string, + role: MeetRoomMemberRole + ): Promise { + this.logger.verbose(`Generating room member token without join meeting permission for room '${roomId}'`); + + // Get participant permissions (without join meeting) + const permissions = await this.getRoomMemberPermissions(roomId, role, false); + + // Generate token without participant name + return this.tokenService.generateRoomMemberToken(role, permissions); + } + + /** + * Gets the permissions for a room member based on their role. + * + * @param roomId - The ID of the room + * @param role - The role of the room member + * @param addJoinPermission - Whether to include join permission (for meeting access) + * @returns The permissions for the room member + */ + async getRoomMemberPermissions( + roomId: string, + role: MeetRoomMemberRole, + addJoinPermission = true + ): Promise { + const recordingPermissions = await this.getRecordingPermissions(roomId, role); + + switch (role) { + case MeetRoomMemberRole.MODERATOR: + return this.generateModeratorPermissions( + roomId, + recordingPermissions.canRetrieveRecordings, + recordingPermissions.canDeleteRecordings, + addJoinPermission + ); + case MeetRoomMemberRole.SPEAKER: + return this.generateSpeakerPermissions( + roomId, + recordingPermissions.canRetrieveRecordings, + recordingPermissions.canDeleteRecordings, + addJoinPermission + ); + } + } + + protected generateModeratorPermissions( + roomId: string, + canRetrieveRecordings: boolean, + canDeleteRecordings: boolean, + addJoinPermission: boolean + ): MeetRoomMemberPermissions { + return { + livekit: { + roomJoin: addJoinPermission, + room: roomId, + canPublish: true, + canSubscribe: true, + canPublishData: true, + canUpdateOwnMetadata: true + }, + meet: { + canRecord: true, + canRetrieveRecordings, + canDeleteRecordings, + canChat: true, + canChangeVirtualBackground: true + } + }; + } + + protected generateSpeakerPermissions( + roomId: string, + canRetrieveRecordings: boolean, + canDeleteRecordings: boolean, + addJoinPermission: boolean + ): MeetRoomMemberPermissions { + return { + livekit: { + roomJoin: addJoinPermission, + room: roomId, + canPublish: true, + canSubscribe: true, + canPublishData: true, + canUpdateOwnMetadata: true + }, + meet: { + canRecord: false, + canRetrieveRecordings, + canDeleteRecordings, + canChat: true, + canChangeVirtualBackground: true + } + }; + } + + protected async getRecordingPermissions( + roomId: string, + role: MeetRoomMemberRole + ): Promise<{ + canRetrieveRecordings: boolean; + canDeleteRecordings: boolean; + }> { + const room = await this.roomService.getMeetRoom(roomId); + const recordingAccess = room.config.recording.allowAccessTo; + + if (!recordingAccess) { + // Default to no access if not configured + return { + canRetrieveRecordings: false, + canDeleteRecordings: false + }; + } + + // A room member can delete recordings if they are a moderator and the recording access is not set to admin + const canDeleteRecordings = + role === MeetRoomMemberRole.MODERATOR && recordingAccess !== MeetRecordingAccess.ADMIN; + + /* A room member can retrieve recordings if + - they can delete recordings + - they are a speaker and the recording access includes speakers + */ + const canRetrieveRecordings = + canDeleteRecordings || + (role === MeetRoomMemberRole.SPEAKER && recordingAccess === MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER); + + return { + canRetrieveRecordings, + canDeleteRecordings + }; + } + + /** + * Parses and validates room member token metadata. + */ + parseRoomMemberTokenMetadata(metadata: string): MeetRoomMemberTokenMetadata { + try { + const parsedMetadata = JSON.parse(metadata); + return validateRoomMemberTokenMetadata(parsedMetadata); + } catch (error) { + this.logger.error('Failed to parse room member token metadata:', error); + throw new Error('Invalid room member token metadata format'); + } + } + + async kickParticipantFromMeeting(roomId: string, participantIdentity: string): Promise { + this.logger.verbose(`Kicking participant '${participantIdentity}' from room '${roomId}'`); + return this.livekitService.deleteParticipant(roomId, participantIdentity); + } + + async updateParticipantRole( + roomId: string, + participantIdentity: string, + newRole: MeetRoomMemberRole + ): Promise { + try { + const meetRoom = await this.roomService.getMeetRoom(roomId); + + const participant = await this.getParticipantFromMeeting(roomId, participantIdentity); + const metadata: MeetRoomMemberTokenMetadata = this.parseRoomMemberTokenMetadata(participant.metadata); + + // Update role and permissions in metadata + metadata.role = newRole; + const { meet } = await this.getRoomMemberPermissions(roomId, newRole); + metadata.permissions = meet; + + await this.livekitService.updateParticipantMetadata(roomId, participantIdentity, JSON.stringify(metadata)); + + const { speakerSecret, moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(meetRoom); + const secret = newRole === MeetRoomMemberRole.MODERATOR ? moderatorSecret : speakerSecret; + await this.frontendEventService.sendParticipantRoleUpdatedSignal( + roomId, + participantIdentity, + newRole, + secret + ); + } catch (error) { + this.logger.error('Error updating participant role:', error); + throw error; + } + } + + protected async existsParticipantInMeeting(roomId: string, participantIdentity: string): Promise { + this.logger.verbose(`Checking if participant '${participantIdentity}' exists in room '${roomId}'`); + return this.livekitService.participantExists(roomId, participantIdentity); + } + + protected async getParticipantFromMeeting(roomId: string, participantIdentity: string): Promise { + this.logger.verbose(`Fetching participant '${participantIdentity}'`); + return this.livekitService.getParticipant(roomId, participantIdentity); + } + + /** + * Releases a participant's reserved name when they disconnect from meeting. + * This should be called when a participant leaves the meeting to free up the name. + * + * @param roomId - The room identifier + * @param participantName - The participant name to release + */ + async releaseParticipantName(roomId: string, participantName: string): Promise { + try { + await this.participantNameService.releaseName(roomId, participantName); + this.logger.verbose(`Released participant name '${participantName}' for room '${roomId}'`); + } catch (error) { + this.logger.warn(`Error releasing participant name '${participantName}' for room '${roomId}':`, error); + } + } + + /** + * Cleans up expired participant name reservations for a meeting. + * This can be called during room cleanup or periodically. + * + * @param roomId - The room identifier + */ + async cleanupParticipantNames(roomId: string): Promise { + await this.participantNameService.cleanupExpiredReservations(roomId); + } +} diff --git a/meet-ce/backend/src/services/room.service.ts b/meet-ce/backend/src/services/room.service.ts index 28fcbf12..39641471 100644 --- a/meet-ce/backend/src/services/room.service.ts +++ b/meet-ce/backend/src/services/room.service.ts @@ -1,6 +1,5 @@ import { MeetingEndAction, - MeetRecordingAccess, MeetRoom, MeetRoomConfig, MeetRoomDeletionErrorCode, @@ -8,10 +7,9 @@ import { MeetRoomDeletionPolicyWithRecordings, MeetRoomDeletionSuccessCode, MeetRoomFilters, + MeetRoomMemberRole, MeetRoomOptions, - MeetRoomStatus, - ParticipantRole, - RecordingPermissions + MeetRoomStatus } from '@openvidu-meet/typings'; import { inject, injectable } from 'inversify'; import { CreateOptions, Room } from 'livekit-server-sdk'; @@ -21,10 +19,8 @@ import { uid } from 'uid/single'; import { INTERNAL_CONFIG } from '../config/internal-config.js'; import { MEET_NAME_ID } from '../environment.js'; import { MeetRoomHelper, UtilsHelper } from '../helpers/index.js'; -import { validateRecordingTokenMetadata } from '../middlewares/index.js'; import { errorDeletingRoom, - errorInvalidRoomSecret, errorRoomActiveMeeting, errorRoomNotFound, internalError, @@ -32,14 +28,13 @@ import { } from '../models/error.model.js'; import { RoomRepository } from '../repositories/index.js'; import { - DistributedEventService, FrontendEventService, IScheduledTask, LiveKitService, LoggerService, RecordingService, - TaskSchedulerService, - TokenService + RequestSessionService, + TaskSchedulerService } from './index.js'; /** @@ -55,10 +50,9 @@ export class RoomService { @inject(RoomRepository) protected roomRepository: RoomRepository, @inject(RecordingService) protected recordingService: RecordingService, @inject(LiveKitService) protected livekitService: LiveKitService, - @inject(DistributedEventService) protected distributedEventService: DistributedEventService, @inject(FrontendEventService) protected frontendEventService: FrontendEventService, @inject(TaskSchedulerService) protected taskSchedulerService: TaskSchedulerService, - @inject(TokenService) protected tokenService: TokenService + @inject(RequestSessionService) protected requestSessionService: RequestSessionService ) { const roomGarbageCollectorTask: IScheduledTask = { name: 'roomGarbageCollector', @@ -223,7 +217,7 @@ export class RoomService { * @param roomId - The name of the room to retrieve. * @returns A promise that resolves to an {@link MeetRoom} object. */ - async getMeetRoom(roomId: string, fields?: string, participantRole?: ParticipantRole): Promise { + async getMeetRoom(roomId: string, fields?: string): Promise { const meetRoom = await this.roomRepository.findByRoomId(roomId); if (!meetRoom) { @@ -233,8 +227,10 @@ export class RoomService { const filteredRoom = UtilsHelper.filterObjectFields(meetRoom, fields); - // Remove moderatorUrl if the participant is a speaker to prevent access to moderator links - if (participantRole === ParticipantRole.SPEAKER) { + // Remove moderatorUrl if the room member is a speaker to prevent access to moderator links + const role = this.requestSessionService.getRoomMemberRole(); + + if (role === MeetRoomMemberRole.SPEAKER) { delete filteredRoom.moderatorUrl; } @@ -634,74 +630,6 @@ export class RoomService { return { successful, failed }; } - /** - * Validates a secret against a room's moderator and speaker secrets and returns the corresponding role. - * - * @param roomId - The unique identifier of the room to check - * @param secret - The secret to validate against the room's moderator and speaker secrets - * @returns A promise that resolves to the participant role (MODERATOR or SPEAKER) if the secret is valid - * @throws Error if the moderator or speaker secrets cannot be extracted from their URLs - * @throws Error if the provided secret doesn't match any of the room's secrets (unauthorized) - */ - async getRoomRoleBySecret(roomId: string, secret: string): Promise { - const room = await this.getMeetRoom(roomId); - const { moderatorSecret, speakerSecret } = MeetRoomHelper.extractSecretsFromRoom(room); - - switch (secret) { - case moderatorSecret: - return ParticipantRole.MODERATOR; - case speakerSecret: - return ParticipantRole.SPEAKER; - default: - throw errorInvalidRoomSecret(room.roomId, secret); - } - } - - /** - * Generates a token with recording permissions for a specific room. - * - * @param roomId - The unique identifier of the room for which the recording token is being generated. - * @param secret - The secret associated with the room, used to determine the user's role. - * @returns A promise that resolves to the generated recording token as a string. - * @throws An error if the room with the given `roomId` is not found. - */ - async generateRecordingToken(roomId: string, secret: string): Promise { - const role = await this.getRoomRoleBySecret(roomId, secret); - const permissions = await this.getRecordingPermissions(roomId, role); - return await this.tokenService.generateRecordingToken(roomId, role, permissions); - } - - protected async getRecordingPermissions(roomId: string, role: ParticipantRole): Promise { - const room = await this.getMeetRoom(roomId); - const recordingAccess = room.config?.recording.allowAccessTo; - - // A participant can delete recordings if they are a moderator and the recording access is not set to admin - const canDeleteRecordings = role === ParticipantRole.MODERATOR && recordingAccess !== MeetRecordingAccess.ADMIN; - - /* A participant can retrieve recordings if - - they can delete recordings - - they are a speaker and the recording access includes speakers - */ - const canRetrieveRecordings = - canDeleteRecordings || - (role === ParticipantRole.SPEAKER && recordingAccess === MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER); - - return { - canRetrieveRecordings, - canDeleteRecordings - }; - } - - parseRecordingTokenMetadata(metadata: string) { - try { - const parsedMetadata = JSON.parse(metadata); - return validateRecordingTokenMetadata(parsedMetadata); - } catch (error) { - this.logger.error('Failed to parse recording token metadata:', error); - throw new Error('Invalid recording token metadata format'); - } - } - /** * This method checks for rooms that have an auto-deletion date in the past and * tries to delete them based on their auto-deletion policy. diff --git a/meet-ce/backend/src/services/storage/legacy-storage.service.ts b/meet-ce/backend/src/services/storage/legacy-storage.service.ts index 027f2b1d..dfd074c0 100644 --- a/meet-ce/backend/src/services/storage/legacy-storage.service.ts +++ b/meet-ce/backend/src/services/storage/legacy-storage.service.ts @@ -1,4 +1,4 @@ -import { GlobalConfig, MeetApiKey, MeetRecordingInfo, MeetRoom, User } from '@openvidu-meet/typings'; +import { GlobalConfig, MeetApiKey, MeetRecordingInfo, MeetRoom, MeetUser } from '@openvidu-meet/typings'; import { inject, injectable } from 'inversify'; import { OpenViduMeetError, RedisKeyName } from '../../models/index.js'; import { LoggerService, RedisService } from '../index.js'; @@ -289,11 +289,11 @@ export class LegacyStorageService { * @param username - The username of the user to retrieve * @returns A promise that resolves to the user data, or null if not found */ - async getUser(username: string): Promise { + async getUser(username: string): Promise { const redisKey = RedisKeyName.USER + username; const storageKey = this.keyBuilder.buildUserKey(username); - const user = await this.getFromCacheAndStorage(redisKey, storageKey); + const user = await this.getFromCacheAndStorage(redisKey, storageKey); return user; } diff --git a/meet-ce/backend/src/services/token.service.ts b/meet-ce/backend/src/services/token.service.ts index bfb9182e..13d4534d 100644 --- a/meet-ce/backend/src/services/token.service.ts +++ b/meet-ce/backend/src/services/token.service.ts @@ -1,11 +1,8 @@ import { - LiveKitPermissions, - MeetTokenMetadata, - OpenViduMeetPermissions, - ParticipantOptions, - ParticipantRole, - RecordingPermissions, - User + MeetRoomMemberPermissions, + MeetRoomMemberRole, + MeetRoomMemberTokenMetadata, + MeetUser } from '@openvidu-meet/typings'; import { inject, injectable } from 'inversify'; import { jwtDecode } from 'jwt-decode'; @@ -18,7 +15,7 @@ import { LoggerService } from './index.js'; export class TokenService { constructor(@inject(LoggerService) protected logger: LoggerService) {} - async generateAccessToken(user: User): Promise { + async generateAccessToken(user: MeetUser): Promise { const tokenOptions: AccessTokenOptions = { identity: user.username, ttl: INTERNAL_CONFIG.ACCESS_TOKEN_EXPIRATION, @@ -29,7 +26,7 @@ export class TokenService { return await this.generateJwtToken(tokenOptions); } - async generateRefreshToken(user: User): Promise { + async generateRefreshToken(user: MeetUser): Promise { const tokenOptions: AccessTokenOptions = { identity: user.username, ttl: INTERNAL_CONFIG.REFRESH_TOKEN_EXPIRATION, @@ -40,54 +37,24 @@ export class TokenService { return await this.generateJwtToken(tokenOptions); } - async generateParticipantToken( - participantOptions: ParticipantOptions, - lkPermissions: LiveKitPermissions, - roles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[], - selectedRole: ParticipantRole + async generateRoomMemberToken( + role: MeetRoomMemberRole, + permissions: MeetRoomMemberPermissions, + participantName?: string ): Promise { - const { roomId, participantName } = participantOptions; - this.logger.info( - `Generating token for room '${roomId}'` + (participantName ? ` and participant '${participantName}'` : '') - ); - - let { participantIdentity } = participantOptions; - - if (participantName && !participantIdentity) { - participantIdentity = participantName; - } - - const metadata: MeetTokenMetadata = { + const metadata: MeetRoomMemberTokenMetadata = { livekitUrl: LIVEKIT_URL, - roles, - selectedRole + role, + permissions: permissions.meet }; + const tokenOptions: AccessTokenOptions = { - identity: participantIdentity, + identity: participantName, name: participantName, - ttl: INTERNAL_CONFIG.PARTICIPANT_TOKEN_EXPIRATION, + ttl: INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_EXPIRATION, metadata: JSON.stringify(metadata) }; - return await this.generateJwtToken(tokenOptions, lkPermissions as VideoGrant); - } - - async generateRecordingToken( - roomId: string, - role: ParticipantRole, - permissions: RecordingPermissions - ): Promise { - this.logger.info(`Generating recording token for room ${roomId}`); - const tokenOptions: AccessTokenOptions = { - ttl: INTERNAL_CONFIG.RECORDING_TOKEN_EXPIRATION, - metadata: JSON.stringify({ - role, - recordingPermissions: permissions - }) - }; - const grants: VideoGrant = { - room: roomId - }; - return await this.generateJwtToken(tokenOptions, grants); + return await this.generateJwtToken(tokenOptions, permissions.livekit as VideoGrant); } private async generateJwtToken(tokenOptions: AccessTokenOptions, grants?: VideoGrant): Promise { diff --git a/meet-ce/backend/src/services/user.service.ts b/meet-ce/backend/src/services/user.service.ts index 8d6896ee..7218ed15 100644 --- a/meet-ce/backend/src/services/user.service.ts +++ b/meet-ce/backend/src/services/user.service.ts @@ -1,4 +1,4 @@ -import { User, UserDTO, UserRole } from '@openvidu-meet/typings'; +import { MeetUser, MeetUserDTO, MeetUserRole } from '@openvidu-meet/typings'; import { inject, injectable } from 'inversify'; import { INTERNAL_CONFIG } from '../config/internal-config.js'; import { MEET_INITIAL_ADMIN_PASSWORD, MEET_INITIAL_ADMIN_USER } from '../environment.js'; @@ -18,17 +18,17 @@ export class UserService { * Initializes the default admin user */ async initializeAdminUser(): Promise { - const admin: User = { + const admin: MeetUser = { username: MEET_INITIAL_ADMIN_USER, passwordHash: await PasswordHelper.hashPassword(MEET_INITIAL_ADMIN_PASSWORD), - roles: [UserRole.ADMIN, UserRole.USER] + roles: [MeetUserRole.ADMIN, MeetUserRole.USER] }; await this.userRepository.create(admin); this.logger.info(`Admin user initialized with default credentials`); } - async authenticateUser(username: string, password: string): Promise { + async authenticateUser(username: string, password: string): Promise { const user = await this.getUser(username); if (!user || !(await PasswordHelper.verifyPassword(password, user.passwordHash))) { @@ -38,23 +38,23 @@ export class UserService { return user; } - async getUser(username: string): Promise { + async getUser(username: string): Promise { return this.userRepository.findByUsername(username); } - getAnonymousUser(): User { + getAnonymousUser(): MeetUser { return { username: INTERNAL_CONFIG.ANONYMOUS_USER, passwordHash: '', - roles: [UserRole.USER] + roles: [MeetUserRole.USER] }; } - getApiUser(): User { + getApiUser(): MeetUser { return { username: INTERNAL_CONFIG.API_USER, passwordHash: '', - roles: [UserRole.APP] + roles: [MeetUserRole.APP] }; } @@ -76,7 +76,7 @@ export class UserService { } // Convert user to UserDTO to remove sensitive information - convertToDTO(user: User): UserDTO { + convertToDTO(user: MeetUser): MeetUserDTO { const { passwordHash, ...userDTO } = user; return userDTO; } diff --git a/meet-ce/backend/src/utils/cookie.utils.ts b/meet-ce/backend/src/utils/cookie.utils.ts deleted file mode 100644 index 680b80dd..00000000 --- a/meet-ce/backend/src/utils/cookie.utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { CookieOptions } from 'express'; -import ms, { StringValue } from 'ms'; - -export const getCookieOptions = (path: string, expiration?: string): CookieOptions => { - return { - httpOnly: true, - secure: true, - sameSite: 'none', - partitioned: true, - maxAge: expiration ? ms(expiration as StringValue) : undefined, - path - }; -}; diff --git a/meet-ce/backend/src/utils/index.ts b/meet-ce/backend/src/utils/index.ts index 2b83da95..dbecc55e 100644 --- a/meet-ce/backend/src/utils/index.ts +++ b/meet-ce/backend/src/utils/index.ts @@ -1,5 +1,4 @@ export * from './array.utils.js'; -export * from './cookie.utils.js'; export * from './token.utils.js'; export * from './url.utils.js'; export * from './path.utils.js'; diff --git a/meet-ce/backend/src/utils/token.utils.ts b/meet-ce/backend/src/utils/token.utils.ts index 9484946a..7b724751 100644 --- a/meet-ce/backend/src/utils/token.utils.ts +++ b/meet-ce/backend/src/utils/token.utils.ts @@ -1,26 +1,5 @@ -import { AuthTransportMode } from '@openvidu-meet/typings'; import { Request } from 'express'; -import { container } from '../config/index.js'; import { INTERNAL_CONFIG } from '../config/internal-config.js'; -import { GlobalConfigService, LoggerService } from '../services/index.js'; - -/** - * Gets the current authentication transport mode from global config. - * - * @returns The current transport mode - */ -export const getAuthTransportMode = async (): Promise => { - try { - const configService = container.get(GlobalConfigService); - const globalConfig = await configService.getGlobalConfig(); - return globalConfig.securityConfig.authentication.authTransportMode; - } catch (error) { - const logger = container.get(LoggerService); - logger.error('Error fetching auth transport mode:', error); - // Fallback to header mode in case of error - return AuthTransportMode.HEADER; - } -}; /** * Extracts the access token from the request based on the configured transport mode. @@ -28,13 +7,8 @@ export const getAuthTransportMode = async (): Promise => { * @param req - Express request object * @returns The JWT token string or undefined if not found */ -export const getAccessToken = async (req: Request): Promise => { - return getTokenFromRequest( - req, - INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, - INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME, - 'accessToken' - ); +export const getAccessToken = (req: Request): string | undefined => { + return getTokenFromRequest(req, INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, 'accessToken'); }; /** @@ -43,37 +17,18 @@ export const getAccessToken = async (req: Request): Promise * @param req - Express request object * @returns The JWT refresh token string or undefined if not found */ -export const getRefreshToken = async (req: Request): Promise => { - return getTokenFromRequest(req, INTERNAL_CONFIG.REFRESH_TOKEN_HEADER, INTERNAL_CONFIG.REFRESH_TOKEN_COOKIE_NAME); +export const getRefreshToken = (req: Request): string | undefined => { + return getTokenFromRequest(req, INTERNAL_CONFIG.REFRESH_TOKEN_HEADER); }; /** - * Extracts the participant token from the request based on the configured transport mode. + * Extracts the room member token from the request based on the configured transport mode. * * @param req - Express request object - * @returns The JWT participant token string or undefined if not found + * @returns The JWT room member token string or undefined if not found */ -export const getParticipantToken = async (req: Request): Promise => { - return getTokenFromRequest( - req, - INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, - INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME - ); -}; - -/** - * Extracts the recording token from the request based on the configured transport mode. - * - * @param req - Express request object - * @returns The JWT recording token string or undefined if not found - */ -export const getRecordingToken = async (req: Request): Promise => { - return getTokenFromRequest( - req, - INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, - INTERNAL_CONFIG.RECORDING_TOKEN_COOKIE_NAME, - 'recordingToken' - ); +export const getRoomMemberToken = (req: Request): string | undefined => { + return getTokenFromRequest(req, INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, 'roomMemberToken'); }; /** @@ -81,23 +36,10 @@ export const getRecordingToken = async (req: Request): Promise => { - const transportMode = await getAuthTransportMode(); - - if (transportMode === AuthTransportMode.COOKIE) { - // Try to get from cookie - return req.cookies[cookieName]; - } - +const getTokenFromRequest = (req: Request, headerName: string, queryParamName?: string): string | undefined => { // Try to get from header const headerValue = req.headers[headerName]; @@ -108,7 +50,7 @@ const getTokenFromRequest = async ( /** * If not found in header, try to get from query parameter - * This is needed to send access/recording tokens via URL for video playback + * This is needed to send tokens via URL for video playback * since we cannot set custom headers in video element requests */ if (queryParamName) { diff --git a/meet-ce/backend/src/utils/url.utils.ts b/meet-ce/backend/src/utils/url.utils.ts index d1434c27..201cae82 100644 --- a/meet-ce/backend/src/utils/url.utils.ts +++ b/meet-ce/backend/src/utils/url.utils.ts @@ -1,6 +1,6 @@ import { container } from '../config/dependency-injector.config.js'; import { MEET_BASE_URL } from '../environment.js'; -import { HttpContextService } from '../services/http-context.service.js'; +import { BaseUrlService } from '../services/base-url.service.js'; /** * Returns the base URL for the application. @@ -28,6 +28,6 @@ export const getBaseUrl = (): string => { return baseUrl; } - const httpContextService = container.get(HttpContextService); - return httpContextService.getBaseUrl(); + const baseUrlService = container.get(BaseUrlService); + return baseUrlService.getBaseUrl(); }; diff --git a/meet-ce/backend/tests/helpers/assertion-helpers.ts b/meet-ce/backend/tests/helpers/assertion-helpers.ts index a5609faf..55559e6b 100644 --- a/meet-ce/backend/tests/helpers/assertion-helpers.ts +++ b/meet-ce/backend/tests/helpers/assertion-helpers.ts @@ -1,8 +1,8 @@ import { expect } from '@jest/globals'; +import { Response } from 'supertest'; 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, @@ -14,9 +14,9 @@ import { MeetRoomConfig, MeetRoomDeletionPolicyWithMeeting, MeetRoomDeletionPolicyWithRecordings, - MeetRoomStatus, - ParticipantPermissions, - ParticipantRole + MeetRoomMemberPermissions, + MeetRoomMemberRole, + MeetRoomStatus } from '@openvidu-meet/typings'; export const expectErrorResponse = ( @@ -483,41 +483,43 @@ export const expectValidGetRecordingUrlResponse = (response: Response, recording expect(parsedUrl.searchParams.get('secret')).toBeDefined(); }; -export const expectValidRoomRolesAndPermissionsResponse = (response: Response, roomId: string) => { +export const expectValidRoomMemberRolesAndPermissionsResponse = (response: Response, roomId: string) => { expect(response.status).toBe(200); expect(response.body).toEqual( expect.arrayContaining([ { - role: ParticipantRole.MODERATOR, - permissions: getPermissions(roomId, ParticipantRole.MODERATOR) + role: MeetRoomMemberRole.MODERATOR, + permissions: getPermissions(roomId, MeetRoomMemberRole.MODERATOR, true, true) }, { - role: ParticipantRole.SPEAKER, - permissions: getPermissions(roomId, ParticipantRole.SPEAKER) + role: MeetRoomMemberRole.SPEAKER, + permissions: getPermissions(roomId, MeetRoomMemberRole.SPEAKER, true, false) } ]) ); }; -export const expectValidRoomRoleAndPermissionsResponse = ( +export const expectValidRoomMemberRoleAndPermissionsResponse = ( response: Response, roomId: string, - participantRole: ParticipantRole + role: MeetRoomMemberRole ) => { expect(response.status).toBe(200); expect(response.body).toEqual({ - role: participantRole, - permissions: getPermissions(roomId, participantRole) + role: role, + permissions: getPermissions(roomId, role, true, role === MeetRoomMemberRole.MODERATOR) }); }; export const getPermissions = ( roomId: string, - role: ParticipantRole, + role: MeetRoomMemberRole, + canRetrieveRecordings: boolean, + canDeleteRecordings: boolean, addJoinPermission = true -): ParticipantPermissions => { +): MeetRoomMemberPermissions => { switch (role) { - case ParticipantRole.MODERATOR: + case MeetRoomMemberRole.MODERATOR: return { livekit: { roomJoin: addJoinPermission, @@ -527,13 +529,15 @@ export const getPermissions = ( canPublishData: true, canUpdateOwnMetadata: true }, - openvidu: { + meet: { canRecord: true, + canRetrieveRecordings, + canDeleteRecordings, canChat: true, canChangeVirtualBackground: true } }; - case ParticipantRole.SPEAKER: + case MeetRoomMemberRole.SPEAKER: return { livekit: { roomJoin: addJoinPermission, @@ -543,24 +547,26 @@ export const getPermissions = ( canPublishData: true, canUpdateOwnMetadata: true }, - openvidu: { + meet: { canRecord: false, + canRetrieveRecordings, + canDeleteRecordings, canChat: true, canChangeVirtualBackground: true } }; - default: - throw new Error(`Unknown role ${role}`); } }; -export const expectValidParticipantTokenResponse = ( +export const expectValidRoomMemberTokenResponse = ( response: Response, roomId: string, - participantRole: ParticipantRole, + role: MeetRoomMemberRole, + addJoinPermission = false, participantName?: string, participantIdentity?: string, - otherRoles: ParticipantRole[] = [] + canRetrieveRecordings?: boolean, + canDeleteRecordings?: boolean ) => { expect(response.status).toBe(200); expect(response.body).toHaveProperty('token'); @@ -568,27 +574,19 @@ export const expectValidParticipantTokenResponse = ( const token = response.body.token; const decodedToken = decodeJWTToken(token); - const permissions = getPermissions(roomId, participantRole, !!participantName); - const rolesAndPermissions = otherRoles.map((role) => ({ - role, - permissions: getPermissions(roomId, role, !!participantName).openvidu - })); + canRetrieveRecordings = canRetrieveRecordings ?? true; + canDeleteRecordings = canDeleteRecordings ?? role === MeetRoomMemberRole.MODERATOR; + const permissions = getPermissions(roomId, role, canRetrieveRecordings, canDeleteRecordings, addJoinPermission); - if (!rolesAndPermissions.some((r) => r.role === participantRole)) { - rolesAndPermissions.push({ - role: participantRole, - permissions: permissions.openvidu - }); - } - - if (participantName) { + if (addJoinPermission) { + expect(participantName).toBeDefined(); expect(decodedToken).toHaveProperty('name', participantName); expect(decodedToken).toHaveProperty('sub'); if (participantIdentity) { expect(decodedToken.sub).toBe(participantIdentity); } else { - expect(decodedToken.sub).toContain(participantName.replace(/\s+/g, '')); // Ensure sub contains the name without spaces + expect(decodedToken.sub).toBe(participantName); } } else { expect(decodedToken).not.toHaveProperty('name'); @@ -598,34 +596,9 @@ export const expectValidParticipantTokenResponse = ( expect(decodedToken).toHaveProperty('video', permissions.livekit); expect(decodedToken).toHaveProperty('metadata'); const metadata = JSON.parse(decodedToken.metadata || '{}'); - expect(metadata).toHaveProperty('roles'); - expect(metadata.roles).toEqual(expect.arrayContaining(rolesAndPermissions)); - expect(metadata).toHaveProperty('selectedRole', participantRole); -}; - -export const expectValidRecordingTokenResponse = ( - response: Response, - roomId: string, - participantRole: ParticipantRole, - canRetrieveRecordings: boolean, - canDeleteRecordings: boolean -) => { - expect(response.status).toBe(200); - expect(response.body).toHaveProperty('token'); - - const token = response.body.token; - const decodedToken = decodeJWTToken(token); - - expect(decodedToken).toHaveProperty('video', { - room: roomId - }); - expect(decodedToken).toHaveProperty('metadata'); - const metadata = JSON.parse(decodedToken.metadata || '{}'); - expect(metadata).toHaveProperty('role', participantRole); - expect(metadata).toHaveProperty('recordingPermissions', { - canRetrieveRecordings, - canDeleteRecordings - }); + expect(metadata).toHaveProperty('livekitUrl'); + expect(metadata).toHaveProperty('role', role); + expect(metadata).toHaveProperty('permissions', permissions.meet); }; const decodeJWTToken = (token: string) => { diff --git a/meet-ce/backend/tests/helpers/request-helpers.ts b/meet-ce/backend/tests/helpers/request-helpers.ts index 10b18518..82776d32 100644 --- a/meet-ce/backend/tests/helpers/request-helpers.ts +++ b/meet-ce/backend/tests/helpers/request-helpers.ts @@ -1,7 +1,6 @@ import { expect } from '@jest/globals'; import { AuthMode, - AuthTransportMode, MeetAppearanceConfig, MeetRecordingAccess, MeetRecordingInfo, @@ -10,10 +9,11 @@ import { MeetRoomConfig, MeetRoomDeletionPolicyWithMeeting, MeetRoomDeletionPolicyWithRecordings, + MeetRoomMemberRole, + MeetRoomMemberTokenMetadata, + MeetRoomMemberTokenOptions, MeetRoomOptions, - MeetTokenMetadata, - ParticipantOptions, - ParticipantRole, + MeetRoomStatus, SecurityConfig, WebhookConfig } from '@openvidu-meet/typings'; @@ -64,7 +64,7 @@ export const generateApiKey = async (): Promise => { const accessToken = await loginUser(); const response = await request(app) .post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/api-keys`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken) .send(); expect(response.status).toBe(201); expect(response.body).toHaveProperty('key'); @@ -77,7 +77,7 @@ export const getApiKeys = async () => { const accessToken = await loginUser(); const response = await request(app) .get(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/api-keys`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken) .send(); return response; }; @@ -88,7 +88,7 @@ export const deleteApiKeys = async () => { const accessToken = await loginUser(); const response = await request(app) .delete(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/api-keys`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken) .send(); return response; }; @@ -121,7 +121,7 @@ export const updateRoomsAppearanceConfig = async (config: { appearance: MeetAppe const accessToken = await loginUser(); const response = await request(app) .put(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/config/rooms/appearance`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken) .send(config); return response; }; @@ -132,7 +132,7 @@ export const getWebbhookConfig = async () => { const accessToken = await loginUser(); const response = await request(app) .get(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/config/webhooks`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken) .send(); return response; }; @@ -143,7 +143,7 @@ export const updateWebbhookConfig = async (config: WebhookConfig) => { const accessToken = await loginUser(); const response = await request(app) .put(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/config/webhooks`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken) .send(config); return response; @@ -171,7 +171,7 @@ export const updateSecurityConfig = async (config: SecurityConfig) => { const accessToken = await loginUser(); const response = await request(app) .put(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/config/security`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken) .send(config); return response; }; @@ -187,22 +187,6 @@ export const changeSecurityConfig = async (authMode: AuthMode) => { expect(response.status).toBe(200); }; -export const changeAuthTransportMode = async (authTransportMode: AuthTransportMode) => { - // Get current config to avoid overwriting other properties - let response = await getSecurityConfig(); - expect(response.status).toBe(200); - const currentConfig = response.body; - - currentConfig.authentication.authTransportMode = authTransportMode; - response = await updateSecurityConfig(currentConfig); - expect(response.status).toBe(200); -}; - -const getAuthTransportMode = async (): Promise => { - const response = await getSecurityConfig(); - return response.body.authentication.authTransportMode; -}; - export const restoreDefaultGlobalConfig = async () => { const configService = container.get(GlobalConfigService); const defaultGlobalConfig = configService['getDefaultConfig'](); @@ -210,8 +194,7 @@ export const restoreDefaultGlobalConfig = async () => { }; /** - * Logs in a user and returns the access token in the format - * "Bearer " or the cookie string if in cookie mode + * Logs in a user and returns the access token in the format "Bearer " */ export const loginUser = async (): Promise => { checkAppIsRunning(); @@ -221,47 +204,16 @@ export const loginUser = async (): Promise => { .send(CREDENTIALS.admin) .expect(200); - const authTransportMode = await getAuthTransportMode(); - - // Return token in header or cookie based on transport mode - if (authTransportMode === AuthTransportMode.COOKIE) { - const cookie = extractCookieFromHeaders(response, INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME); - return cookie!; - } - expect(response.body).toHaveProperty('accessToken'); return `Bearer ${response.body.accessToken}`; }; -/** - * Extracts cookie from response headers - * - * @param response - The supertest response - * @param cookieName - Name of the cookie to extract - * @returns The cookie string - */ -export const extractCookieFromHeaders = (response: Response, cookieName: string): string | undefined => { - expect(response.headers['set-cookie']).toBeDefined(); - const cookies = response.headers['set-cookie'] as unknown as string[]; - return cookies?.find((cookie) => cookie.startsWith(`${cookieName}=`)); -}; - -/** - * Selects the appropriate HTTP header name based on the format of the provided access token. - * - * If the access token starts with 'Bearer ', the specified header name is returned (typically 'Authorization'). - * Otherwise, 'Cookie' is returned, indicating that the token should be sent as a cookie. - */ -const selectHeaderBasedOnToken = (headerName: string, accessToken: string): string => { - return accessToken.startsWith('Bearer ') ? headerName : 'Cookie'; -}; - export const getProfile = async (accessToken: string) => { checkAppIsRunning(); return await request(app) .get(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/users/profile`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken) .send(); }; @@ -270,7 +222,7 @@ export const changePassword = async (currentPassword: string, newPassword: strin return await request(app) .post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/users/change-password`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken) .send({ currentPassword, newPassword }); }; @@ -299,19 +251,17 @@ export const getRooms = async (query: Record = {}) => { * * @param roomId - The unique identifier of the room to retrieve * @param fields - Optional fields to filter in the response + * @param roomMemberToken - Optional room member token for authentication * @returns A Promise that resolves to the room data * @throws Error if the app instance is not defined */ -export const getRoom = async (roomId: string, fields?: string, participantToken?: string, role?: ParticipantRole) => { +export const getRoom = async (roomId: string, fields?: string, roomMemberToken?: string) => { checkAppIsRunning(); const req = request(app).get(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${roomId}`).query({ fields }); - if (participantToken && role) { - req.set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, participantToken).set( - INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, - role - ); + if (roomMemberToken) { + req.set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); } else { req.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY); } @@ -353,7 +303,7 @@ export const updateRecordingAccessConfigInRoom = async (roomId: string, recordin expect(response.status).toBe(200); }; -export const updateRoomStatus = async (roomId: string, status: string) => { +export const updateRoomStatus = async (roomId: string, status: MeetRoomStatus) => { checkAppIsRunning(); return await request(app) @@ -430,7 +380,7 @@ export const runReleaseActiveRecordingLock = async (roomId: string) => { await recordingService.releaseRecordingLockIfNoEgress(roomId); }; -export const getRoomRoles = async (roomId: string) => { +export const getRoomMemberRoles = async (roomId: string) => { checkAppIsRunning(); const response = await request(app) @@ -439,7 +389,7 @@ export const getRoomRoles = async (roomId: string) => { return response; }; -export const getRoomRoleBySecret = async (roomId: string, secret: string) => { +export const getRoomMemberRoleBySecret = async (roomId: string, secret: string) => { checkAppIsRunning(); const response = await request(app) @@ -448,66 +398,33 @@ export const getRoomRoleBySecret = async (roomId: string, secret: string) => { return response; }; -export const generateParticipantTokenRequest = async ( - participantOptions: ParticipantOptions, - previousToken?: string -) => { +export const generateRoomMemberTokenRequest = async (roomId: string, tokenOptions: MeetRoomMemberTokenOptions) => { checkAppIsRunning(); // Disable authentication to generate the token await changeSecurityConfig(AuthMode.NONE); - // Generate the participant token - const req = request(app).post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/participants/token`); - - if (previousToken) { - req.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, previousToken), previousToken); - } - - req.send(participantOptions); - return await req; + // Generate the room member token + const response = await request(app) + .post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/rooms/${roomId}/token`) + .send(tokenOptions); + return response; }; /** - * Generates a participant token for a room and returns the JWT token in the format "Bearer " + * Generates a room member token for a room and returns the JWT token in the format "Bearer " */ -export const generateParticipantToken = async ( +export const generateRoomMemberToken = async ( roomId: string, - secret: string, - participantName: string + tokenOptions: MeetRoomMemberTokenOptions ): Promise => { - const response = await generateParticipantTokenRequest({ - roomId, - secret, - participantName - }); + const response = await generateRoomMemberTokenRequest(roomId, tokenOptions); expect(response.status).toBe(200); - const authTransportMode = await getAuthTransportMode(); - - // Return token in header or cookie based on transport mode - if (authTransportMode === AuthTransportMode.COOKIE) { - const cookie = extractCookieFromHeaders(response, INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME); - return cookie!; - } - expect(response.body).toHaveProperty('token'); return `Bearer ${response.body.token}`; }; -export const refreshParticipantToken = async (participantOptions: ParticipantOptions, previousToken: string) => { - checkAppIsRunning(); - - // Disable authentication to generate the token - await changeSecurityConfig(AuthMode.NONE); - - const response = await request(app) - .post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/participants/token/refresh`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, previousToken), previousToken) - .send(participantOptions); - return response; -}; - /** * Adds a fake participant to a LiveKit room for testing purposes. * @@ -543,7 +460,7 @@ export const joinFakeParticipant = async (roomId: string, participantIdentity: s export const updateParticipantMetadata = async ( roomId: string, participantIdentity: string, - metadata: MeetTokenMetadata + metadata: MeetRoomMemberTokenMetadata ) => { await ensureLivekitCliInstalled(); spawn('lk', [ @@ -625,15 +542,14 @@ export const disconnectFakeParticipants = async () => { export const updateParticipant = async ( roomId: string, participantIdentity: string, - newRole: ParticipantRole, + newRole: MeetRoomMemberRole, moderatorToken: string ) => { checkAppIsRunning(); const response = await request(app) .put(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/meetings/${roomId}/participants/${participantIdentity}/role`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, moderatorToken), moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR) + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, moderatorToken) .send({ role: newRole }); return response; }; @@ -643,8 +559,7 @@ export const kickParticipant = async (roomId: string, participantIdentity: strin const response = await request(app) .delete(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/meetings/${roomId}/participants/${participantIdentity}`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, moderatorToken), moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR) + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, moderatorToken) .send(); return response; }; @@ -654,56 +569,19 @@ export const endMeeting = async (roomId: string, moderatorToken: string) => { const response = await request(app) .delete(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/meetings/${roomId}`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, moderatorToken), moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR) + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, moderatorToken) .send(); await sleep('1s'); return response; }; -export const generateRecordingTokenRequest = async (roomId: string, secret: string) => { - checkAppIsRunning(); - - // Disable authentication to generate the token - await changeSecurityConfig(AuthMode.NONE); - - const response = await request(app) - .post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/rooms/${roomId}/recording-token`) - .send({ - secret - }); - return response; -}; - -/** - * Generates a token for retrieving/deleting recordings from a room and returns the JWT token in the format "Bearer " - */ -export const generateRecordingToken = async (roomId: string, secret: string) => { - const response = await generateRecordingTokenRequest(roomId, secret); - expect(response.status).toBe(200); - - const authTransportMode = await getAuthTransportMode(); - - // Return token in header or cookie based on transport mode - if (authTransportMode === AuthTransportMode.COOKIE) { - const cookie = extractCookieFromHeaders(response, INTERNAL_CONFIG.RECORDING_TOKEN_COOKIE_NAME); - return cookie!; - } - - expect(response.body).toHaveProperty('token'); - return `Bearer ${response.body.token}`; -}; - export const startRecording = async (roomId: string, moderatorToken: string) => { checkAppIsRunning(); return await request(app) .post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, moderatorToken), moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR) - .send({ - roomId - }); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, moderatorToken) + .send({ roomId }); }; export const stopRecording = async (recordingId: string, moderatorToken: string) => { @@ -711,8 +589,7 @@ export const stopRecording = async (recordingId: string, moderatorToken: string) const response = await request(app) .post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings/${recordingId}/stop`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, moderatorToken), moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR) + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, moderatorToken) .send(); await sleep('2.5s'); @@ -758,15 +635,15 @@ export const deleteRecording = async (recordingId: string) => { .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY); }; -export const bulkDeleteRecordings = async (recordingIds: string[], recordingToken?: string): Promise => { +export const bulkDeleteRecordings = async (recordingIds: string[], roomMemberToken?: string): Promise => { checkAppIsRunning(); const req = request(app) .delete(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings`) .query({ recordingIds: recordingIds.join(',') }); - if (recordingToken) { - req.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken), recordingToken); + if (roomMemberToken) { + req.set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); } else { req.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY); } @@ -777,7 +654,7 @@ export const bulkDeleteRecordings = async (recordingIds: string[], recordingToke export const downloadRecordings = async ( recordingIds: string[], asBuffer = true, - recordingToken?: string + roomMemberToken?: string ): Promise => { checkAppIsRunning(); @@ -785,8 +662,8 @@ export const downloadRecordings = async ( .get(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/download`) .query({ recordingIds: recordingIds.join(',') }); - if (recordingToken) { - req.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken), recordingToken); + if (roomMemberToken) { + req.set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); } else { req.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY); } @@ -819,8 +696,7 @@ export const stopAllRecordings = async (moderatorToken: string) => { const tasks = recordingIds.map((recordingId: string) => request(app) .post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings/${recordingId}/stop`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, moderatorToken), moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR) + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, moderatorToken) .send() ); const results = await Promise.all(tasks); @@ -841,12 +717,12 @@ export const getAllRecordings = async (query: Record = {}) => { .query(query); }; -export const getAllRecordingsFromRoom = async (recordingToken: string) => { +export const getAllRecordingsFromRoom = async (roomMemberToken: string) => { checkAppIsRunning(); return await request(app) .get(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken), recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); }; export const deleteAllRecordings = async () => { @@ -881,7 +757,7 @@ export const getAnalytics = async () => { const accessToken = await loginUser(); const response = await request(app) .get(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/analytics`) - .set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken) .send(); return response; diff --git a/meet-ce/backend/tests/helpers/test-scenarios.ts b/meet-ce/backend/tests/helpers/test-scenarios.ts index bb88c630..6e56850b 100644 --- a/meet-ce/backend/tests/helpers/test-scenarios.ts +++ b/meet-ce/backend/tests/helpers/test-scenarios.ts @@ -1,12 +1,12 @@ +import { MeetRoom, MeetRoomConfig } from '@openvidu-meet/typings'; import express, { Request, Response } from 'express'; import http from 'http'; import { StringValue } from 'ms'; import { MeetRoomHelper } from '../../src/helpers'; -import { MeetRoom, MeetRoomConfig } from '@openvidu-meet/typings'; import { expectValidStartRecordingResponse } from './assertion-helpers'; import { createRoom, - generateParticipantToken, + generateRoomMemberToken, joinFakeParticipant, sleep, startRecording, @@ -48,11 +48,11 @@ export const setupSingleRoom = async ( config }); - // Extract the room secrets and generate participant tokens + // Extract the room secrets and generate room member tokens const { moderatorSecret, speakerSecret } = MeetRoomHelper.extractSecretsFromRoom(room); const [moderatorToken, speakerToken] = await Promise.all([ - generateParticipantToken(room.roomId, moderatorSecret, 'MODERATOR'), - generateParticipantToken(room.roomId, speakerSecret, 'SPEAKER') + generateRoomMemberToken(room.roomId, { secret: moderatorSecret, grantJoinMeetingPermission: false }), + generateRoomMemberToken(room.roomId, { secret: speakerSecret, grantJoinMeetingPermission: false }) ]); // Join participant if needed @@ -76,7 +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, roomConfig?: MeetRoomConfig): Promise => { +export const setupMultiRoomTestContext = async ( + numRooms: number, + withParticipants: boolean, + roomConfig?: MeetRoomConfig +): Promise => { const rooms: RoomData[] = []; for (let i = 0; i < numRooms; i++) { diff --git a/meet-ce/backend/tests/integration/api/auth/login.test.ts b/meet-ce/backend/tests/integration/api/auth/login.test.ts index 436e8a2a..6485a8cb 100644 --- a/meet-ce/backend/tests/integration/api/auth/login.test.ts +++ b/meet-ce/backend/tests/integration/api/auth/login.test.ts @@ -1,14 +1,9 @@ import { beforeAll, describe, expect, it } from '@jest/globals'; -import { AuthTransportMode } from '@openvidu-meet/typings'; import { Express } from 'express'; import request from 'supertest'; import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js'; import { expectValidationError } from '../../../helpers/assertion-helpers.js'; -import { - changeAuthTransportMode, - extractCookieFromHeaders, - startTestServer -} from '../../../helpers/request-helpers.js'; +import { startTestServer } from '../../../helpers/request-helpers.js'; const AUTH_PATH = `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/auth`; @@ -36,28 +31,6 @@ describe('Authentication API Tests', () => { expect(response.body).toHaveProperty('refreshToken'); }); - it('should successfully login and set cookies in cookie mode', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - const response = await request(app) - .post(`${AUTH_PATH}/login`) - .send({ - username: 'admin', - password: 'admin' - }) - .expect(200); - - // Check for access and refresh token cookies - const accessTokenCookie = extractCookieFromHeaders(response, INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME); - const refreshTokenCookie = extractCookieFromHeaders(response, INTERNAL_CONFIG.REFRESH_TOKEN_COOKIE_NAME); - expect(accessTokenCookie).toBeDefined(); - expect(refreshTokenCookie).toBeDefined(); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should return 404 for invalid credentials', async () => { const response = await request(app) .post(`${AUTH_PATH}/login`) diff --git a/meet-ce/backend/tests/integration/api/auth/logout.test.ts b/meet-ce/backend/tests/integration/api/auth/logout.test.ts index 3c5bbbd9..bdfca976 100644 --- a/meet-ce/backend/tests/integration/api/auth/logout.test.ts +++ b/meet-ce/backend/tests/integration/api/auth/logout.test.ts @@ -1,13 +1,8 @@ import { beforeAll, describe, expect, it } from '@jest/globals'; -import { AuthTransportMode } from '@openvidu-meet/typings'; import { Express } from 'express'; import request from 'supertest'; import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js'; -import { - changeAuthTransportMode, - extractCookieFromHeaders, - startTestServer -} from '../../../helpers/request-helpers.js'; +import { startTestServer } from '../../../helpers/request-helpers.js'; const AUTH_PATH = `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/auth`; @@ -25,23 +20,5 @@ describe('Authentication API Tests', () => { expect(response.body).toHaveProperty('message'); expect(response.body.message).toBe('Logout successful'); }); - - it('should successfully logout and clear cookies in cookie mode', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - const response = await request(app).post(`${AUTH_PATH}/logout`).expect(200); - - // Check that the access and refresh token cookies are cleared - const accessTokenCookie = extractCookieFromHeaders(response, INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME); - const refreshTokenCookie = extractCookieFromHeaders(response, INTERNAL_CONFIG.REFRESH_TOKEN_COOKIE_NAME); - expect(accessTokenCookie).toBeDefined(); - expect(accessTokenCookie).toContain('Expires=Thu, 01 Jan 1970 00:00:00 GMT'); - expect(refreshTokenCookie).toBeDefined(); - expect(refreshTokenCookie).toContain('Expires=Thu, 01 Jan 1970 00:00:00 GMT'); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); }); }); diff --git a/meet-ce/backend/tests/integration/api/auth/refresh-token.test.ts b/meet-ce/backend/tests/integration/api/auth/refresh-token.test.ts index d7421b4a..a1d76b3c 100644 --- a/meet-ce/backend/tests/integration/api/auth/refresh-token.test.ts +++ b/meet-ce/backend/tests/integration/api/auth/refresh-token.test.ts @@ -1,13 +1,8 @@ import { beforeAll, describe, expect, it } from '@jest/globals'; -import { AuthTransportMode } from '@openvidu-meet/typings'; import { Express } from 'express'; import request from 'supertest'; import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js'; -import { - changeAuthTransportMode, - extractCookieFromHeaders, - startTestServer -} from '../../../helpers/request-helpers.js'; +import { startTestServer } from '../../../helpers/request-helpers.js'; const AUTH_PATH = `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/auth`; @@ -41,38 +36,6 @@ describe('Authentication API Tests', () => { expect(response.body).toHaveProperty('accessToken'); }); - it('should successfully refresh token and set new access token cookie in cookie mode', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // First, login to get a valid refresh token cookie - const loginResponse = await request(app) - .post(`${AUTH_PATH}/login`) - .send({ - username: 'admin', - password: 'admin' - }) - .expect(200); - - const refreshTokenCookie = extractCookieFromHeaders( - loginResponse, - INTERNAL_CONFIG.REFRESH_TOKEN_COOKIE_NAME - ); - expect(refreshTokenCookie).toBeDefined(); - - const response = await request(app) - .post(`${AUTH_PATH}/refresh`) - .set('Cookie', refreshTokenCookie!) - .expect(200); - - // Check that a new access token cookie is set - const newAccessTokenCookie = extractCookieFromHeaders(response, INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME); - expect(newAccessTokenCookie).toBeDefined(); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should return 400 when no refresh token is provided', async () => { const response = await request(app).post(`${AUTH_PATH}/refresh`).expect(400); diff --git a/meet-ce/backend/tests/integration/api/global-config/security.test.ts b/meet-ce/backend/tests/integration/api/global-config/security.test.ts index 0f31900d..f55765d4 100644 --- a/meet-ce/backend/tests/integration/api/global-config/security.test.ts +++ b/meet-ce/backend/tests/integration/api/global-config/security.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeAll, describe, expect, it } from '@jest/globals'; -import { AuthMode, AuthTransportMode, AuthType, SecurityConfig } from '@openvidu-meet/typings'; +import { AuthMode, AuthType, SecurityConfig } from '@openvidu-meet/typings'; import { expectValidationError } from '../../../helpers/assertion-helpers.js'; import { getSecurityConfig, @@ -24,7 +24,6 @@ describe('Security Config API Tests', () => { authMethod: { type: AuthType.SINGLE_USER }, - authTransportMode: AuthTransportMode.HEADER, authModeToAccessRoom: AuthMode.ALL_USERS } }; @@ -74,29 +73,10 @@ describe('Security Config API Tests', () => { ); }); - it('should reject when authTransportMode is not a valid enum value', async () => { - const response = await updateSecurityConfig({ - authentication: { - authMethod: { - type: AuthType.SINGLE_USER - }, - authModeToAccessRoom: AuthMode.ALL_USERS, - authTransportMode: 'invalid' - } - } as unknown as SecurityConfig); - - expectValidationError( - response, - 'authentication.authTransportMode', - "Invalid enum value. Expected 'cookie' | 'header', received 'invalid'" - ); - }); - - it('should reject when authModeToAccessRoom, authTransportMode or authMethod are not provided', async () => { + it('should reject when authModeToAccessRoom or authMethod are not provided', async () => { let response = await updateSecurityConfig({ authentication: { - authMode: AuthMode.NONE, - authTransportMode: AuthTransportMode.HEADER + authModeToAccessRoom: AuthMode.NONE } } as unknown as SecurityConfig); expectValidationError(response, 'authentication.authMethod', 'Required'); @@ -105,18 +85,7 @@ describe('Security Config API Tests', () => { authentication: { authMethod: { type: AuthType.SINGLE_USER - }, - authModeToAccessRoom: AuthMode.NONE - } - } as unknown as SecurityConfig); - expectValidationError(response, 'authentication.authTransportMode', 'Required'); - - response = await updateSecurityConfig({ - authentication: { - authMethod: { - type: AuthType.SINGLE_USER - }, - authTransportMode: AuthTransportMode.HEADER + } } } as unknown as SecurityConfig); expectValidationError(response, 'authentication.authModeToAccessRoom', 'Required'); @@ -138,7 +107,6 @@ describe('Security Config API Tests', () => { authMethod: { type: AuthType.SINGLE_USER }, - authTransportMode: AuthTransportMode.HEADER, authModeToAccessRoom: AuthMode.NONE } }; diff --git a/meet-ce/backend/tests/integration/api/participants/participant-name.service.test.ts b/meet-ce/backend/tests/integration/api/meetings/participant-name.service.test.ts similarity index 100% rename from meet-ce/backend/tests/integration/api/participants/participant-name.service.test.ts rename to meet-ce/backend/tests/integration/api/meetings/participant-name.service.test.ts diff --git a/meet-ce/backend/tests/integration/api/meetings/update-participant.test.ts b/meet-ce/backend/tests/integration/api/meetings/update-participant.test.ts index 45134e1b..5023e50a 100644 --- a/meet-ce/backend/tests/integration/api/meetings/update-participant.test.ts +++ b/meet-ce/backend/tests/integration/api/meetings/update-participant.test.ts @@ -1,8 +1,8 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { MeetRoomMemberRole, MeetRoomMemberTokenMetadata, MeetSignalType } from '@openvidu-meet/typings'; import { container } from '../../../../src/config/index.js'; import { LIVEKIT_URL } from '../../../../src/environment.js'; import { FrontendEventService, LiveKitService } from '../../../../src/services/index.js'; -import { MeetSignalType, MeetTokenMetadata, ParticipantRole } from '@openvidu-meet/typings'; import { getPermissions } from '../../../helpers/assertion-helpers.js'; import { deleteAllRooms, @@ -31,16 +31,11 @@ describe('Meetings API Tests', () => { }); describe('Update Participant Tests', () => { - const setParticipantMetadata = async (roomId: string, role: ParticipantRole) => { - const metadata: MeetTokenMetadata = { + const setParticipantMetadata = async (roomId: string, role: MeetRoomMemberRole) => { + const metadata: MeetRoomMemberTokenMetadata = { livekitUrl: LIVEKIT_URL, - roles: [ - { - role: role, - permissions: getPermissions(roomId, role).openvidu - } - ], - selectedRole: role + role, + permissions: getPermissions(roomId, role, true, true).meet }; await updateParticipantMetadata(roomId, participantIdentity, metadata); }; @@ -53,12 +48,12 @@ describe('Meetings API Tests', () => { const frontendEventService = container.get(FrontendEventService); const sendSignalSpy = jest.spyOn(frontendEventService as any, 'sendSignal'); - await setParticipantMetadata(roomData.room.roomId, ParticipantRole.SPEAKER); + await setParticipantMetadata(roomData.room.roomId, MeetRoomMemberRole.SPEAKER); const response = await updateParticipant( roomData.room.roomId, participantIdentity, - ParticipantRole.MODERATOR, + MeetRoomMemberRole.MODERATOR, roomData.moderatorToken ); expect(response.status).toBe(200); @@ -68,9 +63,7 @@ describe('Meetings API Tests', () => { expect(participant).toBeDefined(); expect(participant).toHaveProperty('metadata'); const metadata = JSON.parse(participant.metadata || '{}'); - expect(metadata).toHaveProperty('roles'); - expect(metadata.roles).toContainEqual(expect.objectContaining({ role: ParticipantRole.MODERATOR })); - expect(metadata).toHaveProperty('selectedRole', ParticipantRole.MODERATOR); + expect(metadata).toHaveProperty('role', MeetRoomMemberRole.MODERATOR); // Verify sendSignal method has been called twice expect(sendSignalSpy).toHaveBeenCalledTimes(2); @@ -81,7 +74,7 @@ describe('Meetings API Tests', () => { { roomId: roomData.room.roomId, participantIdentity, - newRole: ParticipantRole.MODERATOR, + newRole: MeetRoomMemberRole.MODERATOR, secret: expect.any(String), timestamp: expect.any(Number) }, @@ -97,7 +90,7 @@ describe('Meetings API Tests', () => { { roomId: roomData.room.roomId, participantIdentity, - newRole: ParticipantRole.MODERATOR, + newRole: MeetRoomMemberRole.MODERATOR, secret: undefined, timestamp: expect.any(Number) }, @@ -109,12 +102,12 @@ describe('Meetings API Tests', () => { }); it('should update participant role from moderator to speaker', async () => { - await setParticipantMetadata(roomData.room.roomId, ParticipantRole.MODERATOR); + await setParticipantMetadata(roomData.room.roomId, MeetRoomMemberRole.MODERATOR); const response = await updateParticipant( roomData.room.roomId, participantIdentity, - ParticipantRole.SPEAKER, + MeetRoomMemberRole.SPEAKER, roomData.moderatorToken ); expect(response.status).toBe(200); @@ -124,16 +117,14 @@ describe('Meetings API Tests', () => { expect(participant).toBeDefined(); expect(participant).toHaveProperty('metadata'); const metadata = JSON.parse(participant.metadata || '{}'); - expect(metadata).toHaveProperty('roles'); - expect(metadata.roles).toContainEqual(expect.objectContaining({ role: ParticipantRole.SPEAKER })); - expect(metadata).toHaveProperty('selectedRole', ParticipantRole.SPEAKER); + expect(metadata).toHaveProperty('role', MeetRoomMemberRole.SPEAKER); }); it('should fail with 404 if participant does not exist', async () => { const response = await updateParticipant( roomData.room.roomId, 'NON_EXISTENT_PARTICIPANT', - ParticipantRole.MODERATOR, + MeetRoomMemberRole.MODERATOR, roomData.moderatorToken ); expect(response.status).toBe(404); @@ -148,7 +139,7 @@ describe('Meetings API Tests', () => { response = await updateParticipant( roomData.room.roomId, participantIdentity, - ParticipantRole.MODERATOR, + MeetRoomMemberRole.MODERATOR, roomData.moderatorToken ); expect(response.status).toBe(404); diff --git a/meet-ce/backend/tests/integration/api/participants/generate-token.test.ts b/meet-ce/backend/tests/integration/api/participants/generate-token.test.ts deleted file mode 100644 index b05bf6a0..00000000 --- a/meet-ce/backend/tests/integration/api/participants/generate-token.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; -import { AuthTransportMode, ParticipantOptions, ParticipantRole } from '@openvidu-meet/typings'; -import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js'; -import { expectValidationError, expectValidParticipantTokenResponse } from '../../../helpers/assertion-helpers.js'; -import { - changeAuthTransportMode, - deleteAllRooms, - disconnectFakeParticipants, - endMeeting, - extractCookieFromHeaders, - generateParticipantToken, - generateParticipantTokenRequest, - startTestServer, - updateRoomStatus -} from '../../../helpers/request-helpers.js'; -import { RoomData, setupSingleRoom } from '../../../helpers/test-scenarios.js'; - -const participantName = 'TEST_PARTICIPANT'; - -describe('Participant API Tests', () => { - let roomData: RoomData; - - beforeAll(async () => { - await startTestServer(); - }); - - beforeEach(async () => { - roomData = await setupSingleRoom(); - }); - - // Force to cleanup participant name reservations after each test - afterEach(async () => { - await disconnectFakeParticipants(); - await deleteAllRooms(); - }); - - describe('Generate Participant Token Tests', () => { - it('should generate a participant token without join permissions when not specifying participant name', async () => { - const response = await generateParticipantTokenRequest({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret - }); - expectValidParticipantTokenResponse(response, roomData.room.roomId, ParticipantRole.MODERATOR); - }); - - it('should generate a participant token with moderator permissions when using the moderator secret', async () => { - const response = await generateParticipantTokenRequest({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName - }); - expectValidParticipantTokenResponse( - response, - roomData.room.roomId, - ParticipantRole.MODERATOR, - participantName - ); - }); - - it('should generate a participant token with speaker permissions when using the speaker secret', async () => { - const response = await generateParticipantTokenRequest({ - roomId: roomData.room.roomId, - secret: roomData.speakerSecret, - participantName - }); - expectValidParticipantTokenResponse( - response, - roomData.room.roomId, - ParticipantRole.SPEAKER, - participantName - ); - }); - - it(`should generate a participant token with both speaker and moderator permissions - when using the speaker secret after having a moderator token in cookie mode`, async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - const moderatorToken = await generateParticipantToken( - roomData.room.roomId, - roomData.moderatorSecret, - `${participantName}_MODERATOR` - ); - const speakerResponse = await generateParticipantTokenRequest( - { - roomId: roomData.room.roomId, - secret: roomData.speakerSecret, - participantName: `${participantName}_SPEAKER` - }, - moderatorToken - ); - expectValidParticipantTokenResponse( - speakerResponse, - roomData.room.roomId, - ParticipantRole.SPEAKER, - `${participantName}_SPEAKER`, - undefined, - [ParticipantRole.MODERATOR] - ); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - - it('should generate a participant token and store it in a cookie when in cookie mode', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Generate the participant token - const response = await generateParticipantTokenRequest({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName - }); - expectValidParticipantTokenResponse( - response, - roomData.room.roomId, - ParticipantRole.MODERATOR, - participantName - ); - - // Check that the token is included in a cookie - const participantTokenCookie = extractCookieFromHeaders( - response, - INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME - ); - expect(participantTokenCookie).toBeDefined(); - expect(participantTokenCookie).toContain(response.body.token); - expect(participantTokenCookie).toContain('HttpOnly'); - expect(participantTokenCookie).toContain('SameSite=None'); - expect(participantTokenCookie).toContain('Secure'); - expect(participantTokenCookie).toContain('Path=/'); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - - it('should success when participant already exists in the room', async () => { - roomData = await setupSingleRoom(true); - let response = await generateParticipantTokenRequest({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName - }); - - // First participant using API. LK CLI participants can reuse the same name. - expectValidParticipantTokenResponse( - response, - roomData.room.roomId, - ParticipantRole.MODERATOR, - participantName - ); - - response = await generateParticipantTokenRequest({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName - }); - - // Second participant using API, the participant name should be unique - expectValidParticipantTokenResponse( - response, - roomData.room.roomId, - ParticipantRole.MODERATOR, - participantName + '_1' - ); - - // Recreate the room without the participant - roomData = await setupSingleRoom(); - }); - - it('should fail with 409 when room is closed', async () => { - await endMeeting(roomData.room.roomId, roomData.moderatorToken); - await updateRoomStatus(roomData.room.roomId, 'closed'); - const response = await generateParticipantTokenRequest({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName - }); - expect(response.status).toBe(409); - }); - - it('should fail with 404 when room does not exist', async () => { - const response = await generateParticipantTokenRequest({ - roomId: 'non_existent_room', - secret: roomData.moderatorSecret, - participantName - }); - expect(response.status).toBe(404); - }); - - it('should fail with 400 when secret is invalid', async () => { - const response = await generateParticipantTokenRequest({ - roomId: roomData.room.roomId, - secret: 'invalid_secret', - participantName - }); - expect(response.status).toBe(400); - }); - }); - - describe('Generate Participant Token Validation Tests', () => { - it('should fail when roomId is not provided', async () => { - const response = await generateParticipantTokenRequest({ - secret: roomData.moderatorSecret, - participantName - } as unknown as ParticipantOptions); - expectValidationError(response, 'roomId', 'Required'); - }); - - it('should fail when secret is not provided', async () => { - const response = await generateParticipantTokenRequest({ - roomId: roomData.room.roomId, - participantName - } as unknown as ParticipantOptions); - expectValidationError(response, 'secret', 'Required'); - }); - - it('should fail when secret is empty', async () => { - const response = await generateParticipantTokenRequest({ - roomId: roomData.room.roomId, - secret: '', - participantName - }); - expectValidationError(response, 'secret', 'Secret is required'); - }); - }); -}); diff --git a/meet-ce/backend/tests/integration/api/participants/refresh-token.test.ts b/meet-ce/backend/tests/integration/api/participants/refresh-token.test.ts deleted file mode 100644 index 4a44e917..00000000 --- a/meet-ce/backend/tests/integration/api/participants/refresh-token.test.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; -import { AuthTransportMode, ParticipantOptions, ParticipantRole } from '@openvidu-meet/typings'; -import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js'; -import { expectValidationError, expectValidParticipantTokenResponse } from '../../../helpers/assertion-helpers.js'; -import { - changeAuthTransportMode, - deleteAllRooms, - disconnectFakeParticipants, - extractCookieFromHeaders, - refreshParticipantToken, - sleep, - startTestServer -} from '../../../helpers/request-helpers.js'; -import { RoomData, setupSingleRoom } from '../../../helpers/test-scenarios.js'; - -const participantName = 'TEST_PARTICIPANT'; - -describe('Participant API Tests', () => { - let roomData: RoomData; - - beforeAll(async () => { - await startTestServer(); - - // Set short expiration for testing - const initialTokenExpiration = INTERNAL_CONFIG.PARTICIPANT_TOKEN_EXPIRATION; - INTERNAL_CONFIG.PARTICIPANT_TOKEN_EXPIRATION = '1s'; - - roomData = await setupSingleRoom(true); - await sleep('2s'); // Ensure the token is expired - - // Restore original expiration after setup - INTERNAL_CONFIG.PARTICIPANT_TOKEN_EXPIRATION = initialTokenExpiration; - }); - - afterAll(async () => { - await disconnectFakeParticipants(); - await deleteAllRooms(); - }); - - describe('Refresh Participant Token Tests', () => { - it('should refresh participant token with moderator permissions when using the moderator secret', async () => { - const response = await refreshParticipantToken( - { - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName, - participantIdentity: participantName - }, - roomData.moderatorToken - ); - expectValidParticipantTokenResponse( - response, - roomData.room.roomId, - ParticipantRole.MODERATOR, - participantName, - participantName - ); - }); - - it('should refresh participant token with speaker permissions when using the speaker secret', async () => { - const response = await refreshParticipantToken( - { - roomId: roomData.room.roomId, - secret: roomData.speakerSecret, - participantName, - participantIdentity: participantName - }, - roomData.speakerToken - ); - expectValidParticipantTokenResponse( - response, - roomData.room.roomId, - ParticipantRole.SPEAKER, - participantName, - participantName - ); - }); - - it('should refresh participant token and store it in a cookie when in cookie mode', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Create a new room to obtain participant token in cookie mode - const newRoomData = await setupSingleRoom(true); - - // Refresh the participant token - const response = await refreshParticipantToken( - { - roomId: newRoomData.room.roomId, - secret: newRoomData.moderatorSecret, - participantName, - participantIdentity: participantName - }, - newRoomData.moderatorToken - ); - expectValidParticipantTokenResponse( - response, - newRoomData.room.roomId, - ParticipantRole.MODERATOR, - participantName, - participantName - ); - - // Check that the token is included in a cookie - const participantTokenCookie = extractCookieFromHeaders( - response, - INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME - ); - expect(participantTokenCookie).toBeDefined(); - expect(participantTokenCookie).toContain(response.body.token); - expect(participantTokenCookie).toContain('HttpOnly'); - expect(participantTokenCookie).toContain('SameSite=None'); - expect(participantTokenCookie).toContain('Secure'); - expect(participantTokenCookie).toContain('Path=/'); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - - it('should fail with 400 when secret is invalid', async () => { - const response = await refreshParticipantToken( - { - roomId: roomData.room.roomId, - secret: 'invalid_secret', - participantName, - participantIdentity: participantName - }, - roomData.moderatorToken - ); - expect(response.status).toBe(400); - }); - - it('should fail with 400 when previous token is not provided', async () => { - const response = await refreshParticipantToken( - { - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName, - participantIdentity: participantName - }, - '' - ); - expect(response.status).toBe(400); - expect(response.body.message).toBe('No participant token provided'); - }); - - it('should fail with 400 when participantIdentity is not provided', async () => { - const response = await refreshParticipantToken( - { - roomId: roomData.room.roomId, - secret: 'invalid_secret', - participantName - }, - roomData.moderatorToken - ); - expect(response.status).toBe(400); - }); - - it('should fail with 404 when participant does not exist in the room', async () => { - const newRoomData = await setupSingleRoom(); - const response = await refreshParticipantToken( - { - roomId: newRoomData.room.roomId, - secret: newRoomData.moderatorSecret, - participantName, - participantIdentity: participantName - }, - roomData.moderatorToken - ); - expect(response.status).toBe(404); - }); - - it('should fail with 404 when room does not exist', async () => { - const response = await refreshParticipantToken( - { - roomId: 'non_existent_room', - secret: roomData.moderatorSecret, - participantName, - participantIdentity: participantName - }, - roomData.moderatorToken - ); - expect(response.status).toBe(404); - }); - }); - - describe('Refresh Participant Token Validation Tests', () => { - it('should fail when roomId is not provided', async () => { - const response = await refreshParticipantToken( - { - secret: roomData.moderatorSecret, - participantName, - participantIdentity: participantName - } as unknown as ParticipantOptions, - roomData.moderatorToken - ); - expectValidationError(response, 'roomId', 'Required'); - }); - - it('should fail when secret is not provided', async () => { - const response = await refreshParticipantToken( - { - roomId: roomData.room.roomId, - participantName, - participantIdentity: participantName - } as unknown as ParticipantOptions, - roomData.moderatorToken - ); - expectValidationError(response, 'secret', 'Required'); - }); - - it('should fail when secret is empty', async () => { - const response = await refreshParticipantToken( - { - roomId: roomData.room.roomId, - secret: '', - participantName, - participantIdentity: participantName - }, - roomData.moderatorToken - ); - expectValidationError(response, 'secret', 'Secret is required'); - }); - }); -}); diff --git a/meet-ce/backend/tests/integration/api/recordings/bulk-delete-recording.test.ts b/meet-ce/backend/tests/integration/api/recordings/bulk-delete-recording.test.ts index 5d0f959e..65ee4d68 100644 --- a/meet-ce/backend/tests/integration/api/recordings/bulk-delete-recording.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/bulk-delete-recording.test.ts @@ -5,7 +5,7 @@ import { deleteAllRecordings, deleteAllRooms, disconnectFakeParticipants, - generateRecordingToken, + generateRoomMemberToken, getAllRecordings, startTestServer, stopRecording @@ -119,14 +119,14 @@ describe('Recording API Tests', () => { const recordingId = roomData.recordingId!; // Generate a recording token for the room - const recordingToken = await generateRecordingToken(roomId, roomData.moderatorSecret); + const roomMemberToken = await generateRoomMemberToken(roomId, { secret: roomData.moderatorSecret }); // Create another room and start a recording const otherRoomData = await setupSingleRoomWithRecording(true); const otherRecordingId = otherRoomData.recordingId!; // Intenta eliminar ambas grabaciones usando el token de la primera sala - const deleteResponse = await bulkDeleteRecordings([recordingId, otherRecordingId], recordingToken); + const deleteResponse = await bulkDeleteRecordings([recordingId, otherRecordingId], roomMemberToken); expect(deleteResponse.status).toBe(400); expect(deleteResponse.body).toEqual({ diff --git a/meet-ce/backend/tests/integration/api/recordings/download-recordings.test.ts b/meet-ce/backend/tests/integration/api/recordings/download-recordings.test.ts index 5faefb41..a2f6d31b 100644 --- a/meet-ce/backend/tests/integration/api/recordings/download-recordings.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/download-recordings.test.ts @@ -7,7 +7,7 @@ import { deleteAllRooms, disconnectFakeParticipants, downloadRecordings, - generateRecordingToken, + generateRoomMemberToken, startTestServer } from '../../../helpers/request-helpers'; import { setupMultiRecordingsTestContext, setupSingleRoomWithRecording } from '../../../helpers/test-scenarios'; @@ -59,12 +59,12 @@ describe('Recording API Tests', () => { const roomData = await setupSingleRoomWithRecording(true); const roomId = roomData.room.roomId; const recordingId = roomData.recordingId!; - const recordingToken = await generateRecordingToken(roomId, roomData.moderatorSecret); + const roomMemberToken = await generateRoomMemberToken(roomId, { secret: roomData.moderatorSecret }); const otherRoomData = await setupSingleRoomWithRecording(true); const otherRecordingId = otherRoomData.recordingId!; - const res = await downloadRecordings([recordingId, otherRecordingId], true, recordingToken); + const res = await downloadRecordings([recordingId, otherRecordingId], true, roomMemberToken); expect(res.status).toBe(200); const entries = await getZipEntries(res.body); @@ -75,12 +75,12 @@ describe('Recording API Tests', () => { it('should return an error if none of the recordings belong to the room in the token', async () => { const roomData = await setupSingleRoomWithRecording(true); const roomId = roomData.room.roomId; - const recordingToken = await generateRecordingToken(roomId, roomData.moderatorSecret); + const roomMemberToken = await generateRoomMemberToken(roomId, { secret: roomData.moderatorSecret }); const otherRoomData = await setupSingleRoomWithRecording(true); const otherRecordingId = otherRoomData.recordingId!; - const res = await downloadRecordings([otherRecordingId], false, recordingToken); + const res = await downloadRecordings([otherRecordingId], false, roomMemberToken); expect(res.status).toBe(400); expect(res.body).toHaveProperty('error'); diff --git a/meet-ce/backend/tests/integration/api/recordings/get-recordings.test.ts b/meet-ce/backend/tests/integration/api/recordings/get-recordings.test.ts index fb0b4a08..5e62de73 100644 --- a/meet-ce/backend/tests/integration/api/recordings/get-recordings.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/get-recordings.test.ts @@ -10,7 +10,7 @@ import { deleteAllRecordings, deleteAllRooms, disconnectFakeParticipants, - generateRecordingToken, + generateRoomMemberToken, getAllRecordings, getAllRecordingsFromRoom, startTestServer @@ -63,12 +63,12 @@ describe('Recordings API Tests', () => { const roomId = roomData.room.roomId; // Generate a recording token for the room - const recordingToken = await generateRecordingToken(roomId, roomData.speakerSecret); + const roomMemberToken = await generateRoomMemberToken(roomId, { secret: roomData.speakerSecret }); // Create a new room and start a recording roomData = await setupSingleRoomWithRecording(true); - const response = await getAllRecordingsFromRoom(recordingToken); + const response = await getAllRecordingsFromRoom(roomMemberToken); expectSuccessListRecordingResponse(response, 1, false, false); expect(response.body.recordings[0].roomId).toBe(roomId); }); diff --git a/meet-ce/backend/tests/integration/api/recordings/orphaned-locks-collector.test.ts b/meet-ce/backend/tests/integration/api/recordings/orphaned-locks-collector.test.ts index a72640e3..0a85d091 100644 --- a/meet-ce/backend/tests/integration/api/recordings/orphaned-locks-collector.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/orphaned-locks-collector.test.ts @@ -254,7 +254,7 @@ describe('Recording Garbage Collector Tests', () => { const now = 1_000_000; jest.setSystemTime(now); - (mutexService.getLockCreatedAt as jest.Mock).mockResolvedValueOnce((now - ms('5m')) as never); // 5 minutos ago + (mutexService.getLockCreatedAt as jest.Mock).mockResolvedValueOnce((now - ms('5m')) as never); // 5 minutes ago // Configure mocks específicos para este test (livekitService.roomExists as jest.Mock).mockResolvedValue(true as never); @@ -302,7 +302,7 @@ describe('Recording Garbage Collector Tests', () => { (mutexService.lockExists as jest.Mock).mockResolvedValueOnce(true as never); (mutexService.getLockCreatedAt as jest.Mock).mockResolvedValueOnce((Date.now() - ms('5m')) as never); - // Configure mocks específicos para este test + // Configure specific mocks for this test (livekitService.roomExists as jest.Mock).mockResolvedValueOnce(true as never); (livekitService.getRoom as jest.Mock).mockResolvedValueOnce({ numParticipants: 0, @@ -348,7 +348,7 @@ describe('Recording Garbage Collector Tests', () => { (mutexService.lockExists as jest.Mock).mockResolvedValue(true as never); (mutexService.getLockCreatedAt as jest.Mock).mockResolvedValueOnce((Date.now() - ms('5m')) as never); - // Configure mocks específicos para este test + // Configure specific mocks for this test (livekitService.roomExists as jest.Mock).mockResolvedValueOnce(false as never); (livekitService.getInProgressRecordingsEgress as jest.Mock).mockResolvedValueOnce([ { @@ -378,7 +378,7 @@ describe('Recording Garbage Collector Tests', () => { (mutexService.lockExists as jest.Mock).mockResolvedValue(true as never); (mutexService.getLockCreatedAt as jest.Mock).mockResolvedValueOnce((Date.now() - ms('5m')) as never); - // Configure mocks específicos para este test + // Configure specific mocks for this test (livekitService.roomExists as jest.Mock).mockResolvedValueOnce(false as never); (livekitService.getInProgressRecordingsEgress as jest.Mock).mockResolvedValueOnce([] as never); diff --git a/meet-ce/backend/tests/integration/api/rooms/garbage-collector.test.ts b/meet-ce/backend/tests/integration/api/rooms/garbage-collector.test.ts index 62a1f446..0e9695c3 100644 --- a/meet-ce/backend/tests/integration/api/rooms/garbage-collector.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/garbage-collector.test.ts @@ -2,17 +2,14 @@ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; import ms from 'ms'; import { setInternalConfig } from '../../../../src/config/internal-config.js'; import { MeetRoomHelper } from '../../../../src/helpers/room.helper.js'; -import { - MeetRoomDeletionPolicyWithMeeting, - MeetRoomDeletionPolicyWithRecordings -} from '@openvidu-meet/typings'; +import { MeetRoomDeletionPolicyWithMeeting, MeetRoomDeletionPolicyWithRecordings } from '@openvidu-meet/typings'; import { createRoom, deleteAllRecordings, deleteAllRooms, disconnectFakeParticipants, endMeeting, - generateParticipantToken, + generateRoomMemberToken, getRoom, joinFakeParticipant, runRoomGarbageCollector, @@ -102,7 +99,7 @@ describe('Room Garbage Collector Tests', () => { // End the meeting const { moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(room); - const moderatorToken = await generateParticipantToken(room.roomId, moderatorSecret, 'moderator'); + const moderatorToken = await generateRoomMemberToken(room.roomId, { secret: moderatorSecret }); await endMeeting(room.roomId, moderatorToken); // Verify that the room is deleted @@ -180,7 +177,7 @@ describe('Room Garbage Collector Tests', () => { // Start recording const { moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(room1); - const moderatorToken = await generateParticipantToken(room1.roomId, moderatorSecret, 'moderator'); + const moderatorToken = await generateRoomMemberToken(room1.roomId, { secret: moderatorSecret }); await startRecording(room1.roomId, moderatorToken); await runRoomGarbageCollector(); diff --git a/meet-ce/backend/tests/integration/api/rooms/generate-recording-token.test.ts b/meet-ce/backend/tests/integration/api/rooms/generate-recording-token.test.ts deleted file mode 100644 index 6b2b49dd..00000000 --- a/meet-ce/backend/tests/integration/api/rooms/generate-recording-token.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; -import { INTERNAL_CONFIG, setInternalConfig } from '../../../../src/config/internal-config.js'; -import { AuthTransportMode, MeetRecordingAccess, ParticipantRole } from '@openvidu-meet/typings'; -import { expectValidRecordingTokenResponse } from '../../../helpers/assertion-helpers.js'; -import { - changeAuthTransportMode, - deleteAllRecordings, - deleteAllRooms, - disconnectFakeParticipants, - extractCookieFromHeaders, - generateRecordingTokenRequest, - sleep, - startTestServer, - updateRecordingAccessConfigInRoom -} from '../../../helpers/request-helpers.js'; -import { RoomData, setupSingleRoomWithRecording } from '../../../helpers/test-scenarios.js'; - -describe('Room API Tests', () => { - let roomData: RoomData; - - beforeAll(async () => { - setInternalConfig({ - MEETING_DEPARTURE_TIMEOUT: '1s' - }); - await startTestServer(); - roomData = await setupSingleRoomWithRecording(true); - // Disconnect all participants to allow the room updates request to succeed - await disconnectFakeParticipants(); - await sleep('1s'); // Wait for the meeting to be closed after all participants have left - }); - - afterAll(async () => { - await disconnectFakeParticipants(); - await Promise.all([deleteAllRooms(), deleteAllRecordings()]); - }); - - describe('Generate Recording Token Tests', () => { - it('should generate a recording token with canRetrieve and canDelete permissions when using the moderator secret and recording access is admin_moderator', async () => { - await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - - const response = await generateRecordingTokenRequest(roomData.room.roomId, roomData.moderatorSecret); - expectValidRecordingTokenResponse(response, roomData.room.roomId, ParticipantRole.MODERATOR, true, true); - }); - - it('should generate a recording token with canRetrieve and canDelete permissions when using the moderator secret and recording access is admin_moderator_speaker', async () => { - await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER); - - const response = await generateRecordingTokenRequest(roomData.room.roomId, roomData.moderatorSecret); - expectValidRecordingTokenResponse(response, roomData.room.roomId, ParticipantRole.MODERATOR, true, true); - }); - - it('should generate a recording token without any permissions when using the speaker secret and recording access is admin_moderator', async () => { - await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - - const response = await generateRecordingTokenRequest(roomData.room.roomId, roomData.speakerSecret); - expectValidRecordingTokenResponse(response, roomData.room.roomId, ParticipantRole.SPEAKER, false, false); - }); - - it('should generate a recording token with canRetrieve permission but not canDelete when using the speaker secret and recording access is admin_moderator_speaker', async () => { - await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER); - - const response = await generateRecordingTokenRequest(roomData.room.roomId, roomData.speakerSecret); - expectValidRecordingTokenResponse(response, roomData.room.roomId, ParticipantRole.SPEAKER, true, false); - }); - - it('should generate a recording token and store it in a cookie when in cookie mode', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Generate the recording token - await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER); - const response = await generateRecordingTokenRequest(roomData.room.roomId, roomData.moderatorSecret); - expectValidRecordingTokenResponse(response, roomData.room.roomId, ParticipantRole.MODERATOR, true, true); - - // Check that the token is included in a cookie - const recordingTokenCookie = extractCookieFromHeaders( - response, - INTERNAL_CONFIG.RECORDING_TOKEN_COOKIE_NAME - ); - expect(recordingTokenCookie).toBeDefined(); - expect(recordingTokenCookie).toContain(response.body.token); - expect(recordingTokenCookie).toContain('HttpOnly'); - expect(recordingTokenCookie).toContain('SameSite=None'); - expect(recordingTokenCookie).toContain('Secure'); - expect(recordingTokenCookie).toContain('Path=/'); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - - it('should fail with a 404 error if the room does not exist', async () => { - const response = await generateRecordingTokenRequest('non-existent-room-id', roomData.moderatorSecret); - expect(response.status).toBe(404); - }); - - it('should fail with a 400 error if the secret is invalid', async () => { - const response = await generateRecordingTokenRequest(roomData.room.roomId, 'invalid-secret'); - expect(response.status).toBe(400); - }); - }); -}); diff --git a/meet-ce/backend/tests/integration/api/rooms/generate-room-member-token.test.ts b/meet-ce/backend/tests/integration/api/rooms/generate-room-member-token.test.ts new file mode 100644 index 00000000..83aa5022 --- /dev/null +++ b/meet-ce/backend/tests/integration/api/rooms/generate-room-member-token.test.ts @@ -0,0 +1,308 @@ +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { + MeetRecordingAccess, + MeetRoomMemberRole, + MeetRoomMemberTokenOptions, + MeetRoomStatus +} from '@openvidu-meet/typings'; +import { expectValidationError, expectValidRoomMemberTokenResponse } from '../../../helpers/assertion-helpers.js'; +import { + deleteAllRooms, + disconnectFakeParticipants, + endMeeting, + generateRoomMemberTokenRequest, + startTestServer, + updateRecordingAccessConfigInRoom, + updateRoomStatus +} from '../../../helpers/request-helpers.js'; +import { RoomData, setupSingleRoom } from '../../../helpers/test-scenarios.js'; + +describe('Room API Tests', () => { + let roomData: RoomData; + let roomId: string; + + beforeAll(async () => { + await startTestServer(); + roomData = await setupSingleRoom(); + roomId = roomData.room.roomId; + }); + + afterAll(async () => { + await disconnectFakeParticipants(); + await deleteAllRooms(); + }); + + describe('Generate Room Member Token Tests', () => { + it('should generate a room member token with moderator permissions when using the moderator secret', async () => { + const response = await generateRoomMemberTokenRequest(roomId, { + secret: roomData.moderatorSecret + }); + expectValidRoomMemberTokenResponse(response, roomId, MeetRoomMemberRole.MODERATOR); + }); + + it('should generate a room member token with speaker permissions when using the speaker secret', async () => { + const response = await generateRoomMemberTokenRequest(roomId, { + secret: roomData.speakerSecret + }); + expectValidRoomMemberTokenResponse(response, roomId, MeetRoomMemberRole.SPEAKER); + }); + + it('should generate a room member token without join meeting permission when not specifying grantJoinMeetingPermission', async () => { + const response = await generateRoomMemberTokenRequest(roomId, { + secret: roomData.moderatorSecret + }); + expectValidRoomMemberTokenResponse(response, roomId, MeetRoomMemberRole.MODERATOR, false); + }); + + it('should generate a room member token with join meeting permission when specifying grantJoinMeetingPermission true and participantName', async () => { + const participantName = 'TEST_PARTICIPANT'; + const response = await generateRoomMemberTokenRequest(roomId, { + secret: roomData.moderatorSecret, + grantJoinMeetingPermission: true, + participantName + }); + expectValidRoomMemberTokenResponse(response, roomId, MeetRoomMemberRole.MODERATOR, true, participantName); + + // End the meeting for further tests + await endMeeting(roomId, roomData.moderatorToken); + }); + + it('should success when when specifying grantJoinMeetingPermission true and participant already exists in the room', async () => { + const participantName = 'TEST_PARTICIPANT'; + + // Create token for the first participant + let response = await generateRoomMemberTokenRequest(roomId, { + secret: roomData.moderatorSecret, + grantJoinMeetingPermission: true, + participantName + }); + expectValidRoomMemberTokenResponse(response, roomId, MeetRoomMemberRole.MODERATOR, true, participantName); + + // Create token for the second participant with the same name + response = await generateRoomMemberTokenRequest(roomId, { + secret: roomData.moderatorSecret, + grantJoinMeetingPermission: true, + participantName + }); + expectValidRoomMemberTokenResponse( + response, + roomId, + MeetRoomMemberRole.MODERATOR, + true, + participantName + '_1' // Participant name should be unique + ); + + // End the meeting for further tests + await endMeeting(roomId, roomData.moderatorToken); + }); + + it('should refresh a room member token with join meeting permission for an existing participant', async () => { + const participantName = 'TEST_PARTICIPANT'; + + // Create room with initial participant + const roomWithParticipant = await setupSingleRoom(true); + + // Refresh token for the participant by specifying participantIdentity + const response = await generateRoomMemberTokenRequest(roomWithParticipant.room.roomId, { + secret: roomWithParticipant.moderatorSecret, + grantJoinMeetingPermission: true, + participantName, + participantIdentity: participantName + }); + expectValidRoomMemberTokenResponse( + response, + roomWithParticipant.room.roomId, + MeetRoomMemberRole.MODERATOR, + true, + participantName, + participantName + ); + }); + + it('should fail with 409 when generating a room member token with join meeting permission and room is closed', async () => { + // Close the room + await updateRoomStatus(roomId, MeetRoomStatus.CLOSED); + + const response = await generateRoomMemberTokenRequest(roomId, { + secret: roomData.moderatorSecret, + grantJoinMeetingPermission: true, + participantName: 'TEST_PARTICIPANT' + }); + expect(response.status).toBe(409); + + // Reopen the room for further tests + await updateRoomStatus(roomId, MeetRoomStatus.OPEN); + }); + + it('should fail with 404 when room does not exist', async () => { + const response = await generateRoomMemberTokenRequest('non-existent-room-id', { + secret: roomData.moderatorSecret + }); + expect(response.status).toBe(404); + }); + + it('should fail with 404 when refreshing token and participant does not exist in the meeting', async () => { + const participantName = 'NON_EXISTENT_PARTICIPANT'; + const response = await generateRoomMemberTokenRequest(roomId, { + secret: roomData.moderatorSecret, + grantJoinMeetingPermission: true, + participantName, + participantIdentity: participantName + }); + expect(response.status).toBe(404); + }); + + it('should fail with 400 when secret is invalid', async () => { + const response = await generateRoomMemberTokenRequest(roomId, { + secret: 'invalid-secret' + }); + expect(response.status).toBe(400); + }); + }); + + describe('Generate Room Member Token Recording Permissions Tests', () => { + afterAll(async () => { + // Reset recording access to default for other tests + await updateRecordingAccessConfigInRoom(roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER); + }); + + it(`should generate a room member token with canRetrieve and canDelete permissions + when using the moderator secret and recording access is admin_moderator_speaker`, async () => { + await updateRecordingAccessConfigInRoom(roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER); + + const response = await generateRoomMemberTokenRequest(roomId, { secret: roomData.moderatorSecret }); + expectValidRoomMemberTokenResponse( + response, + roomId, + MeetRoomMemberRole.MODERATOR, + false, + undefined, + undefined, + true, // canRetrieveRecordings + true // canDeleteRecordings + ); + }); + + it(`should generate a room member token with canRetrieve permission but not canDelete + when using the speaker secret and recording access is admin_moderator_speaker`, async () => { + await updateRecordingAccessConfigInRoom(roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER); + + const response = await generateRoomMemberTokenRequest(roomId, { secret: roomData.speakerSecret }); + expectValidRoomMemberTokenResponse( + response, + roomId, + MeetRoomMemberRole.SPEAKER, + false, + undefined, + undefined, + true, // canRetrieveRecordings + false // canDeleteRecordings + ); + }); + + it(`should generate a room member token with canRetrieve and canDelete permissions + when using the moderator secret and recording access is admin_moderator`, async () => { + await updateRecordingAccessConfigInRoom(roomId, MeetRecordingAccess.ADMIN_MODERATOR); + + const response = await generateRoomMemberTokenRequest(roomId, { secret: roomData.moderatorSecret }); + expectValidRoomMemberTokenResponse( + response, + roomId, + MeetRoomMemberRole.MODERATOR, + false, + undefined, + undefined, + true, // canRetrieveRecordings + true // canDeleteRecordings + ); + }); + + it(`should generate a room member token without any permissions + when using the speaker secret and recording access is admin_moderator`, async () => { + await updateRecordingAccessConfigInRoom(roomId, MeetRecordingAccess.ADMIN_MODERATOR); + + const response = await generateRoomMemberTokenRequest(roomId, { secret: roomData.speakerSecret }); + expectValidRoomMemberTokenResponse( + response, + roomId, + MeetRoomMemberRole.SPEAKER, + false, + undefined, + undefined, + false, // canRetrieveRecordings + false // canDeleteRecordings + ); + }); + + it(`should generate a room member token without any permissions + when using the moderator secret and recording access is admin`, async () => { + await updateRecordingAccessConfigInRoom(roomId, MeetRecordingAccess.ADMIN); + + const response = await generateRoomMemberTokenRequest(roomId, { secret: roomData.moderatorSecret }); + expectValidRoomMemberTokenResponse( + response, + roomId, + MeetRoomMemberRole.MODERATOR, + false, + undefined, + undefined, + false, // canRetrieveRecordings + false // canDeleteRecordings + ); + }); + + it(`should generate a room member token without any permissions + when using the speaker secret and recording access is admin`, async () => { + await updateRecordingAccessConfigInRoom(roomId, MeetRecordingAccess.ADMIN); + + const response = await generateRoomMemberTokenRequest(roomId, { secret: roomData.speakerSecret }); + expectValidRoomMemberTokenResponse( + response, + roomId, + MeetRoomMemberRole.SPEAKER, + false, + undefined, + undefined, + false, // canRetrieveRecordings + false // canDeleteRecordings + ); + }); + }); + + describe('Generate Room Member Token Validation Tests', () => { + it('should fail when secret is not provided', async () => { + const response = await generateRoomMemberTokenRequest( + roomData.room.roomId, + {} as unknown as MeetRoomMemberTokenOptions + ); + expectValidationError(response, 'secret', 'Required'); + }); + + it('should fail when secret is empty', async () => { + const response = await generateRoomMemberTokenRequest(roomData.room.roomId, { + secret: '' + }); + expectValidationError(response, 'secret', 'Secret is required'); + }); + + it('should fail when grantJoinMeetingPermission is not a boolean', async () => { + const response = await generateRoomMemberTokenRequest(roomData.room.roomId, { + secret: roomData.moderatorSecret, + grantJoinMeetingPermission: 'not-a-boolean' as unknown as boolean + }); + expectValidationError(response, 'grantJoinMeetingPermission', 'Expected boolean'); + }); + + it('should fail when grantJoinMeetingPermission is true but participantName is not provided', async () => { + const response = await generateRoomMemberTokenRequest(roomData.room.roomId, { + secret: roomData.moderatorSecret, + grantJoinMeetingPermission: true + }); + expectValidationError( + response, + 'participantName', + 'participantName is required when grantJoinMeetingPermission is true' + ); + }); + }); +}); diff --git a/meet-ce/backend/tests/integration/api/rooms/get-room-roles.test.ts b/meet-ce/backend/tests/integration/api/rooms/get-room-member-roles.test.ts similarity index 51% rename from meet-ce/backend/tests/integration/api/rooms/get-room-roles.test.ts rename to meet-ce/backend/tests/integration/api/rooms/get-room-member-roles.test.ts index ae35860b..d8a3ce26 100644 --- a/meet-ce/backend/tests/integration/api/rooms/get-room-roles.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/get-room-member-roles.test.ts @@ -1,13 +1,13 @@ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; -import { ParticipantRole } from '@openvidu-meet/typings'; +import { MeetRoomMemberRole } from '@openvidu-meet/typings'; import { - expectValidRoomRoleAndPermissionsResponse, - expectValidRoomRolesAndPermissionsResponse + expectValidRoomMemberRoleAndPermissionsResponse, + expectValidRoomMemberRolesAndPermissionsResponse } from '../../../helpers/assertion-helpers.js'; import { deleteAllRooms, - getRoomRoleBySecret, - getRoomRoles, + getRoomMemberRoleBySecret, + getRoomMemberRoles, startTestServer } from '../../../helpers/request-helpers.js'; import { RoomData, setupSingleRoom } from '../../../helpers/test-scenarios.js'; @@ -24,36 +24,40 @@ describe('Room API Tests', () => { await deleteAllRooms(); }); - describe('Get Room Roles Tests', () => { + describe('Get Room Member Roles Tests', () => { it('should retrieve all roles and associated permissions for a room', async () => { - const response = await getRoomRoles(roomData.room.roomId); - expectValidRoomRolesAndPermissionsResponse(response, roomData.room.roomId); + const response = await getRoomMemberRoles(roomData.room.roomId); + expectValidRoomMemberRolesAndPermissionsResponse(response, roomData.room.roomId); }); it('should return a 404 error if the room does not exist', async () => { - const response = await getRoomRoles('non-existent-room-id'); + const response = await getRoomMemberRoles('non-existent-room-id'); expect(response.status).toBe(404); }); }); - describe('Get Room Role Tests', () => { + describe('Get Room Member Role Tests', () => { it('should retrieve moderator role and associated permissions for a room with a valid moderator secret', async () => { - const response = await getRoomRoleBySecret(roomData.room.roomId, roomData.moderatorSecret); - expectValidRoomRoleAndPermissionsResponse(response, roomData.room.roomId, ParticipantRole.MODERATOR); + const response = await getRoomMemberRoleBySecret(roomData.room.roomId, roomData.moderatorSecret); + expectValidRoomMemberRoleAndPermissionsResponse( + response, + roomData.room.roomId, + MeetRoomMemberRole.MODERATOR + ); }); it('should retrieve speaker role and associated permissions for a room with a valid speaker secret', async () => { - const response = await getRoomRoleBySecret(roomData.room.roomId, roomData.speakerSecret); - expectValidRoomRoleAndPermissionsResponse(response, roomData.room.roomId, ParticipantRole.SPEAKER); + const response = await getRoomMemberRoleBySecret(roomData.room.roomId, roomData.speakerSecret); + expectValidRoomMemberRoleAndPermissionsResponse(response, roomData.room.roomId, MeetRoomMemberRole.SPEAKER); }); it('should return a 404 error if the room does not exist', async () => { - const response = await getRoomRoleBySecret('non-existent-room-id', roomData.moderatorSecret); + const response = await getRoomMemberRoleBySecret('non-existent-room-id', roomData.moderatorSecret); expect(response.status).toBe(404); }); it('should return a 400 error if the secret is invalid', async () => { - const response = await getRoomRoleBySecret(roomData.room.roomId, 'invalid-secret'); + const response = await getRoomMemberRoleBySecret(roomData.room.roomId, 'invalid-secret'); expect(response.status).toBe(400); }); }); diff --git a/meet-ce/backend/tests/integration/api/rooms/get-room.test.ts b/meet-ce/backend/tests/integration/api/rooms/get-room.test.ts index 69246c22..8d752676 100644 --- a/meet-ce/backend/tests/integration/api/rooms/get-room.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/get-room.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeAll, describe, expect, it } from '@jest/globals'; +import { MeetRecordingAccess } from '@openvidu-meet/typings'; import ms from 'ms'; -import { MeetRecordingAccess, ParticipantRole } from'@openvidu-meet/typings'; import { expectSuccessRoomResponse, expectValidationError, @@ -100,12 +100,7 @@ describe('Room API Tests', () => { it('should retrieve a room without moderatorUrl when participant is speaker', async () => { const roomData = await setupSingleRoom(); - const response = await getRoom( - roomData.room.roomId, - undefined, - roomData.speakerToken, - ParticipantRole.SPEAKER - ); + const response = await getRoom(roomData.room.roomId, undefined, roomData.speakerToken); expect(response.status).toBe(200); expect(response.body.moderatorUrl).toBeUndefined(); }); diff --git a/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts b/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts index 379b0d13..c860a794 100644 --- a/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts @@ -5,12 +5,11 @@ import { FrontendEventService } from '../../../../src/services/index.js'; import { createRoom, deleteAllRooms, - disconnectFakeParticipants, getRoom, startTestServer, updateRoomConfig } from '../../../helpers/request-helpers.js'; -import { RoomData, setupSingleRoom } from '../../../helpers/test-scenarios.js'; +import { setupSingleRoom } from '../../../helpers/test-scenarios.js'; describe('Room API Tests', () => { beforeAll(async () => { @@ -117,34 +116,9 @@ describe('Room API Tests', () => { expect(getResponse.body.config).toEqual(partialConfig); }); - it('should return 404 when updating non-existent room', async () => { - const nonExistentRoomId = 'non-existent-room'; - - const config = { - recording: { - enabled: false, - allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER - }, - chat: { enabled: false }, - virtualBackground: { enabled: false } - }; - const response = await updateRoomConfig(nonExistentRoomId, config); - - expect(response.status).toBe(404); - expect(response.body.message).toContain(`'${nonExistentRoomId}' does not exist`); - }); - }); - - describe('Update Room Config with Active Meeting Tests', () => { - let roomData: RoomData; - - afterEach(async () => { - await disconnectFakeParticipants(); - }); - it('should reject room config update when there is an active meeting', async () => { - // Create a room and start a meeting - roomData = await setupSingleRoom(true); + // Create a room with active meeting + const roomData = await setupSingleRoom(true); // Try to update room config const newConfig = { @@ -163,12 +137,27 @@ describe('Room API Tests', () => { }; const response = await updateRoomConfig(roomData.room.roomId, newConfig); - - // Should return 409 Conflict expect(response.status).toBe(409); expect(response.body.error).toBe('Room Error'); expect(response.body.message).toContain(`Room '${roomData.room.roomId}' has an active meeting`); }); + + it('should return 404 when updating non-existent room', async () => { + const nonExistentRoomId = 'non-existent-room'; + + const config = { + recording: { + enabled: false, + allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER + }, + chat: { enabled: false }, + virtualBackground: { enabled: false } + }; + const response = await updateRoomConfig(nonExistentRoomId, config); + + expect(response.status).toBe(404); + expect(response.body.message).toContain(`'${nonExistentRoomId}' does not exist`); + }); }); describe('Update Room Config Validation failures', () => { diff --git a/meet-ce/backend/tests/integration/api/rooms/update-room-status.test.ts b/meet-ce/backend/tests/integration/api/rooms/update-room-status.test.ts index 54b6850e..35ad4431 100644 --- a/meet-ce/backend/tests/integration/api/rooms/update-room-status.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/update-room-status.test.ts @@ -9,6 +9,7 @@ import { updateRoomStatus } from '../../../helpers/request-helpers.js'; import { setupSingleRoom } from '../../../helpers/test-scenarios.js'; +import { MeetRoomStatus } from '@openvidu-meet/typings'; describe('Room API Tests', () => { beforeAll(async () => { @@ -27,7 +28,7 @@ describe('Room API Tests', () => { }); // Update the room status - const response = await updateRoomStatus(createdRoom.roomId, 'open'); + const response = await updateRoomStatus(createdRoom.roomId, MeetRoomStatus.OPEN); expect(response.status).toBe(200); expect(response.body).toHaveProperty('message'); @@ -43,7 +44,7 @@ describe('Room API Tests', () => { }); // Update the room status - const response = await updateRoomStatus(createdRoom.roomId, 'closed'); + const response = await updateRoomStatus(createdRoom.roomId, MeetRoomStatus.CLOSED); expect(response.status).toBe(200); expect(response.body).toHaveProperty('message'); @@ -57,7 +58,7 @@ describe('Room API Tests', () => { const roomData = await setupSingleRoom(true); // Update the room status - const response = await updateRoomStatus(roomData.room.roomId, 'closed'); + const response = await updateRoomStatus(roomData.room.roomId, MeetRoomStatus.CLOSED); expect(response.status).toBe(202); expect(response.body).toHaveProperty('message'); @@ -79,7 +80,7 @@ describe('Room API Tests', () => { it('should fail with 404 when updating non-existent room', async () => { const nonExistentRoomId = 'non-existent-room'; - const response = await updateRoomStatus(nonExistentRoomId, 'closed'); + const response = await updateRoomStatus(nonExistentRoomId, MeetRoomStatus.CLOSED); expect(response.status).toBe(404); expect(response.body.message).toContain(`'${nonExistentRoomId}' does not exist`); @@ -87,17 +88,27 @@ describe('Room API Tests', () => { }); describe('Update Room Status Validation failures', () => { - it('should fail when status is invalid', async () => { - const { roomId } = await createRoom({ - roomName: 'validation-test' - }); + let roomId: string; - // Invalid status - const response = await updateRoomStatus(roomId, 'invalid_status'); + beforeAll(async () => { + const room = await createRoom(); + roomId = room.roomId; + }); + + it('should fail when status is invalid', async () => { + const response = await updateRoomStatus(roomId, 'invalid_status' as MeetRoomStatus); expect(response.status).toBe(422); expect(response.body.error).toContain('Unprocessable Entity'); expect(JSON.stringify(response.body.details)).toContain('Invalid enum value'); }); + + it('should fail when status is active_meeting', async () => { + const response = await updateRoomStatus(roomId, MeetRoomStatus.ACTIVE_MEETING); + + expect(response.status).toBe(422); + expect(response.body.error).toContain('Unprocessable Entity'); + expect(JSON.stringify(response.body.details)).toContain("Invalid enum value. Expected 'open' | 'closed'"); + }); }); }); diff --git a/meet-ce/backend/tests/integration/api/security/analytics-security.test.ts b/meet-ce/backend/tests/integration/api/security/analytics-security.test.ts index 2e76d53c..764a94ee 100644 --- a/meet-ce/backend/tests/integration/api/security/analytics-security.test.ts +++ b/meet-ce/backend/tests/integration/api/security/analytics-security.test.ts @@ -1,10 +1,9 @@ import { beforeAll, describe, expect, it } from '@jest/globals'; -import { AuthTransportMode } from '@openvidu-meet/typings'; 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 { changeAuthTransportMode, loginUser, startTestServer } from '../../../helpers/request-helpers.js'; +import { loginUser, startTestServer } from '../../../helpers/request-helpers.js'; const ANALYTICS_PATH = `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/analytics`; @@ -32,20 +31,6 @@ describe('Analytics API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when user is authenticated as admin and token is sent in cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login again to get token in cookie - const newAdminAccessToken = await loginUser(); - - const response = await request(app).get(ANALYTICS_PATH).set('Cookie', newAdminAccessToken); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).get(ANALYTICS_PATH); expect(response.status).toBe(401); diff --git a/meet-ce/backend/tests/integration/api/security/api-key-security.test.ts b/meet-ce/backend/tests/integration/api/security/api-key-security.test.ts index 2c3c6086..69481cfb 100644 --- a/meet-ce/backend/tests/integration/api/security/api-key-security.test.ts +++ b/meet-ce/backend/tests/integration/api/security/api-key-security.test.ts @@ -1,15 +1,8 @@ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; -import { AuthTransportMode } from '@openvidu-meet/typings'; import { Express } from 'express'; import request from 'supertest'; import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js'; -import { - changeAuthTransportMode, - generateApiKey, - loginUser, - restoreDefaultApiKeys, - startTestServer -} from '../../../helpers/request-helpers.js'; +import { generateApiKey, loginUser, restoreDefaultApiKeys, startTestServer } from '../../../helpers/request-helpers.js'; const API_KEYS_PATH = `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/api-keys`; @@ -34,19 +27,6 @@ describe('API Keys API Security Tests', () => { expect(response.status).toBe(201); }); - it('should succeed when user is authenticated as admin and token is sent in cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - await request(app).post(`${API_KEYS_PATH}`).set('Cookie', adminCookie).expect(201); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).post(`${API_KEYS_PATH}`); expect(response.status).toBe(401); @@ -61,19 +41,6 @@ describe('API Keys API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when user is authenticated as admin and token is sent in cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - await request(app).get(`${API_KEYS_PATH}`).set('Cookie', adminCookie).expect(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).get(`${API_KEYS_PATH}`); expect(response.status).toBe(401); @@ -93,19 +60,6 @@ describe('API Keys API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when user is authenticated as admin and token is sent in cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - await request(app).delete(`${API_KEYS_PATH}`).set('Cookie', adminCookie).expect(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).delete(`${API_KEYS_PATH}`); expect(response.status).toBe(401); diff --git a/meet-ce/backend/tests/integration/api/security/global-config-security.test.ts b/meet-ce/backend/tests/integration/api/security/global-config-security.test.ts index 7dc5514b..f29a0b37 100644 --- a/meet-ce/backend/tests/integration/api/security/global-config-security.test.ts +++ b/meet-ce/backend/tests/integration/api/security/global-config-security.test.ts @@ -1,15 +1,10 @@ import { beforeAll, describe, expect, it } from '@jest/globals'; -import { AuthMode, AuthTransportMode, AuthType, MeetRoomThemeMode } from '@openvidu-meet/typings'; +import { AuthMode, AuthType, MeetRoomThemeMode } from '@openvidu-meet/typings'; 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 { - changeAuthTransportMode, - loginUser, - restoreDefaultGlobalConfig, - startTestServer -} from '../../../helpers/request-helpers.js'; +import { loginUser, restoreDefaultGlobalConfig, startTestServer } from '../../../helpers/request-helpers.js'; const CONFIG_PATH = `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/config`; @@ -46,23 +41,6 @@ describe('Global Config API Security Tests', () => { await restoreDefaultGlobalConfig(); }); - it('should succeed when user is authenticated as admin in cookie mode', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - const response = await request(app) - .put(`${CONFIG_PATH}/webhooks`) - .set('Cookie', adminCookie) - .send(webhookConfig); - expect(response.status).toBe(200); - - // This method already restores the config to default (header mode) - await restoreDefaultGlobalConfig(); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).put(`${CONFIG_PATH}/webhooks`).send(webhookConfig); expect(response.status).toBe(401); @@ -84,20 +62,6 @@ describe('Global Config API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when user is authenticated as admin in cookie mode', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - const response = await request(app).get(`${CONFIG_PATH}/webhooks`).set('Cookie', adminCookie); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).get(`${CONFIG_PATH}/webhooks`); expect(response.status).toBe(401); @@ -110,7 +74,6 @@ describe('Global Config API Security Tests', () => { authMethod: { type: AuthType.SINGLE_USER }, - authTransportMode: AuthTransportMode.HEADER, authModeToAccessRoom: AuthMode.ALL_USERS } }; @@ -133,23 +96,6 @@ describe('Global Config API Security Tests', () => { await restoreDefaultGlobalConfig(); }); - it('should succeed when user is authenticated as admin in cookie mode', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - const response = await request(app) - .put(`${CONFIG_PATH}/security`) - .set('Cookie', adminCookie) - .send(securityConfig); - expect(response.status).toBe(200); - - // This method already restores the config to default (header mode) - await restoreDefaultGlobalConfig(); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).put(`${CONFIG_PATH}/security`).send(securityConfig); expect(response.status).toBe(401); @@ -194,22 +140,6 @@ describe('Global Config API Security Tests', () => { await restoreDefaultGlobalConfig(); }); - it('should succeed when user is authenticated as admin in cookie mode', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - const response = await request(app) - .put(`${CONFIG_PATH}/rooms/appearance`) - .set('Cookie', adminCookie) - .send(appearanceConfig); - expect(response.status).toBe(200); - - await restoreDefaultGlobalConfig(); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).put(`${CONFIG_PATH}/rooms/appearance`).send(appearanceConfig); expect(response.status).toBe(401); diff --git a/meet-ce/backend/tests/integration/api/security/meeting-security.test.ts b/meet-ce/backend/tests/integration/api/security/meeting-security.test.ts index 33249a9d..3ffaa529 100644 --- a/meet-ce/backend/tests/integration/api/security/meeting-security.test.ts +++ b/meet-ce/backend/tests/integration/api/security/meeting-security.test.ts @@ -1,12 +1,11 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; +import { MeetRoomMemberRole, MeetRoomMemberTokenMetadata } from '@openvidu-meet/typings'; import { Express } from 'express'; import request from 'supertest'; import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js'; import { LIVEKIT_URL, MEET_INITIAL_API_KEY } from '../../../../src/environment.js'; -import { AuthTransportMode, MeetTokenMetadata, ParticipantRole } from '@openvidu-meet/typings'; import { getPermissions } from '../../../helpers/assertion-helpers.js'; import { - changeAuthTransportMode, deleteAllRooms, disconnectFakeParticipants, loginUser, @@ -54,61 +53,36 @@ describe('Meeting API Security Tests', () => { it('should succeed when participant is moderator', async () => { const response = await request(app) .delete(`${MEETINGS_PATH}/${roomData.room.roomId}`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.moderatorToken); expect(response.status).toBe(200); }); - it('should succeed when participant is moderator and token is sent in cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Create a new room to obtain participant token in cookie mode - const newRoomData = await setupSingleRoom(true); - - const response = await request(app) - .delete(`${MEETINGS_PATH}/${newRoomData.room.roomId}`) - .set('Cookie', newRoomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when participant is moderator of a different room', async () => { const newRoomData = await setupSingleRoom(); const response = await request(app) .delete(`${MEETINGS_PATH}/${roomData.room.roomId}`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, newRoomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); + .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) .delete(`${MEETINGS_PATH}/${roomData.room.roomId}`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.speakerToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.SPEAKER); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.speakerToken); expect(response.status).toBe(403); }); }); describe('Update Participant in Meeting Tests', () => { const PARTICIPANT_NAME = 'TEST_PARTICIPANT'; - const role = ParticipantRole.MODERATOR; + const role = MeetRoomMemberRole.MODERATOR; beforeEach(async () => { - const metadata: MeetTokenMetadata = { + const metadata: MeetRoomMemberTokenMetadata = { livekitUrl: LIVEKIT_URL, - roles: [ - { - role: ParticipantRole.SPEAKER, - permissions: getPermissions(roomData.room.roomId, ParticipantRole.SPEAKER).openvidu - } - ], - selectedRole: ParticipantRole.SPEAKER + role: MeetRoomMemberRole.SPEAKER, + permissions: getPermissions(roomData.room.roomId, MeetRoomMemberRole.SPEAKER, true, true).meet }; await updateParticipantMetadata(roomData.room.roomId, PARTICIPANT_NAME, metadata); }); @@ -132,47 +106,17 @@ describe('Meeting API Security Tests', () => { it('should succeed when participant is moderator', async () => { const response = await request(app) .put(`${MEETINGS_PATH}/${roomData.room.roomId}/participants/${PARTICIPANT_NAME}/role`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR) + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.moderatorToken) .send({ role }); expect(response.status).toBe(200); }); - it('should succeed when participant is moderator and token is sent in cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Create a new room to obtain participant token in cookie mode - const newRoomData = await setupSingleRoom(true); - await updateParticipantMetadata(newRoomData.room.roomId, PARTICIPANT_NAME, { - livekitUrl: LIVEKIT_URL, - roles: [ - { - role: ParticipantRole.SPEAKER, - permissions: getPermissions(newRoomData.room.roomId, ParticipantRole.SPEAKER).openvidu - } - ], - selectedRole: ParticipantRole.SPEAKER - }); - - const response = await request(app) - .put(`${MEETINGS_PATH}/${newRoomData.room.roomId}/participants/${PARTICIPANT_NAME}/role`) - .set('Cookie', newRoomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR) - .send({ role }); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when participant is moderator of a different room', async () => { const newRoomData = await setupSingleRoom(); const response = await request(app) .put(`${MEETINGS_PATH}/${roomData.room.roomId}/participants/${PARTICIPANT_NAME}/role`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, newRoomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR) + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, newRoomData.moderatorToken) .send({ role }); expect(response.status).toBe(403); }); @@ -180,8 +124,7 @@ describe('Meeting API Security Tests', () => { it('should fail when participant is speaker', async () => { const response = await request(app) .put(`${MEETINGS_PATH}/${roomData.room.roomId}/participants/${PARTICIPANT_NAME}/role`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.speakerToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.SPEAKER) + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.speakerToken) .send({ role }); expect(response.status).toBe(403); }); @@ -207,43 +150,23 @@ describe('Meeting API Security Tests', () => { it('should succeed when participant is moderator', async () => { const response = await request(app) .delete(`${MEETINGS_PATH}/${roomData.room.roomId}/participants/${PARTICIPANT_IDENTITY}`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.moderatorToken); expect(response.status).toBe(200); }); - it('should succeed when participant is moderator and token is sent in cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Create a new room to obtain participant token in cookie mode - const newRoomData = await setupSingleRoom(true); - - const response = await request(app) - .delete(`${MEETINGS_PATH}/${newRoomData.room.roomId}/participants/${PARTICIPANT_IDENTITY}`) - .set('Cookie', newRoomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when participant is moderator of a different room', async () => { const newRoomData = await setupSingleRoom(); const response = await request(app) .delete(`${MEETINGS_PATH}/${roomData.room.roomId}/participants/${PARTICIPANT_IDENTITY}`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, newRoomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); + .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) .delete(`${MEETINGS_PATH}/${roomData.room.roomId}/participants/${PARTICIPANT_IDENTITY}`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.speakerToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.SPEAKER); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.speakerToken); expect(response.status).toBe(403); }); }); diff --git a/meet-ce/backend/tests/integration/api/security/participant-security.test.ts b/meet-ce/backend/tests/integration/api/security/participant-security.test.ts deleted file mode 100644 index 08d4c7c4..00000000 --- a/meet-ce/backend/tests/integration/api/security/participant-security.test.ts +++ /dev/null @@ -1,348 +0,0 @@ -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 { AuthMode, AuthTransportMode } from '@openvidu-meet/typings'; -import { - changeAuthTransportMode, - changeSecurityConfig, - deleteAllRooms, - disconnectFakeParticipants, - loginUser, - sleep, - startTestServer -} from '../../../helpers/request-helpers.js'; -import { RoomData, setupSingleRoom } from '../../../helpers/test-scenarios.js'; - -const PARTICIPANTS_PATH = `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/participants`; - -describe('Participant API Security Tests', () => { - const PARTICIPANT_NAME = 'TEST_PARTICIPANT'; - - let app: Express; - let adminAccessToken: string; - - beforeAll(async () => { - app = await startTestServer(); - adminAccessToken = await loginUser(); - }); - - afterAll(async () => { - await disconnectFakeParticipants(); - await deleteAllRooms(); - }); - - describe('Generate Participant Token Tests', () => { - let roomData: RoomData; - - beforeAll(async () => { - roomData = await setupSingleRoom(); - }); - - it('should succeed when no authentication is required and participant is speaker', async () => { - await changeSecurityConfig(AuthMode.NONE); - - const response = await request(app).post(`${PARTICIPANTS_PATH}/token`).send({ - roomId: roomData.room.roomId, - secret: roomData.speakerSecret, - participantName: PARTICIPANT_NAME - }); - expect(response.status).toBe(200); - }); - - it('should succeed when no authentication is required and participant is moderator', async () => { - await changeSecurityConfig(AuthMode.NONE); - - const response = await request(app).post(`${PARTICIPANTS_PATH}/token`).send({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName: PARTICIPANT_NAME - }); - expect(response.status).toBe(200); - }); - - it('should succeed when authentication is required for moderator and participant is speaker', async () => { - await changeSecurityConfig(AuthMode.MODERATORS_ONLY); - - const response = await request(app).post(`${PARTICIPANTS_PATH}/token`).send({ - roomId: roomData.room.roomId, - secret: roomData.speakerSecret, - participantName: PARTICIPANT_NAME - }); - expect(response.status).toBe(200); - }); - - it('should succeed when authentication is required for moderator, participant is moderator and authenticated', async () => { - await changeSecurityConfig(AuthMode.MODERATORS_ONLY); - - const response = await request(app) - .post(`${PARTICIPANTS_PATH}/token`) - .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, adminAccessToken) - .send({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName: PARTICIPANT_NAME - }); - expect(response.status).toBe(200); - }); - - it('should succeed when authentication is required for moderator, participant is moderator and authenticated via cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - const response = await request(app).post(`${PARTICIPANTS_PATH}/token`).set('Cookie', adminCookie).send({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName: PARTICIPANT_NAME - }); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - - it('should fail when authentication is required for moderator and participant is moderator but not authenticated', async () => { - await changeSecurityConfig(AuthMode.MODERATORS_ONLY); - - const response = await request(app).post(`${PARTICIPANTS_PATH}/token`).send({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName: PARTICIPANT_NAME - }); - expect(response.status).toBe(401); - }); - - it('should succeed when authentication is required for all users, participant is speaker and authenticated', async () => { - await changeSecurityConfig(AuthMode.ALL_USERS); - - const response = await request(app) - .post(`${PARTICIPANTS_PATH}/token`) - .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, adminAccessToken) - .send({ - roomId: roomData.room.roomId, - secret: roomData.speakerSecret, - participantName: PARTICIPANT_NAME - }); - expect(response.status).toBe(200); - }); - - it('should fail when authentication is required for all users and participant is speaker but not authenticated', async () => { - await changeSecurityConfig(AuthMode.ALL_USERS); - - const response = await request(app).post(`${PARTICIPANTS_PATH}/token`).send({ - roomId: roomData.room.roomId, - secret: roomData.speakerSecret, - participantName: PARTICIPANT_NAME - }); - expect(response.status).toBe(401); - }); - - it('should succeed when authentication is required for all users, participant is moderator and authenticated', async () => { - await changeSecurityConfig(AuthMode.ALL_USERS); - - const response = await request(app) - .post(`${PARTICIPANTS_PATH}/token`) - .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, adminAccessToken) - .send({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName: PARTICIPANT_NAME - }); - expect(response.status).toBe(200); - }); - - it('should fail when authentication is required for all users and participant is moderator but not authenticated', async () => { - await changeSecurityConfig(AuthMode.ALL_USERS); - - const response = await request(app).post(`${PARTICIPANTS_PATH}/token`).send({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName: PARTICIPANT_NAME - }); - expect(response.status).toBe(401); - }); - }); - - describe('Refresh Participant Token Tests', () => { - let roomData: RoomData; - - beforeAll(async () => { - // Set short expiration for testing - const initialTokenExpiration = INTERNAL_CONFIG.PARTICIPANT_TOKEN_EXPIRATION; - INTERNAL_CONFIG.PARTICIPANT_TOKEN_EXPIRATION = '1s'; - - roomData = await setupSingleRoom(true); - await sleep('2s'); // Ensure the token is expired - - // Restore original expiration after setup - INTERNAL_CONFIG.PARTICIPANT_TOKEN_EXPIRATION = initialTokenExpiration; - }); - - it('should succeed when no authentication is required and participant is speaker', async () => { - await changeSecurityConfig(AuthMode.NONE); - - const response = await request(app) - .post(`${PARTICIPANTS_PATH}/token/refresh`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.speakerToken) - .send({ - roomId: roomData.room.roomId, - secret: roomData.speakerSecret, - participantName: PARTICIPANT_NAME, - participantIdentity: PARTICIPANT_NAME - }); - expect(response.status).toBe(200); - }); - - it('should succeed when no authentication is required and participant is moderator', async () => { - await changeSecurityConfig(AuthMode.NONE); - - const response = await request(app) - .post(`${PARTICIPANTS_PATH}/token/refresh`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.moderatorToken) - .send({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName: PARTICIPANT_NAME, - participantIdentity: PARTICIPANT_NAME - }); - expect(response.status).toBe(200); - }); - - it('should succeed when authentication is required for moderator and participant is speaker', async () => { - await changeSecurityConfig(AuthMode.MODERATORS_ONLY); - - const response = await request(app) - .post(`${PARTICIPANTS_PATH}/token/refresh`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.speakerToken) - .send({ - roomId: roomData.room.roomId, - secret: roomData.speakerSecret, - participantName: PARTICIPANT_NAME, - participantIdentity: PARTICIPANT_NAME - }); - expect(response.status).toBe(200); - }); - - it('should succeed when authentication is required for moderator, participant is moderator and authenticated', async () => { - await changeSecurityConfig(AuthMode.MODERATORS_ONLY); - - const response = await request(app) - .post(`${PARTICIPANTS_PATH}/token/refresh`) - .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, adminAccessToken) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.moderatorToken) - .send({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName: PARTICIPANT_NAME, - participantIdentity: PARTICIPANT_NAME - }); - expect(response.status).toBe(200); - }); - - it('should succeed when authentication is required for moderator, participant is moderator and authenticated via cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - // Create a new room to obtain participant token in cookie mode - const newRoomData = await setupSingleRoom(true); - - const response = await request(app) - .post(`${PARTICIPANTS_PATH}/token/refresh`) - .set('Cookie', adminCookie) - .set('Cookie', newRoomData.moderatorToken) - .send({ - roomId: newRoomData.room.roomId, - secret: newRoomData.moderatorSecret, - participantName: PARTICIPANT_NAME, - participantIdentity: PARTICIPANT_NAME - }); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - - it('should fail when authentication is required for moderator and participant is moderator but not authenticated', async () => { - await changeSecurityConfig(AuthMode.MODERATORS_ONLY); - - const response = await request(app) - .post(`${PARTICIPANTS_PATH}/token/refresh`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.moderatorToken) - .send({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName: PARTICIPANT_NAME, - participantIdentity: PARTICIPANT_NAME - }); - expect(response.status).toBe(401); - }); - - it('should succeed when authentication is required for all users, participant is speaker and authenticated', async () => { - await changeSecurityConfig(AuthMode.ALL_USERS); - - const response = await request(app) - .post(`${PARTICIPANTS_PATH}/token/refresh`) - .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, adminAccessToken) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.speakerToken) - .send({ - roomId: roomData.room.roomId, - secret: roomData.speakerSecret, - participantName: PARTICIPANT_NAME, - participantIdentity: PARTICIPANT_NAME - }); - expect(response.status).toBe(200); - }); - - it('should fail when authentication is required for all users and participant is speaker but not authenticated', async () => { - await changeSecurityConfig(AuthMode.ALL_USERS); - - const response = await request(app) - .post(`${PARTICIPANTS_PATH}/token/refresh`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.speakerToken) - .send({ - roomId: roomData.room.roomId, - secret: roomData.speakerSecret, - participantName: PARTICIPANT_NAME, - participantIdentity: PARTICIPANT_NAME - }); - expect(response.status).toBe(401); - }); - - it('should succeed when authentication is required for all users, participant is moderator and authenticated', async () => { - await changeSecurityConfig(AuthMode.ALL_USERS); - - const response = await request(app) - .post(`${PARTICIPANTS_PATH}/token/refresh`) - .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, adminAccessToken) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.moderatorToken) - .send({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName: PARTICIPANT_NAME, - participantIdentity: PARTICIPANT_NAME - }); - expect(response.status).toBe(200); - }); - - it('should fail when authentication is required for all users and participant is moderator but not authenticated', async () => { - await changeSecurityConfig(AuthMode.ALL_USERS); - - const response = await request(app) - .post(`${PARTICIPANTS_PATH}/token/refresh`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.moderatorToken) - .send({ - roomId: roomData.room.roomId, - secret: roomData.moderatorSecret, - participantName: PARTICIPANT_NAME, - participantIdentity: PARTICIPANT_NAME - }); - expect(response.status).toBe(401); - }); - }); -}); diff --git a/meet-ce/backend/tests/integration/api/security/recording-security.test.ts b/meet-ce/backend/tests/integration/api/security/recording-security.test.ts index 10d33fd4..56b15e29 100644 --- a/meet-ce/backend/tests/integration/api/security/recording-security.test.ts +++ b/meet-ce/backend/tests/integration/api/security/recording-security.test.ts @@ -1,16 +1,16 @@ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { MeetRecordingAccess } from '@openvidu-meet/typings'; 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 { AuthTransportMode, MeetRecordingAccess, ParticipantRole } from '@openvidu-meet/typings'; import { expectValidStopRecordingResponse } from '../../../helpers/assertion-helpers.js'; import { - changeAuthTransportMode, deleteAllRecordings, deleteAllRooms, disconnectFakeParticipants, - generateRecordingToken, + endMeeting, + generateRoomMemberToken, getRecordingUrl, loginUser, startTestServer, @@ -64,8 +64,7 @@ describe('Recording API Security Tests', () => { const response = await request(app) .post(INTERNAL_RECORDINGS_PATH) .send({ roomId: roomData.room.roomId }) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.moderatorToken); expect(response.status).toBe(201); // Stop recording to clean up @@ -74,42 +73,13 @@ describe('Recording API Security Tests', () => { expectValidStopRecordingResponse(stopResponse, recordingId, roomData.room.roomId, roomData.room.roomName); }); - it('should succeed when participant is moderator and token is sent in cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Create a new room to obtain participant token in cookie mode - const newRoomData = await setupSingleRoom(true); - - const response = await request(app) - .post(INTERNAL_RECORDINGS_PATH) - .send({ roomId: newRoomData.room.roomId }) - .set('Cookie', newRoomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); - expect(response.status).toBe(201); - - // Stop recording to clean up - const recordingId = response.body.recordingId; - const stopResponse = await stopRecording(recordingId, newRoomData.moderatorToken); - expectValidStopRecordingResponse( - stopResponse, - recordingId, - newRoomData.room.roomId, - newRoomData.room.roomName - ); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - 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) .send({ roomId: roomData.room.roomId }) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, newRoomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, newRoomData.moderatorToken); expect(response.status).toBe(403); }); @@ -117,8 +87,7 @@ describe('Recording API Security Tests', () => { const response = await request(app) .post(INTERNAL_RECORDINGS_PATH) .send({ roomId: roomData.room.roomId }) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.speakerToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.SPEAKER); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.speakerToken); expect(response.status).toBe(403); }); }); @@ -152,43 +121,23 @@ describe('Recording API Security Tests', () => { it('should succeed when participant is moderator', async () => { const response = await request(app) .post(`${INTERNAL_RECORDINGS_PATH}/${roomData.recordingId}/stop`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.moderatorToken); expect(response.status).toBe(202); }); - it('should succeed when participant is moderator and token is sent in cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Create a new room to obtain participant token in cookie mode - const newRoomData = await setupSingleRoomWithRecording(); - - const response = await request(app) - .post(`${INTERNAL_RECORDINGS_PATH}/${newRoomData.recordingId}/stop`) - .set('Cookie', newRoomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); - expect(response.status).toBe(202); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - 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`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, newRoomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); + .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`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.speakerToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.SPEAKER); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.speakerToken); expect(response.status).toBe(403); }); }); @@ -200,6 +149,9 @@ describe('Recording API Security Tests', () => { beforeAll(async () => { roomData = await setupSingleRoomWithRecording(true); recordingId = roomData.recordingId!; + + // End the meeting to be able to update the room config + await endMeeting(roomData.room.roomId, roomData.moderatorToken); }); describe('Get Recordings Tests', () => { @@ -217,66 +169,57 @@ describe('Recording API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when recording access is admin_moderator_speaker and participant is speaker', async () => { + it('should succeed when recording access is admin_moderator_speaker and user is speaker', async () => { await updateRecordingAccessConfigInRoom( roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.speakerSecret + }); const response = await request(app) .get(RECORDINGS_PATH) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(200); }); - it('should succeed when recording access is admin_moderator_speaker and participant is speaker, token in cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - + it('should succeed when recording access is admin_moderator_speaker and user is moderator', async () => { await updateRecordingAccessConfigInRoom( roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); - - const response = await request(app).get(RECORDINGS_PATH).set('Cookie', recordingToken); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - - it('should succeed when recording access is admin_moderator_speaker and participant is moderator', async () => { - await updateRecordingAccessConfigInRoom( - roomData.room.roomId, - MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER - ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.moderatorSecret + }); const response = await request(app) .get(RECORDINGS_PATH) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(200); }); - it('should fail when recording access is admin_moderator and participant is speaker', async () => { + it('should fail when recording access is admin_moderator and user is speaker', async () => { await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.speakerSecret + }); const response = await request(app) .get(RECORDINGS_PATH) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(403); }); - it('should succeed when recording access is admin_moderator and participant is moderator', async () => { + it('should succeed when recording access is admin_moderator and user is moderator', async () => { await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.moderatorSecret + }); const response = await request(app) .get(RECORDINGS_PATH) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(200); }); }); @@ -296,66 +239,57 @@ describe('Recording API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when recording access is admin_moderator_speaker and participant is speaker', async () => { + it('should succeed when recording access is admin_moderator_speaker and user is speaker', async () => { await updateRecordingAccessConfigInRoom( roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.speakerSecret + }); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}`) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(200); }); - it('should succeed when recording access is admin_moderator_speaker and participant is speaker, token in cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - + it('should succeed when recording access is admin_moderator_speaker and user is moderator', async () => { await updateRecordingAccessConfigInRoom( roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); - - const response = await request(app).get(RECORDINGS_PATH).set('Cookie', recordingToken); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - - it('should succeed when recording access is admin_moderator_speaker and participant is moderator', async () => { - await updateRecordingAccessConfigInRoom( - roomData.room.roomId, - MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER - ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.moderatorSecret + }); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}`) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(200); }); - it('should fail when recording access is admin_moderator and participant is speaker', async () => { + it('should fail when recording access is admin_moderator and user is speaker', async () => { await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.speakerSecret + }); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}`) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(403); }); - it('should succeed when recording access is admin_moderator and participant is moderator', async () => { + it('should succeed when recording access is admin_moderator and user is moderator', async () => { await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.moderatorSecret + }); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}`) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(200); }); @@ -430,68 +364,57 @@ describe('Recording API Security Tests', () => { expect(response.status).toBe(404); }); - it('should fail when recording access is admin_moderator_speaker and participant is speaker', async () => { + it('should fail when recording access is admin_moderator_speaker and user is speaker', async () => { await updateRecordingAccessConfigInRoom( roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.speakerSecret + }); const response = await request(app) .delete(`${RECORDINGS_PATH}/${fakeRecordingId}`) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(403); }); - it('should succeed when recording access is admin_moderator_speaker and participant is moderator', async () => { + it('should succeed when recording access is admin_moderator_speaker and user is moderator', async () => { await updateRecordingAccessConfigInRoom( roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.moderatorSecret + }); const response = await request(app) .delete(`${RECORDINGS_PATH}/${fakeRecordingId}`) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(404); }); - it('should succeed when recording access is admin_moderator_speaker and participant is moderator, token in cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - await updateRecordingAccessConfigInRoom( - roomData.room.roomId, - MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER - ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); - - const response = await request(app) - .delete(`${RECORDINGS_PATH}/${fakeRecordingId}`) - .set('Cookie', recordingToken); - expect(response.status).toBe(404); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - - it('should fail when recording access is admin_moderator and participant is speaker', async () => { + it('should fail when recording access is admin_moderator and user is speaker', async () => { await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.speakerSecret + }); const response = await request(app) .delete(`${RECORDINGS_PATH}/${fakeRecordingId}`) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(403); }); - it('should succeed when recording access is admin_moderator and participant is moderator', async () => { + it('should succeed when recording access is admin_moderator and user is moderator', async () => { await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.moderatorSecret + }); const response = await request(app) .delete(`${RECORDINGS_PATH}/${fakeRecordingId}`) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(404); }); }); @@ -524,73 +447,61 @@ describe('Recording API Security Tests', () => { expect(response.status).toBe(400); }); - it('should fail when recording access is admin_moderator_speaker and participant is speaker', async () => { + it('should fail when recording access is admin_moderator_speaker and user is speaker', async () => { await updateRecordingAccessConfigInRoom( roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.speakerSecret + }); const response = await request(app) .delete(RECORDINGS_PATH) .query({ recordingIds: fakeRecordingId }) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(403); }); - it('should succeed when recording access is admin_moderator_speaker and participant is moderator', async () => { + it('should succeed when recording access is admin_moderator_speaker and user is moderator', async () => { await updateRecordingAccessConfigInRoom( roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.moderatorSecret + }); const response = await request(app) .delete(RECORDINGS_PATH) .query({ recordingIds: fakeRecordingId }) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(400); }); - it('should succeed when recording access is admin_moderator_speaker and participant is moderator, token in cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - await updateRecordingAccessConfigInRoom( - roomData.room.roomId, - MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER - ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); - - const response = await request(app) - .delete(RECORDINGS_PATH) - .query({ recordingIds: fakeRecordingId }) - .set('Cookie', recordingToken); - expect(response.status).toBe(400); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - - it('should fail when recording access is admin_moderator and participant is speaker', async () => { + it('should fail when recording access is admin_moderator and user is speaker', async () => { await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.speakerSecret + }); const response = await request(app) .delete(RECORDINGS_PATH) .query({ recordingIds: fakeRecordingId }) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(403); }); - it('should succeed when recording access is admin_moderator and participant is moderator', async () => { + it('should succeed when recording access is admin_moderator and user is moderator', async () => { await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.moderatorSecret + }); const response = await request(app) .delete(RECORDINGS_PATH) .query({ recordingIds: fakeRecordingId }) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(400); }); }); @@ -610,86 +521,77 @@ describe('Recording API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when recording access is admin_moderator_speaker and participant is speaker', async () => { + it('should succeed when recording access is admin_moderator_speaker and user is speaker', async () => { await updateRecordingAccessConfigInRoom( roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.speakerSecret + }); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}/media`) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(200); }); - it('should succeed when recording access is admin_moderator_speaker and participant is speaker, token in cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - + it('should succeed when recording access is admin_moderator_speaker and user is speaker, token in query param', async () => { await updateRecordingAccessConfigInRoom( roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); - - const response = await request(app) - .get(`${RECORDINGS_PATH}/${recordingId}/media`) - .set('Cookie', recordingToken); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - - it('should succeed when recording access is admin_moderator_speaker and participant is speaker, token in query param', async () => { - await updateRecordingAccessConfigInRoom( - roomData.room.roomId, - MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER - ); - let recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + let roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.speakerSecret + }); // Remove the "Bearer " prefix if present - if (recordingToken.startsWith('Bearer ')) { - recordingToken = recordingToken.slice(7); + if (roomMemberToken.startsWith('Bearer ')) { + roomMemberToken = roomMemberToken.slice(7); } const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}/media`) - .query({ recordingToken }); + .query({ roomMemberToken }); expect(response.status).toBe(200); }); - it('should succeed when recording access is admin_moderator_speaker and participant is moderator', async () => { + it('should succeed when recording access is admin_moderator_speaker and user is moderator', async () => { await updateRecordingAccessConfigInRoom( roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.moderatorSecret + }); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}/media`) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(200); }); - it('should fail when recording access is admin_moderator and participant is speaker', async () => { + it('should fail when recording access is admin_moderator and user is speaker', async () => { await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.speakerSecret + }); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}/media`) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(403); }); - it('should succeed when recording access is admin_moderator and participant is moderator', async () => { + it('should succeed when recording access is admin_moderator and user is moderator', async () => { await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.moderatorSecret + }); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}/media`) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(200); }); @@ -759,68 +661,57 @@ describe('Recording API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when recording access is admin_moderator_speaker and participant is speaker', async () => { + it('should succeed when recording access is admin_moderator_speaker and user is speaker', async () => { await updateRecordingAccessConfigInRoom( roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.speakerSecret + }); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}/url`) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(200); }); - it('should succeed when recording access is admin_moderator_speaker and participant is speaker, token in cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - + it('should succeed when recording access is admin_moderator_speaker and user is moderator', async () => { await updateRecordingAccessConfigInRoom( roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.moderatorSecret + }); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}/url`) - .set('Cookie', recordingToken); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - - it('should succeed when recording access is admin_moderator_speaker and participant is moderator', async () => { - await updateRecordingAccessConfigInRoom( - roomData.room.roomId, - MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER - ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); - - const response = await request(app) - .get(`${RECORDINGS_PATH}/${recordingId}/url`) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(200); }); - it('should fail when recording access is admin_moderator and participant is speaker', async () => { + it('should fail when recording access is admin_moderator and user is speaker', async () => { await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.speakerSecret + }); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}/url`) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(403); }); - it('should succeed when recording access is admin_moderator and participant is moderator', async () => { + it('should succeed when recording access is admin_moderator and user is moderator', async () => { await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.moderatorSecret + }); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}/url`) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(200); }); }); @@ -842,91 +733,81 @@ describe('Recording API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when recording access is admin_moderator_speaker and participant is speaker', async () => { + it('should succeed when recording access is admin_moderator_speaker and user is speaker', async () => { await updateRecordingAccessConfigInRoom( roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.speakerSecret + }); const response = await request(app) .get(`${RECORDINGS_PATH}/download`) .query({ recordingIds: recordingId }) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(200); }); - it('should succeed when recording access is admin_moderator_speaker and participant is speaker, token in cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - + it('should succeed when recording access is admin_moderator_speaker and user is speaker, token in query param', async () => { await updateRecordingAccessConfigInRoom( roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); - - const response = await request(app) - .get(`${RECORDINGS_PATH}/download`) - .query({ recordingIds: recordingId }) - .set('Cookie', recordingToken); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - - it('should succeed when recording access is admin_moderator_speaker and participant is speaker, token in query param', async () => { - await updateRecordingAccessConfigInRoom( - roomData.room.roomId, - MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER - ); - let recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + let roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.speakerSecret + }); // Remove the "Bearer " prefix if present - if (recordingToken.startsWith('Bearer ')) { - recordingToken = recordingToken.slice(7); + if (roomMemberToken.startsWith('Bearer ')) { + roomMemberToken = roomMemberToken.slice(7); } const response = await request(app) .get(`${RECORDINGS_PATH}/download`) - .query({ recordingIds: recordingId, recordingToken }); + .query({ recordingIds: recordingId, roomMemberToken }); expect(response.status).toBe(200); }); - it('should succeed when recording access is admin_moderator_speaker and participant is moderator', async () => { + it('should succeed when recording access is admin_moderator_speaker and user is moderator', async () => { await updateRecordingAccessConfigInRoom( roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER ); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.moderatorSecret + }); const response = await request(app) .get(`${RECORDINGS_PATH}/download`) .query({ recordingIds: recordingId }) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(200); }); - it('should fail when recording access is admin_moderator and participant is speaker', async () => { + it('should fail when recording access is admin_moderator and user is speaker', async () => { await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.speakerSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.speakerSecret + }); const response = await request(app) .get(`${RECORDINGS_PATH}/download`) .query({ recordingIds: recordingId }) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(403); }); - it('should succeed when recording access is admin_moderator and participant is moderator', async () => { + it('should succeed when recording access is admin_moderator and user is moderator', async () => { await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingToken = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const roomMemberToken = await generateRoomMemberToken(roomData.room.roomId, { + secret: roomData.moderatorSecret + }); const response = await request(app) .get(`${RECORDINGS_PATH}/download`) .query({ recordingIds: recordingId }) - .set(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); expect(response.status).toBe(200); }); }); diff --git a/meet-ce/backend/tests/integration/api/security/room-security.test.ts b/meet-ce/backend/tests/integration/api/security/room-security.test.ts index 3424d426..dcd86719 100644 --- a/meet-ce/backend/tests/integration/api/security/room-security.test.ts +++ b/meet-ce/backend/tests/integration/api/security/room-security.test.ts @@ -1,22 +1,18 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; +import { AuthMode, MeetRecordingAccess } from '@openvidu-meet/typings'; 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 { AuthMode, AuthTransportMode, MeetRecordingAccess, ParticipantRole } from '@openvidu-meet/typings'; import { - changeAuthTransportMode, changeSecurityConfig, createRoom, - deleteAllRecordings, deleteAllRooms, - disconnectFakeParticipants, loginUser, sleep, - startTestServer, - updateRecordingAccessConfigInRoom + startTestServer } from '../../../helpers/request-helpers.js'; -import { RoomData, setupSingleRoom, setupSingleRoomWithRecording } from '../../../helpers/test-scenarios.js'; +import { RoomData, setupSingleRoom } from '../../../helpers/test-scenarios.js'; const ROOMS_PATH = `${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms`; const INTERNAL_ROOMS_PATH = `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/rooms`; @@ -31,8 +27,7 @@ describe('Room API Security Tests', () => { }); afterAll(async () => { - await disconnectFakeParticipants(); - await Promise.all([deleteAllRooms(), deleteAllRecordings()]); + await deleteAllRooms(); }); describe('Create Room Tests', () => { @@ -52,20 +47,6 @@ describe('Room API Security Tests', () => { expect(response.status).toBe(201); }); - it('should succeed when user is authenticated as admin via cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - const response = await request(app).post(ROOMS_PATH).set('Cookie', adminCookie).send({}); - expect(response.status).toBe(201); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).post(ROOMS_PATH).send({}); expect(response.status).toBe(401); @@ -87,20 +68,6 @@ describe('Room API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when user is authenticated as admin via cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - const response = await request(app).post(ROOMS_PATH).set('Cookie', adminCookie).send({}); - expect(response.status).toBe(201); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).get(ROOMS_PATH); expect(response.status).toBe(401); @@ -131,23 +98,6 @@ describe('Room API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when user is authenticated as admin via cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - const response = await request(app) - .delete(ROOMS_PATH) - .query({ roomIds: roomId }) - .set('Cookie', adminCookie); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).delete(ROOMS_PATH).query({ roomIds: roomId }); expect(response.status).toBe(401); @@ -175,52 +125,35 @@ describe('Room API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when user is authenticated as admin via cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - const response = await request(app).get(`${ROOMS_PATH}/${roomData.room.roomId}`).set('Cookie', adminCookie); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).get(`${ROOMS_PATH}/${roomData.room.roomId}`); expect(response.status).toBe(401); }); - it('should succeed when participant is moderator', async () => { + it('should succeed when user is moderator', async () => { const response = await request(app) .get(`${ROOMS_PATH}/${roomData.room.roomId}`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.moderatorToken); expect(response.status).toBe(200); }); - it('should fail when participant is moderator of a different room', async () => { + it('should fail when user is moderator of a different room', async () => { const newRoomData = await setupSingleRoom(); const response = await request(app) .get(`${ROOMS_PATH}/${roomData.room.roomId}`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, newRoomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, newRoomData.moderatorToken); expect(response.status).toBe(403); }); - it('should succeed when participant is speaker', async () => { + it('should succeed when user is speaker', async () => { const response = await request(app) .get(`${ROOMS_PATH}/${roomData.room.roomId}`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.speakerToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.SPEAKER); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.speakerToken); expect(response.status).toBe(200); }); - it('should succeed when user is authenticated but has expired token, and has valid participant token', async () => { + it('should succeed when user is authenticated but has expired token, and has valid room member token', async () => { // Set short access token expiration const initialTokenExpiration = INTERNAL_CONFIG.ACCESS_TOKEN_EXPIRATION; INTERNAL_CONFIG.ACCESS_TOKEN_EXPIRATION = '1s'; @@ -234,8 +167,7 @@ describe('Room API Security Tests', () => { const response = await request(app) .get(`${ROOMS_PATH}/${roomData.room.roomId}`) .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, expiredAccessToken) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.moderatorToken); expect(response.status).toBe(200); }); }); @@ -262,20 +194,6 @@ describe('Room API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when user is authenticated as admin via cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - const response = await request(app).delete(`${ROOMS_PATH}/${roomId}`).set('Cookie', adminCookie); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).delete(`${ROOMS_PATH}/${roomId}`); expect(response.status).toBe(401); @@ -303,60 +221,40 @@ describe('Room API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when user is authenticated as admin via cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - const response = await request(app) - .get(`${ROOMS_PATH}/${roomData.room.roomId}/config`) - .set('Cookie', adminCookie); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).get(`${ROOMS_PATH}/${roomData.room.roomId}/config`); expect(response.status).toBe(401); }); - it('should succeed when participant is moderator', async () => { + it('should succeed when user is moderator', async () => { const response = await request(app) .get(`${ROOMS_PATH}/${roomData.room.roomId}/config`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.moderatorToken); expect(response.status).toBe(200); }); - it('should fail when participant is moderator of a different room', async () => { + it('should fail when user is moderator of a different room', async () => { const newRoomData = await setupSingleRoom(); const response = await request(app) .get(`${ROOMS_PATH}/${roomData.room.roomId}/config`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, newRoomData.moderatorToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, newRoomData.moderatorToken); expect(response.status).toBe(403); }); - it('should succeed when participant is speaker', async () => { + it('should succeed when user is speaker', async () => { const response = await request(app) .get(`${ROOMS_PATH}/${roomData.room.roomId}/config`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, roomData.speakerToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.SPEAKER); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.speakerToken); expect(response.status).toBe(200); }); - it('should fail when participant is speaker of a different room', async () => { + it('should fail when user is speaker of a different room', async () => { const newRoomData = await setupSingleRoom(); const response = await request(app) .get(`${ROOMS_PATH}/${roomData.room.roomId}/config`) - .set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, newRoomData.speakerToken) - .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.SPEAKER); + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, newRoomData.speakerToken); expect(response.status).toBe(403); }); }); @@ -394,23 +292,6 @@ describe('Room API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when user is authenticated as admin via cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - const response = await request(app) - .put(`${ROOMS_PATH}/${roomId}/config`) - .set('Cookie', adminCookie) - .send({ config: roomConfig }); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).put(`${ROOMS_PATH}/${roomId}/config`).send({ config: roomConfig }); expect(response.status).toBe(401); @@ -441,154 +322,111 @@ describe('Room API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when user is authenticated as admin via cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - const response = await request(app) - .put(`${ROOMS_PATH}/${roomId}/status`) - .set('Cookie', adminCookie) - .send({ status: 'open' }); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).put(`${ROOMS_PATH}/${roomId}/status`).send({ status: 'open' }); expect(response.status).toBe(401); }); }); - describe('Generate Recording Token Tests', () => { + describe('Generate Room Member Token Tests', () => { let roomData: RoomData; beforeAll(async () => { - roomData = await setupSingleRoomWithRecording(true); + roomData = await setupSingleRoom(); }); - beforeEach(async () => { - await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER); - }); - - it('should succeed when no authentication is required and participant is speaker', async () => { + it('should succeed when no authentication is required and user is speaker', async () => { await changeSecurityConfig(AuthMode.NONE); - const response = await request(app) - .post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/recording-token`) - .send({ secret: roomData.speakerSecret }); + const response = await request(app).post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/token`).send({ + secret: roomData.speakerSecret + }); expect(response.status).toBe(200); }); - it('should succeed when no authentication is required and participant is moderator', async () => { + it('should succeed when no authentication is required and user is moderator', async () => { await changeSecurityConfig(AuthMode.NONE); - const response = await request(app) - .post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/recording-token`) - .send({ secret: roomData.moderatorSecret }); + const response = await request(app).post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/token`).send({ + secret: roomData.moderatorSecret + }); expect(response.status).toBe(200); }); - it('should succeed when authentication is required for moderator and participant is speaker', async () => { + it('should succeed when authentication is required for moderator and user is speaker', async () => { await changeSecurityConfig(AuthMode.MODERATORS_ONLY); - const response = await request(app) - .post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/recording-token`) - .send({ secret: roomData.speakerSecret }); + const response = await request(app).post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/token`).send({ + secret: roomData.speakerSecret + }); expect(response.status).toBe(200); }); - it('should succeed when authentication is required for moderator, participant is moderator and authenticated', async () => { + it('should succeed when authentication is required for moderator, user is moderator and authenticated', async () => { await changeSecurityConfig(AuthMode.MODERATORS_ONLY); const response = await request(app) - .post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/recording-token`) + .post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/token`) .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, adminAccessToken) - .send({ secret: roomData.moderatorSecret }); + .send({ + secret: roomData.moderatorSecret + }); expect(response.status).toBe(200); }); - it('should succeed when authentication is required for moderator, participant is moderator and authenticated via cookie', async () => { + it('should fail when authentication is required for moderator and user is moderator but not authenticated', async () => { await changeSecurityConfig(AuthMode.MODERATORS_ONLY); - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - const response = await request(app) - .post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/recording-token`) - .set('Cookie', adminCookie) - .send({ secret: roomData.moderatorSecret }); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - - it('should fail when authentication is required for moderator and participant is moderator but not authenticated', async () => { - await changeSecurityConfig(AuthMode.MODERATORS_ONLY); - - const response = await request(app) - .post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/recording-token`) - .send({ secret: roomData.moderatorSecret }); + const response = await request(app).post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/token`).send({ + secret: roomData.moderatorSecret + }); expect(response.status).toBe(401); }); - it('should succeed when authentication is required for all users, participant is speaker and authenticated', async () => { + it('should succeed when authentication is required for all users, user is speaker and authenticated', async () => { await changeSecurityConfig(AuthMode.ALL_USERS); const response = await request(app) - .post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/recording-token`) + .post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/token`) .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, adminAccessToken) - .send({ secret: roomData.speakerSecret }); + .send({ + secret: roomData.speakerSecret + }); expect(response.status).toBe(200); }); - it('should fail when authentication is required for all users and participant is speaker but not authenticated', async () => { + it('should fail when authentication is required for all users and user is speaker but not authenticated', async () => { await changeSecurityConfig(AuthMode.ALL_USERS); - const response = await request(app) - .post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/recording-token`) - .send({ secret: roomData.speakerSecret }); + const response = await request(app).post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/token`).send({ + secret: roomData.speakerSecret + }); expect(response.status).toBe(401); }); - it('should succeed when authentication is required for all users, participant is moderator and authenticated', async () => { + it('should succeed when authentication is required for all users, user is moderator and authenticated', async () => { await changeSecurityConfig(AuthMode.ALL_USERS); const response = await request(app) - .post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/recording-token`) + .post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/token`) .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, adminAccessToken) - .send({ secret: roomData.moderatorSecret }); + .send({ + secret: roomData.moderatorSecret + }); expect(response.status).toBe(200); }); - it('should fail when authentication is required for all users and participant is moderator but not authenticated', async () => { + it('should fail when authentication is required for all users and user is moderator but not authenticated', async () => { await changeSecurityConfig(AuthMode.ALL_USERS); - const response = await request(app) - .post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/recording-token`) - .send({ secret: roomData.moderatorSecret }); + const response = await request(app).post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/token`).send({ + secret: roomData.moderatorSecret + }); expect(response.status).toBe(401); }); - - it('should fail when recording access is set to admin only', async () => { - await updateRecordingAccessConfigInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN); - - const response = await request(app) - .post(`${INTERNAL_ROOMS_PATH}/${roomData.room.roomId}/recording-token`) - .send({ secret: roomData.moderatorSecret }); - expect(response.status).toBe(403); - }); }); - describe('Get Room Roles and Permissions Tests', () => { + describe('Get Room Member Roles and Permissions Tests', () => { let roomId: string; beforeAll(async () => { @@ -602,7 +440,7 @@ describe('Room API Security Tests', () => { }); }); - describe('Get Room Role and Permissions Tests', () => { + describe('Get Room Member Role and Permissions Tests', () => { let roomData: RoomData; beforeAll(async () => { diff --git a/meet-ce/backend/tests/integration/api/security/user-security.test.ts b/meet-ce/backend/tests/integration/api/security/user-security.test.ts index 24a39a70..043b320a 100644 --- a/meet-ce/backend/tests/integration/api/security/user-security.test.ts +++ b/meet-ce/backend/tests/integration/api/security/user-security.test.ts @@ -3,13 +3,7 @@ import { Express } from 'express'; import request from 'supertest'; import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js'; import { MEET_INITIAL_ADMIN_PASSWORD } from '../../../../src/environment.js'; -import { AuthTransportMode } from '@openvidu-meet/typings'; -import { - changeAuthTransportMode, - changePassword, - loginUser, - startTestServer -} from '../../../helpers/request-helpers.js'; +import { changePassword, loginUser, startTestServer } from '../../../helpers/request-helpers.js'; const USERS_PATH = `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/users`; @@ -34,20 +28,6 @@ describe('User API Security Tests', () => { expect(response.status).toBe(200); }); - it('should succeed when user is authenticated as admin via cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - const response = await request(app).get(`${USERS_PATH}/profile`).set('Cookie', adminCookie); - expect(response.status).toBe(200); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).get(`${USERS_PATH}/profile`); expect(response.status).toBe(401); @@ -77,26 +57,6 @@ describe('User API Security Tests', () => { await changePassword(changePasswordRequest.newPassword, MEET_INITIAL_ADMIN_PASSWORD, adminAccessToken); }); - it('should succeed when user is authenticated as admin via cookie', async () => { - // Set auth transport mode to cookie - await changeAuthTransportMode(AuthTransportMode.COOKIE); - - // Login as admin to get access token cookie - const adminCookie = await loginUser(); - - const response = await request(app) - .post(`${USERS_PATH}/change-password`) - .set('Cookie', adminCookie) - .send(changePasswordRequest); - expect(response.status).toBe(200); - - // Reset password - await changePassword(changePasswordRequest.newPassword, MEET_INITIAL_ADMIN_PASSWORD, adminCookie); - - // Revert auth transport mode to header - await changeAuthTransportMode(AuthTransportMode.HEADER); - }); - it('should fail when user is not authenticated', async () => { const response = await request(app).post(`${USERS_PATH}/change-password`).send(changePasswordRequest); expect(response.status).toBe(401); diff --git a/meet-ce/backend/tsconfig.json b/meet-ce/backend/tsconfig.json index 67723abf..e26e681f 100644 --- a/meet-ce/backend/tsconfig.json +++ b/meet-ce/backend/tsconfig.json @@ -13,7 +13,6 @@ "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - "typeRoots": ["src/config/@types"] }, "include": ["src/**/*.ts", "index.ts"], "exclude": ["node_modules", "tests/**/*.ts", "tests/**/*.tsx", "node_modules/@types/cookie-parser/node_modules/@types/express*", "node_modules/@types/cookie-parser/node_modules/@types/express-serve-static-core*"], diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/auth.guard.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/auth.guard.ts index 840d3ab0..659b75a1 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/auth.guard.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/auth.guard.ts @@ -1,15 +1,6 @@ import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router'; -import { ErrorReason } from '../models'; -import { - AuthService, - GlobalConfigService, - NavigationService, - ParticipantService, - RecordingService, - RoomService -} from '../services'; -import { AuthMode, ParticipantRole } from '@openvidu-meet/typings'; +import { AuthService, NavigationService } from '../services'; export const checkUserAuthenticatedGuard: CanActivateFn = async ( _route: ActivatedRouteSnapshot, @@ -46,98 +37,3 @@ export const checkUserNotAuthenticatedGuard: CanActivateFn = async ( // Allow access to the requested page return true; }; - -export const checkParticipantRoleAndAuthGuard: CanActivateFn = async ( - _route: ActivatedRouteSnapshot, - state: RouterStateSnapshot -) => { - const navigationService = inject(NavigationService); - const authService = inject(AuthService); - const configService = inject(GlobalConfigService); - const roomService = inject(RoomService); - const participantService = inject(ParticipantService); - - // Get the role that the participant will have in the room based on the room ID and secret - let participantRole: ParticipantRole; - - try { - const roomId = roomService.getRoomId(); - const secret = roomService.getRoomSecret(); - - const roomRoleAndPermissions = await roomService.getRoomRoleAndPermissions(roomId, secret); - participantRole = roomRoleAndPermissions.role; - participantService.setParticipantRole(participantRole); - } catch (error: any) { - console.error('Error getting participant role:', error); - switch (error.status) { - case 400: - // Invalid secret - return navigationService.redirectToErrorPage(ErrorReason.INVALID_ROOM_SECRET); - case 404: - // Room not found - return navigationService.redirectToErrorPage(ErrorReason.INVALID_ROOM); - default: - return navigationService.redirectToErrorPage(ErrorReason.INTERNAL_ERROR); - } - } - - const authMode = await configService.getAuthModeToAccessRoom(); - - // If the user is a moderator and the room requires authentication for moderators only, - // or if the room requires authentication for all users, - // then check if the user is authenticated - const isAuthRequiredForModerators = - authMode === AuthMode.MODERATORS_ONLY && participantRole === ParticipantRole.MODERATOR; - const isAuthRequiredForAllUsers = authMode === AuthMode.ALL_USERS; - - if (isAuthRequiredForModerators || isAuthRequiredForAllUsers) { - // Check if user is authenticated - const isAuthenticated = await authService.isUserAuthenticated(); - if (!isAuthenticated) { - // Redirect to the login page with query param to redirect back to the room - return navigationService.redirectToLoginPage(state.url); - } - } - - // Allow access to the room - return true; -}; - -export const checkRecordingAuthGuard: CanActivateFn = async ( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot -) => { - const recordingService = inject(RecordingService); - const navigationService = inject(NavigationService); - - const recordingId = route.params['recording-id']; - const secret = route.queryParams['secret']; - - if (!secret) { - // If no secret is provided, redirect to the error page - return navigationService.redirectToErrorPage(ErrorReason.MISSING_RECORDING_SECRET); - } - - try { - // Attempt to access the recording to check if the secret is valid - await recordingService.getRecording(recordingId, secret); - return true; - } catch (error: any) { - console.error('Error checking recording access:', error); - switch (error.status) { - case 400: - // Invalid secret - return navigationService.redirectToErrorPage(ErrorReason.INVALID_RECORDING_SECRET); - case 401: - // Unauthorized access - // Redirect to the login page with query param to redirect back to the recording - return navigationService.redirectToLoginPage(state.url); - case 404: - // Recording not found - return navigationService.redirectToErrorPage(ErrorReason.INVALID_RECORDING); - default: - // Internal error - return navigationService.redirectToErrorPage(ErrorReason.INTERNAL_ERROR); - } - } -}; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts index 25a02d50..4890d00b 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts @@ -1,13 +1,13 @@ import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivateFn } from '@angular/router'; -import { ErrorReason } from '../models'; -import { AppDataService, NavigationService, ParticipantService, RoomService, SessionStorageService } from '../services'; import { WebComponentProperty } from '@openvidu-meet/typings'; +import { ErrorReason } from '../models'; +import { AppDataService, NavigationService, RoomMemberService, RoomService, SessionStorageService } from '../services'; export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => { const navigationService = inject(NavigationService); const roomService = inject(RoomService); - const participantService = inject(ParticipantService); + const roomMemberService = inject(RoomMemberService); const sessionStorageService = inject(SessionStorageService); const { @@ -16,9 +16,10 @@ export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRoute participantName, leaveRedirectUrl, showOnlyRecordings, - e2eeKey + e2eeKey: queryE2eeKey } = extractParams(route); const secret = querySecret || sessionStorageService.getRoomSecret(); + const e2eeKey = queryE2eeKey || sessionStorageService.getE2EEKey(); // Handle leave redirect URL logic handleLeaveRedirectUrl(leaveRedirectUrl); @@ -30,10 +31,13 @@ export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRoute roomService.setRoomId(roomId); roomService.setRoomSecret(secret); - roomService.setE2EEKey(e2eeKey); + + if (e2eeKey) { + roomService.setE2EEKey(e2eeKey); + } if (participantName) { - participantService.setParticipantName(participantName); + roomMemberService.setParticipantName(participantName); } if (showOnlyRecordings === 'true') { diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/validate-access.guard.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/validate-access.guard.ts index f4385e1b..d5340ad3 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/validate-access.guard.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/validate-access.guard.ts @@ -1,40 +1,71 @@ import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router'; import { ErrorReason } from '../models'; -import { NavigationService, ParticipantService, RecordingService, RoomService } from '../services'; +import { NavigationService, RecordingService, RoomMemberService, RoomService } from '../services'; /** - * Guard to validate access to a room by generating a participant token. + * Guard to validate access to a room by generating a room member token. */ export const validateRoomAccessGuard: CanActivateFn = async ( _route: ActivatedRouteSnapshot, - _state: RouterStateSnapshot + state: RouterStateSnapshot ) => { + return validateRoomAccessInternal(state.url); +}; + +/** + * Guard to validate the access to recordings of a room by generating a room member token and checking permissions. + */ +export const validateRoomRecordingsAccessGuard: CanActivateFn = async ( + _route: ActivatedRouteSnapshot, + state: RouterStateSnapshot +) => { + return validateRoomAccessInternal(state.url, true); +}; + +/** + * Internal helper function to validate room access by generating a room member token. + * + * @param pageUrl - The URL of the page being accessed + * @param validateRecordingPermissions - Whether to validate recording access permissions + * @returns True if access is granted, or UrlTree for redirection + */ +const validateRoomAccessInternal = async (pageUrl: string, validateRecordingPermissions = false) => { const roomService = inject(RoomService); - const participantTokenService = inject(ParticipantService); + const roomMemberService = inject(RoomMemberService); const navigationService = inject(NavigationService); const roomId = roomService.getRoomId(); const secret = roomService.getRoomSecret(); try { - await participantTokenService.generateToken({ - roomId, - secret + await roomMemberService.generateToken(roomId, { + secret, + grantJoinMeetingPermission: false }); + + // Perform recording validation if requested + if (validateRecordingPermissions) { + if (!roomMemberService.canRetrieveRecordings()) { + // If the user does not have permission to retrieve recordings, redirect to the error page + return navigationService.redirectToErrorPage(ErrorReason.UNAUTHORIZED_RECORDING_ACCESS); + } + } + return true; } catch (error: any) { - console.error('Error generating participant token:', error); + console.error('Error generating room member token:', error); switch (error.status) { case 400: // Invalid secret return navigationService.redirectToErrorPage(ErrorReason.INVALID_ROOM_SECRET); + case 401: + // Unauthorized access + // Redirect to the login page with query param to redirect back to the page + return navigationService.redirectToLoginPage(pageUrl); case 404: // Room not found return navigationService.redirectToErrorPage(ErrorReason.INVALID_ROOM); - case 409: - // Room is closed - return navigationService.redirectToErrorPage(ErrorReason.CLOSED_ROOM); default: return navigationService.redirectToErrorPage(ErrorReason.INTERNAL_ERROR); } @@ -42,42 +73,42 @@ export const validateRoomAccessGuard: CanActivateFn = async ( }; /** - * Guard to validate the access to recordings of a room by generating a recording token. + * Guard to validate access to a recording by checking the recording secret. */ export const validateRecordingAccessGuard: CanActivateFn = async ( - _route: ActivatedRouteSnapshot, - _state: RouterStateSnapshot + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot ) => { - const roomService = inject(RoomService); const recordingService = inject(RecordingService); const navigationService = inject(NavigationService); - const roomId = roomService.getRoomId(); - const secret = roomService.getRoomSecret(); + const recordingId = route.params['recording-id']; + const secret = route.queryParams['secret']; + + if (!secret) { + // If no secret is provided, redirect to the error page + return navigationService.redirectToErrorPage(ErrorReason.MISSING_RECORDING_SECRET); + } try { - // Generate a token to access recordings in the room - await recordingService.generateRecordingToken(roomId, secret); - - if (!recordingService.canRetrieveRecordings()) { - // If the user does not have permission to retrieve recordings, redirect to the error page - return navigationService.redirectToErrorPage(ErrorReason.UNAUTHORIZED_RECORDING_ACCESS); - } - + // Attempt to access the recording to check if the secret is valid + await recordingService.getRecording(recordingId, secret); return true; } catch (error: any) { - console.error('Error generating recording token:', error); + console.error('Error checking recording access:', error); switch (error.status) { case 400: // Invalid secret - return navigationService.redirectToErrorPage(ErrorReason.INVALID_ROOM_SECRET); - case 403: - // Recording access is configured for admins only - return navigationService.redirectToErrorPage(ErrorReason.RECORDINGS_ADMIN_ONLY_ACCESS); + return navigationService.redirectToErrorPage(ErrorReason.INVALID_RECORDING_SECRET); + case 401: + // Unauthorized access + // Redirect to the login page with query param to redirect back to the recording + return navigationService.redirectToLoginPage(state.url); case 404: - // There are no recordings in the room or the room does not exist - return navigationService.redirectToErrorPage(ErrorReason.NO_RECORDINGS); + // Recording not found + return navigationService.redirectToErrorPage(ErrorReason.INVALID_RECORDING); default: + // Internal error return navigationService.redirectToErrorPage(ErrorReason.INTERNAL_ERROR); } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/interceptors/http.interceptor.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/interceptors/http.interceptor.ts index 22acbdd5..2db7053f 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/interceptors/http.interceptor.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/interceptors/http.interceptor.ts @@ -1,14 +1,13 @@ import { HttpErrorResponse, HttpEvent, HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http'; import { inject } from '@angular/core'; import { Router } from '@angular/router'; -import { AuthService, ParticipantService, RecordingService, RoomService, TokenStorageService } from '../services'; import { catchError, from, Observable, switchMap } from 'rxjs'; +import { AuthService, RoomMemberService, RoomService, TokenStorageService } from '../services'; /** * Adds all necessary authorization headers to the request based on available tokens * - authorization: Bearer token for access token (from localStorage) - * - x-participant-token: Bearer token for participant token (from sessionStorage) - * - x-recording-token: Bearer token for recording token (from sessionStorage) + * - x-room-member-token: Bearer token for room member token (from sessionStorage) */ const addAuthHeadersIfNeeded = ( req: HttpRequest, @@ -22,16 +21,10 @@ const addAuthHeadersIfNeeded = ( headers['authorization'] = `Bearer ${accessToken}`; } - // Add participant token header if available - const participantToken = tokenStorageService.getParticipantToken(); - if (participantToken) { - headers['x-participant-token'] = `Bearer ${participantToken}`; - } - - // Add recording token header if available - const recordingToken = tokenStorageService.getRecordingToken(); - if (recordingToken) { - headers['x-recording-token'] = `Bearer ${recordingToken}`; + // Add room member token header if available + const roomMemberToken = tokenStorageService.getRoomMemberToken(); + if (roomMemberToken) { + headers['x-room-member-token'] = `Bearer ${roomMemberToken}`; } // Clone request with all headers at once if any were added @@ -42,18 +35,12 @@ export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest, ne const router: Router = inject(Router); const authService: AuthService = inject(AuthService); const roomService = inject(RoomService); - const participantTokenService = inject(ParticipantService); - const recordingService = inject(RecordingService); + const roomMemberService = inject(RoomMemberService); const tokenStorageService = inject(TokenStorageService); const pageUrl = router.currentNavigation()?.finalUrl?.toString() || router.url; const requestUrl = req.url; - // Clone request with credentials for cookie mode - req = req.clone({ - withCredentials: true - }); - // Add all authorization headers if tokens exist req = addAuthHeadersIfNeeded(req, tokenStorageService); @@ -83,46 +70,30 @@ export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest, ne ); }; - const refreshParticipantToken = (firstError: HttpErrorResponse): Observable> => { - console.log('Refreshing participant token...'); + const refreshRoomMemberToken = (firstError: HttpErrorResponse): Observable> => { + console.log('Refreshing room member token...'); const roomId = roomService.getRoomId(); const secret = roomService.getRoomSecret(); - const participantName = participantTokenService.getParticipantName(); - const participantIdentity = participantTokenService.getParticipantIdentity(); + const participantName = roomMemberService.getParticipantName(); + const participantIdentity = roomMemberService.getParticipantIdentity(); + const grantJoinMeetingPermission = !!participantIdentity; // Grant join permission if identity is set return from( - participantTokenService.refreshParticipantToken({ roomId, secret, participantName, participantIdentity }) + roomMemberService.generateToken(roomId, { + secret, + grantJoinMeetingPermission, + participantName, + participantIdentity + }) ).pipe( switchMap(() => { - console.log('Participant token refreshed'); + console.log('Room member token refreshed'); req = addAuthHeadersIfNeeded(req, tokenStorageService); return next(req); }), catchError((error: HttpErrorResponse) => { - if (error.url?.includes('/token/refresh')) { - console.error('Error refreshing participant token'); - throw firstError; - } - - throw error; - }) - ); - }; - - const refreshRecordingToken = (firstError: HttpErrorResponse): Observable> => { - console.log('Refreshing recording token...'); - const roomId = roomService.getRoomId(); - const secret = roomService.getRoomSecret(); - - return from(recordingService.generateRecordingToken(roomId, secret)).pipe( - switchMap(() => { - console.log('Recording token refreshed'); - req = addAuthHeadersIfNeeded(req, tokenStorageService); - return next(req); - }), - catchError((error: HttpErrorResponse) => { - if (error.url?.includes('/recording-token')) { - console.error('Error refreshing recording token'); + if (error.url?.includes('/token')) { + console.error('Error refreshing room member token'); throw firstError; } @@ -134,45 +105,24 @@ export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest, ne return next(req).pipe( catchError((error: HttpErrorResponse) => { if (error.status === 401) { - // Error refreshing participant token - if (error.url?.includes('/token/refresh')) { - console.log('Refreshing participant token failed. Refreshing access token first...'); - // This means that first we need to refresh the access token and then the participant token + // Error refreshing room member token + if (error.url?.includes('/token')) { + console.log('Generating room member token failed. Refreshing access token first...'); + // This means that first we need to refresh the access token and then the room member token return refreshAccessToken(error); } - // Error refreshing recording token - if (error.url?.includes('/recording-token')) { - console.log('Refreshing recording token failed. Refreshing access token first...'); - // This means that first we need to refresh the access token and then the recording token - return refreshAccessToken(error); - } - - // Expired recording token - if ( - pageUrl.startsWith('/room/') && - pageUrl.includes('/recordings') && - requestUrl.includes('/recordings') - ) { - // If the error occurred in the room recordings page and the request is to the recordings endpoint, - // refresh the recording token - return refreshRecordingToken(error); - } - - // Expired participant token - if ( - pageUrl.startsWith('/room/') && - !pageUrl.includes('/recordings') && - !requestUrl.includes('/profile') - ) { + // Expired room member token + if (pageUrl.startsWith('/room/') && !requestUrl.includes('/profile')) { // If the error occurred in a room page and the request is not to the profile endpoint, - // refresh the participant token - return refreshParticipantToken(error); + // refresh the room member token + return refreshRoomMemberToken(error); } // Expired access token - if (!pageUrl.startsWith('/login')) { - // If the error occurred in a page that is not the login page, refresh the access token + if (!pageUrl.startsWith('/login') || !!tokenStorageService.getRefreshToken()) { + // If the error occurred in a page that is not the login page, + // or if there is a refresh token available, refresh the access token return refreshAccessToken(error); } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/models/custom-participant.model.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/models/custom-participant.model.ts index 8bc8a8dc..5153b6a6 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/models/custom-participant.model.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/models/custom-participant.model.ts @@ -1,12 +1,12 @@ -import { MeetTokenMetadata, ParticipantRole } from '@openvidu-meet/typings'; +import { MeetRoomMemberRole, MeetRoomMemberTokenMetadata } from '@openvidu-meet/typings'; import { ParticipantModel, ParticipantProperties } from 'openvidu-components-angular'; // Represents a participant in the application. export class CustomParticipantModel extends ParticipantModel { // Indicates the original role of the participant. - private _meetOriginalRole: ParticipantRole; + private _meetOriginalRole: MeetRoomMemberRole; // Indicates the current role of the participant. - private _meetRole: ParticipantRole; + private _meetRole: MeetRoomMemberRole; constructor(props: ParticipantProperties) { super(props); @@ -15,7 +15,7 @@ export class CustomParticipantModel extends ParticipantModel { this._meetRole = this._meetOriginalRole; } - set meetRole(role: ParticipantRole) { + set meetRole(role: MeetRoomMemberRole) { this._meetRole = role; } @@ -24,7 +24,7 @@ export class CustomParticipantModel extends ParticipantModel { * @returns True if the current role is moderator, false otherwise. */ isModerator(): boolean { - return this._meetRole === ParticipantRole.MODERATOR; + return this._meetRole === MeetRoomMemberRole.MODERATOR; } /** @@ -32,12 +32,12 @@ export class CustomParticipantModel extends ParticipantModel { * @returns True if the original role is moderator, false otherwise. */ isOriginalModerator(): boolean { - return this._meetOriginalRole === ParticipantRole.MODERATOR; + return this._meetOriginalRole === MeetRoomMemberRole.MODERATOR; } } -const extractParticipantRole = (metadata: any): ParticipantRole => { - let parsedMetadata: MeetTokenMetadata | undefined; +const extractParticipantRole = (metadata: any): MeetRoomMemberRole => { + let parsedMetadata: MeetRoomMemberTokenMetadata | undefined; try { parsedMetadata = JSON.parse(metadata || '{}'); } catch (e) { @@ -45,7 +45,7 @@ const extractParticipantRole = (metadata: any): ParticipantRole => { } if (!parsedMetadata || typeof parsedMetadata !== 'object') { - return ParticipantRole.SPEAKER; + return MeetRoomMemberRole.SPEAKER; } - return parsedMetadata.selectedRole || ParticipantRole.SPEAKER; + return parsedMetadata.role || MeetRoomMemberRole.SPEAKER; }; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/models/lobby.model.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/models/lobby.model.ts index 950e4429..d9098324 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/models/lobby.model.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/models/lobby.model.ts @@ -15,5 +15,5 @@ export interface LobbyState { backButtonText: string; isE2EEEnabled: boolean; participantForm: FormGroup; - participantToken: string; + roomMemberToken: string; } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/models/navigation.model.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/models/navigation.model.ts index ab795189..ae9e667b 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/models/navigation.model.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/models/navigation.model.ts @@ -6,8 +6,6 @@ export enum ErrorReason { INVALID_RECORDING_SECRET = 'invalid-recording-secret', INVALID_ROOM = 'invalid-room', INVALID_RECORDING = 'invalid-recording', - NO_RECORDINGS = 'no-recordings', UNAUTHORIZED_RECORDING_ACCESS = 'unauthorized-recording-access', - RECORDINGS_ADMIN_ONLY_ACCESS = 'recordings-admin-only-access', INTERNAL_ERROR = 'internal-error' } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/room-wizard.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/room-wizard.component.ts index ce7d2ea7..d5294a8c 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/room-wizard.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/room-wizard.component.ts @@ -4,10 +4,10 @@ import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { ActivatedRoute } from '@angular/router'; +import { MeetRoomOptions } from '@openvidu-meet/typings'; import { StepIndicatorComponent, WizardNavComponent } from '../../../../components'; import { WizardNavigationConfig, WizardStep } from '../../../../models'; import { NavigationService, NotificationService, RoomService, RoomWizardStateService } from '../../../../services'; -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'; @@ -145,7 +145,7 @@ export class RoomWizardComponent implements OnInit { } async createRoomAdvance() { - const roomOptions: BaseRoomOptions = this.wizardService.roomOptions(); + const roomOptions = this.wizardService.roomOptions(); console.log('Wizard completed with data:', roomOptions); // Activate loading state diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/error/error.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/error/error.component.ts index 93e96eb3..c819c900 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/error/error.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/error/error.component.ts @@ -7,10 +7,10 @@ import { ErrorReason } from '../../models'; import { AppDataService, AuthService, NavigationService, WebComponentManagerService } from '../../services'; @Component({ - selector: 'ov-error', - imports: [MatCardModule, MatIconModule, MatButtonModule], - templateUrl: './error.component.html', - styleUrl: './error.component.scss' + selector: 'ov-error', + imports: [MatCardModule, MatIconModule, MatButtonModule], + templateUrl: './error.component.html', + styleUrl: './error.component.scss' }) export class ErrorComponent implements OnInit { errorName = 'Error'; @@ -77,18 +77,10 @@ export class ErrorComponent implements OnInit { title: 'Invalid recording', message: 'The recording you are trying to access does not exist or has been deleted' }, - [ErrorReason.NO_RECORDINGS]: { - title: 'No recordings', - message: 'There are no recordings in this room or the room does not exist' - }, [ErrorReason.UNAUTHORIZED_RECORDING_ACCESS]: { title: 'Unauthorized recording access', message: 'You are not authorized to access the recordings in this room' }, - [ErrorReason.RECORDINGS_ADMIN_ONLY_ACCESS]: { - title: 'Unauthorized recording access', - message: 'Recordings access is configured for admins only in this room' - }, [ErrorReason.INTERNAL_ERROR]: { title: 'Internal error', message: 'An unexpected error occurred, please try again later' diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.html index 448eb3fc..98d02f6b 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.html +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.html @@ -15,7 +15,7 @@ } } @else { ([]); @@ -36,6 +42,7 @@ export class RoomRecordingsComponent implements OnInit { protected loggerService: LoggerService, protected recordingService: RecordingService, protected roomService: RoomService, + protected roomMemberService: RoomMemberService, protected notificationService: NotificationService, protected navigationService: NavigationService, protected route: ActivatedRoute @@ -45,7 +52,7 @@ export class RoomRecordingsComponent implements OnInit { async ngOnInit() { this.roomId = this.route.snapshot.paramMap.get('room-id')!; - this.canDeleteRecordings = this.recordingService.canDeleteRecordings(); + this.canDeleteRecordings = this.roomMemberService.canDeleteRecordings(); // Load recordings const delayLoader = setTimeout(() => { diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts index cca40eb8..3484b975 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts @@ -1,7 +1,6 @@ import { Routes } from '@angular/router'; +import { WebComponentProperty } from '@openvidu-meet/typings'; import { - checkParticipantRoleAndAuthGuard, - checkRecordingAuthGuard, checkRoomEditGuard, checkUserAuthenticatedGuard, checkUserNotAuthenticatedGuard, @@ -10,9 +9,11 @@ import { removeQueryParamsGuard, runGuardsSerially, validateRecordingAccessGuard, - validateRoomAccessGuard + validateRoomAccessGuard, + validateRoomRecordingsAccessGuard } from '../guards'; import { + ConfigComponent, ConsoleComponent, EmbeddedComponent, EndMeetingComponent, @@ -25,10 +26,8 @@ import { RoomsComponent, RoomWizardComponent, UsersPermissionsComponent, - ViewRecordingComponent, - ConfigComponent + ViewRecordingComponent } from '../pages'; -import { WebComponentProperty } from '@openvidu-meet/typings'; export const baseRoutes: Routes = [ { @@ -42,9 +41,8 @@ export const baseRoutes: Routes = [ canActivate: [ runGuardsSerially( extractRoomQueryParamsGuard, - removeQueryParamsGuard(['secret', WebComponentProperty.E2EE_KEY]), - checkParticipantRoleAndAuthGuard, - validateRoomAccessGuard + validateRoomAccessGuard, + removeQueryParamsGuard(['secret', WebComponentProperty.E2EE_KEY]) ) ] }, @@ -54,16 +52,15 @@ export const baseRoutes: Routes = [ canActivate: [ runGuardsSerially( extractRecordingQueryParamsGuard, - removeQueryParamsGuard(['secret', WebComponentProperty.E2EE_KEY]), - checkParticipantRoleAndAuthGuard, - validateRecordingAccessGuard + validateRoomRecordingsAccessGuard, + removeQueryParamsGuard(['secret', WebComponentProperty.E2EE_KEY]) ) ] }, { path: 'recording/:recording-id', component: ViewRecordingComponent, - canActivate: [checkRecordingAuthGuard] + canActivate: [validateRecordingAccessGuard] }, { path: 'disconnected', component: EndMeetingComponent }, { path: 'error', component: ErrorComponent }, diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/auth.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/auth.service.ts index 43d5ca1c..031730bc 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/auth.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/auth.service.ts @@ -1,6 +1,6 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { User, UserRole } from '@openvidu-meet/typings'; +import { MeetUserDTO, MeetUserRole } from '@openvidu-meet/typings'; import { HttpService, NavigationService, TokenStorageService } from '../services'; @Injectable({ @@ -11,7 +11,7 @@ export class AuthService { protected readonly USERS_API = `${HttpService.INTERNAL_API_PATH_PREFIX}/users`; protected hasCheckAuth = false; - protected user: User | null = null; + protected user: MeetUserDTO | null = null; constructor( protected httpService: HttpService, @@ -87,21 +87,21 @@ export class AuthService { return this.user?.username; } - async getUserRoles(): Promise { + async getUserRoles(): Promise { await this.getAuthenticatedUser(); return this.user?.roles; } async isAdmin(): Promise { const roles = await this.getUserRoles(); - return roles ? roles.includes(UserRole.ADMIN) : false; + return roles ? roles.includes(MeetUserRole.ADMIN) : false; } private async getAuthenticatedUser(force = false) { if (force || (!this.user && !this.hasCheckAuth)) { try { const path = `${this.USERS_API}/profile`; - const user = await this.httpService.getRequest(path); + const user = await this.httpService.getRequest(path); this.user = user; } catch (error) { this.user = null; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/feature-configuration.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/feature-configuration.service.ts index 55a05fb6..9f2cad9f 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/feature-configuration.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/feature-configuration.service.ts @@ -2,10 +2,9 @@ import { computed, Injectable, signal } from '@angular/core'; import { MeetAppearanceConfig, MeetRoomConfig, + MeetRoomMemberPermissions, + MeetRoomMemberRole, MeetRoomTheme, - ParticipantPermissions, - ParticipantRole, - RecordingPermissions, TrackSource } from '@openvidu-meet/typings'; import { LoggerService } from 'openvidu-components-angular'; @@ -78,18 +77,16 @@ export class FeatureConfigurationService { // Signals to handle reactive protected roomConfig = signal(undefined); - protected participantPermissions = signal(undefined); - protected participantRole = signal(undefined); - protected recordingPermissions = signal(undefined); + protected roomMemberRole = signal(undefined); + protected roomMemberPermissions = signal(undefined); protected appearanceConfig = signal(undefined); // Computed signal to derive features based on current configurations public readonly features = computed(() => this.calculateFeatures( this.roomConfig(), - this.participantPermissions(), - this.participantRole(), - this.recordingPermissions(), + this.roomMemberRole(), + this.roomMemberPermissions(), this.appearanceConfig() ) ); @@ -107,27 +104,19 @@ export class FeatureConfigurationService { } /** - * Updates participant permissions + * Updates room member role */ - setParticipantPermissions(permissions: ParticipantPermissions): void { - this.log.d('Updating participant permissions', permissions); - this.participantPermissions.set(permissions); + setRoomMemberRole(role: MeetRoomMemberRole): void { + this.log.d('Updating room member role', role); + this.roomMemberRole.set(role); } /** - * Updates participant role + * Updates room member permissions */ - setParticipantRole(role: ParticipantRole): void { - this.log.d('Updating participant role', role); - this.participantRole.set(role); - } - - /** - * Updates recording permissions - */ - setRecordingPermissions(permissions: RecordingPermissions): void { - this.log.d('Updating recording permissions', permissions); - this.recordingPermissions.set(permissions); + setRoomMemberPermissions(permissions: MeetRoomMemberPermissions): void { + this.log.d('Updating room member permissions', permissions); + this.roomMemberPermissions.set(permissions); } /** @@ -143,9 +132,8 @@ export class FeatureConfigurationService { */ protected calculateFeatures( roomConfig?: MeetRoomConfig, - participantPerms?: ParticipantPermissions, - role?: ParticipantRole, - recordingPerms?: RecordingPermissions, + role?: MeetRoomMemberRole, + permissions?: MeetRoomMemberPermissions, appearanceConfig?: MeetAppearanceConfig ): ApplicationFeatures { // Start with default configuration @@ -158,22 +146,23 @@ export class FeatureConfigurationService { features.showBackgrounds = roomConfig.virtualBackground.enabled; } - // Apply participant permissions (these can restrict enabled features) - if (participantPerms) { + // Apply room member permissions (these can restrict enabled features) + if (permissions) { // Only restrict if the feature is already enabled if (features.showRecordingPanel) { - features.canRecordRoom = participantPerms.openvidu.canRecord; + features.canRecordRoom = permissions.meet.canRecord; + features.canRetrieveRecordings = permissions.meet.canRetrieveRecordings; } if (features.showChat) { - features.showChat = participantPerms.openvidu.canChat; + features.showChat = permissions.meet.canChat; } if (features.showBackgrounds) { - features.showBackgrounds = participantPerms.openvidu.canChangeVirtualBackground; + features.showBackgrounds = permissions.meet.canChangeVirtualBackground; } // Media features - const canPublish = participantPerms.livekit.canPublish; - const canPublishSources = participantPerms.livekit.canPublishSources ?? []; + const canPublish = permissions.livekit.canPublish; + const canPublishSources = permissions.livekit.canPublishSources ?? []; features.videoEnabled = canPublish || canPublishSources.includes(TrackSource.CAMERA); features.audioEnabled = canPublish || canPublishSources.includes(TrackSource.MICROPHONE); features.showCamera = features.videoEnabled; @@ -183,12 +172,7 @@ export class FeatureConfigurationService { // Apply role-based configurations if (role) { - features.canModerateRoom = role === ParticipantRole.MODERATOR; - } - - // Apply recording permissions - if (recordingPerms) { - features.canRetrieveRecordings = recordingPerms.canRetrieveRecordings; + features.canModerateRoom = role === MeetRoomMemberRole.MODERATOR; } // Apply appearance configuration @@ -213,9 +197,8 @@ export class FeatureConfigurationService { */ reset(): void { this.roomConfig.set(undefined); - this.participantPermissions.set(undefined); - this.participantRole.set(undefined); - this.recordingPermissions.set(undefined); + this.roomMemberRole.set(undefined); + this.roomMemberPermissions.set(undefined); this.appearanceConfig.set(undefined); } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/global-config.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/global-config.service.ts index 697c24ae..8a3f4065 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/global-config.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/global-config.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; -import { FeatureConfigurationService, HttpService } from '../services'; -import { AuthMode, AuthTransportMode, MeetAppearanceConfig, SecurityConfig, WebhookConfig } from '@openvidu-meet/typings'; +import { AuthMode, MeetAppearanceConfig, SecurityConfig, WebhookConfig } from '@openvidu-meet/typings'; import { LoggerService } from 'openvidu-components-angular'; +import { FeatureConfigurationService, HttpService } from '../services'; @Injectable({ providedIn: 'root' @@ -41,11 +41,6 @@ export class GlobalConfigService { return this.securityConfig!.authentication.authModeToAccessRoom; } - async getAuthTransportMode(): Promise { - await this.getSecurityConfig(); - return this.securityConfig!.authentication.authTransportMode; - } - async saveSecurityConfig(config: SecurityConfig) { const path = `${this.GLOBAL_CONFIG_API}/security`; await this.httpService.putRequest(path, config); @@ -74,12 +69,12 @@ export class GlobalConfigService { async loadRoomsAppearanceConfig(): Promise { try { - const config = await this.getRoomsAppearanceConfig(); - this.featureConfService.setAppearanceConfig(config.appearance); - } catch (error) { - this.log.e('Error loading rooms appearance config:', error); - throw error; - } + const config = await this.getRoomsAppearanceConfig(); + this.featureConfService.setAppearanceConfig(config.appearance); + } catch (error) { + this.log.e('Error loading rooms appearance config:', error); + throw error; + } } async saveRoomsAppearanceConfig(config: MeetAppearanceConfig) { diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/index.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/index.ts index d8cc3329..10883b32 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/index.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/index.ts @@ -4,7 +4,7 @@ export * from './auth.service'; export * from './api-key.service'; export * from './global-config.service'; export * from './room.service'; -export * from './participant.service'; +export * from './room-member.service'; export * from './meeting/meeting.service'; export * from './meeting/meeting-lobby.service'; export * from './meeting/meeting-plugin-manager.service'; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-event-handler.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-event-handler.service.ts index 74a2e3c3..c7a1ac18 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-event-handler.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-event-handler.service.ts @@ -1,34 +1,34 @@ import { Injectable, inject } from '@angular/core'; -import { - Room, - RoomEvent, - DataPacket_Kind, - RemoteParticipant, - ParticipantLeftEvent, - ParticipantLeftReason, - RecordingStartRequestedEvent, - RecordingStopRequestedEvent, - ParticipantModel -} from 'openvidu-components-angular'; -import { - FeatureConfigurationService, - RecordingService, - ParticipantService, - RoomService, - SessionStorageService, - TokenStorageService, - WebComponentManagerService, - NavigationService -} from '../../services'; import { LeftEventReason, - MeetSignalType, MeetParticipantRoleUpdatedPayload, MeetRoomConfigUpdatedPayload, + MeetSignalType, WebComponentEvent, WebComponentOutboundEventMessage } from '@openvidu-meet/typings'; +import { + DataPacket_Kind, + ParticipantLeftEvent, + ParticipantLeftReason, + ParticipantModel, + RecordingStartRequestedEvent, + RecordingStopRequestedEvent, + RemoteParticipant, + Room, + RoomEvent +} from 'openvidu-components-angular'; import { CustomParticipantModel } from '../../models'; +import { + FeatureConfigurationService, + NavigationService, + RecordingService, + RoomMemberService, + RoomService, + SessionStorageService, + TokenStorageService, + WebComponentManagerService +} from '../../services'; /** * Service that handles all LiveKit/OpenVidu room events. @@ -56,7 +56,7 @@ export class MeetingEventHandlerService { // Injected services protected featureConfService = inject(FeatureConfigurationService); protected recordingService = inject(RecordingService); - protected participantService = inject(ParticipantService); + protected roomMemberService = inject(RoomMemberService); protected roomService = inject(RoomService); protected sessionStorageService = inject(SessionStorageService); protected tokenStorageService = inject(TokenStorageService); @@ -109,11 +109,8 @@ export class MeetingEventHandlerService { switch (topic) { case 'recordingStopped': - await this.handleRecordingStopped( - context.roomId, - context.roomSecret, - context.onHasRecordingsChanged - ); + // Notify that recordings are now available + context.onHasRecordingsChanged(true); break; case MeetSignalType.MEET_ROOM_CONFIG_UPDATED: @@ -188,11 +185,14 @@ export class MeetingEventHandlerService { }; this.wcManagerService.sendMessageToParent(message); - // Clean up storage (except on browser unload) + // Clear participant identity and token + this.roomMemberService.clearParticipantIdentity(); + this.tokenStorageService.clearRoomMemberToken(); + + // Clean up room secret and e2ee key (if any), except on browser unload) if (event.reason !== ParticipantLeftReason.BROWSER_UNLOAD) { this.sessionStorageService.removeRoomSecret(); - this.tokenStorageService.clearParticipantToken(); - this.tokenStorageService.clearRecordingToken(); + this.sessionStorageService.removeE2EEKey(); } // Navigate to disconnected page @@ -250,29 +250,9 @@ export class MeetingEventHandlerService { // PRIVATE METHODS - Event Handlers // ============================================ - /** - * Handles recording stopped event. - * Updates hasRecordings flag and refreshes recording token. - */ - private async handleRecordingStopped( - roomId: string, - roomSecret: string, - onHasRecordingsChanged: (hasRecordings: boolean) => void - ): Promise { - // Notify that recordings are now available - onHasRecordingsChanged(true); - - try { - // Refresh recording token to view recordings - await this.recordingService.generateRecordingToken(roomId, roomSecret); - } catch (error) { - console.error('Error refreshing recording token:', error); - } - } - /** * Handles room config updated event. - * Updates feature config and refreshes recording token if needed. + * Updates feature config and refreshes room member token if needed. */ private async handleRoomConfigUpdated( event: MeetRoomConfigUpdatedPayload, @@ -284,19 +264,26 @@ export class MeetingEventHandlerService { // Update feature configuration this.featureConfService.setRoomConfig(config); - // Refresh recording token if recording is enabled + // Refresh room member token if recording is enabled if (config.recording.enabled) { try { - await this.recordingService.generateRecordingToken(roomId, roomSecret); + const participantName = this.roomMemberService.getParticipantName(); + const participantIdentity = this.roomMemberService.getParticipantIdentity(); + await this.roomMemberService.generateToken(roomId, { + secret: roomSecret, + grantJoinMeetingPermission: true, + participantName, + participantIdentity + }); } catch (error) { - console.error('Error refreshing recording token:', error); + console.error('Error refreshing room member token:', error); } } } /** * Handles participant role updated event. - * Updates local or remote participant role and refreshes token if needed. + * Updates local or remote participant role and refreshes room member token if needed. */ private async handleParticipantRoleUpdated( event: MeetParticipantRoleUpdatedPayload, @@ -320,9 +307,9 @@ export class MeetingEventHandlerService { try { // Refresh participant token with new role - await this.participantService.refreshParticipantToken({ - roomId, + await this.roomMemberService.generateToken(roomId, { secret, + grantJoinMeetingPermission: true, participantName, participantIdentity }); @@ -334,7 +321,7 @@ export class MeetingEventHandlerService { // Notify component that participant role was updated onParticipantRoleUpdated?.(); } catch (error) { - console.error('Error refreshing participant token:', error); + console.error('Error refreshing room member token:', error); } } else { // Update remote participant role diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-lobby.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-lobby.service.ts index f16d9918..029d42f8 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-lobby.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-lobby.service.ts @@ -1,18 +1,18 @@ import { inject, Injectable } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { MeetRoomStatus } from '@openvidu-meet/typings'; import { - AuthService, - RecordingService, - RoomService, - ParticipantService, - NavigationService, AppDataService, + AuthService, + NavigationService, + RecordingService, + RoomMemberService, + RoomService, WebComponentManagerService } from '..'; -import { MeetRoomStatus } from '@openvidu-meet/typings'; -import { LobbyState } from '../../models/lobby.model'; import { ErrorReason } from '../../models'; -import { ActivatedRoute } from '@angular/router'; +import { LobbyState } from '../../models/lobby.model'; /** * Service that manages the meeting lobby state and operations. @@ -41,13 +41,13 @@ export class MeetingLobbyService { name: new FormControl('', [Validators.required]), e2eeKey: new FormControl('') }), - participantToken: '' + roomMemberToken: '' }; protected roomService: RoomService = inject(RoomService); protected recordingService: RecordingService = inject(RecordingService); protected authService: AuthService = inject(AuthService); - protected participantService: ParticipantService = inject(ParticipantService); + protected roomMemberService: RoomMemberService = inject(RoomMemberService); protected navigationService: NavigationService = inject(NavigationService); protected appDataService: AppDataService = inject(AppDataService); protected wcManagerService: WebComponentManagerService = inject(WebComponentManagerService); @@ -172,7 +172,7 @@ export class MeetingLobbyService { return; } - await this.generateParticipantToken(); + await this.generateRoomMemberToken(); await this.addParticipantNameToUrl(); await this.roomService.loadRoomConfig(this.state.roomId); } @@ -199,18 +199,14 @@ export class MeetingLobbyService { /** * Checks if there are recordings in the room and updates the visibility of the recordings card. * - * It is necessary to previously generate a recording token in order to list the recordings. - * If token generation fails or the user does not have sufficient permissions to list recordings, - * the error will be caught and the recordings card will be hidden (`showRecordingCard` will be set to `false`). + * If the user does not have sufficient permissions to list recordings, + * the recordings card will be hidden (`showRecordingCard` will be set to `false`). * * If recordings exist, sets `showRecordingCard` to `true`; otherwise, to `false`. */ protected async checkForRecordings(): Promise { try { - const { canRetrieveRecordings } = await this.recordingService.generateRecordingToken( - this.state.roomId, - this.state.roomSecret - ); + const canRetrieveRecordings = this.roomMemberService.canRetrieveRecordings(); if (!canRetrieveRecordings) { this.state.showRecordingCard = false; @@ -234,15 +230,15 @@ export class MeetingLobbyService { /** * Initializes the participant name in the form control. * - * Retrieves the participant name from the ParticipantTokenService first, and if not available, + * Retrieves the participant name from the RoomMemberService first, and if not available, * falls back to the authenticated username. Sets the retrieved name value in the * participant form's 'name' control if a valid name is found. * * @returns A promise that resolves when the participant name has been initialized */ protected async initializeParticipantName(): Promise { - // Apply participant name from ParticipantTokenService if set, otherwise use authenticated username - const currentParticipantName = this.participantService.getParticipantName(); + // Apply participant name from RoomMemberService if set, otherwise use authenticated username + const currentParticipantName = this.roomMemberService.getParticipantName(); const username = await this.authService.getUsername(); const participantName = currentParticipantName || username; @@ -252,24 +248,24 @@ export class MeetingLobbyService { } /** - * Generates a participant token for joining a meeting. + * Generates a room member token for joining a meeting. * - * @throws When participant already exists in the room (status 409) * @returns Promise that resolves when token is generated */ - protected async generateParticipantToken() { + protected async generateRoomMemberToken() { try { - this.state.participantToken = await this.participantService.generateToken( + this.state.roomMemberToken = await this.roomMemberService.generateToken( + this.state.roomId, { - roomId: this.state.roomId, secret: this.state.roomSecret, + grantJoinMeetingPermission: true, participantName: this.participantName }, this.e2eeKey ); - this.participantName = this.participantService.getParticipantName()!; + this.participantName = this.roomMemberService.getParticipantName()!; } catch (error: any) { - console.error('Error generating participant token:', error); + console.error('Error generating room member token:', error); switch (error.status) { case 400: // Invalid secret @@ -287,7 +283,7 @@ export class MeetingLobbyService { await this.navigationService.redirectToErrorPage(ErrorReason.INTERNAL_ERROR, true); } - throw new Error('Error generating participant token'); + throw new Error('Error generating room member token'); } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-plugin-manager.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-plugin-manager.service.ts index 6df06633..c7d1c513 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-plugin-manager.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-plugin-manager.service.ts @@ -1,7 +1,7 @@ -import { Injectable, Optional, Inject } from '@angular/core'; +import { Inject, Injectable, Optional } from '@angular/core'; +import { MEETING_ACTION_HANDLER_TOKEN, MeetingActionHandler, ParticipantControls } from '../../customization'; import { CustomParticipantModel, LobbyState } from '../../models'; -import { MeetingActionHandler, MEETING_ACTION_HANDLER_TOKEN, ParticipantControls } from '../../customization'; -import { ParticipantService } from '../participant.service'; +import { RoomMemberService } from '../room-member.service'; /** * Service that manages plugin inputs and configurations for the MeetingComponent. @@ -19,7 +19,7 @@ import { ParticipantService } from '../participant.service'; @Injectable() export class MeetingPluginManagerService { constructor( - private participantService: ParticipantService, + private roomMemberService: RoomMemberService, @Optional() @Inject(MEETING_ACTION_HANDLER_TOKEN) private actionHandler?: MeetingActionHandler ) {} @@ -167,7 +167,7 @@ export class MeetingPluginManagerService { */ protected getDefaultParticipantControls(participant: CustomParticipantModel): ParticipantControls { const isCurrentUser = participant.isLocal; - const currentUserIsModerator = this.participantService.isModeratorParticipant(); + const currentUserIsModerator = this.roomMemberService.isModerator(); const participantIsModerator = participant.isModerator(); const participantIsOriginalModerator = participant.isOriginalModerator(); diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting.service.ts index 0399f432..0fddd420 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import { HttpService, ParticipantService } from '..'; import { LoggerService } from 'openvidu-components-angular'; +import { HttpService } from '..'; @Injectable({ providedIn: 'root' @@ -12,8 +12,7 @@ export class MeetingService { constructor( protected loggerService: LoggerService, - protected httpService: HttpService, - protected participantService: ParticipantService + protected httpService: HttpService ) { this.log = this.loggerService.get('OpenVidu Meet - MeetingService'); } @@ -26,8 +25,7 @@ export class MeetingService { */ async endMeeting(roomId: string): Promise { const path = `${this.MEETINGS_API}/${roomId}`; - const headers = this.participantService.getParticipantRoleHeader(); - return this.httpService.deleteRequest(path, headers); + return this.httpService.deleteRequest(path); } /** @@ -39,8 +37,7 @@ export class MeetingService { */ async kickParticipant(roomId: string, participantIdentity: string): Promise { const path = `${this.MEETINGS_API}/${roomId}/participants/${participantIdentity}`; - const headers = this.participantService.getParticipantRoleHeader(); - await this.httpService.deleteRequest(path, headers); + await this.httpService.deleteRequest(path); this.log.d(`Participant '${participantIdentity}' kicked from room '${roomId}'`); } @@ -53,9 +50,8 @@ export class MeetingService { */ async changeParticipantRole(roomId: string, participantIdentity: string, newRole: string): Promise { const path = `${this.MEETINGS_API}/${roomId}/participants/${participantIdentity}/role`; - const headers = this.participantService.getParticipantRoleHeader(); const body = { role: newRole }; - await this.httpService.putRequest(path, body, headers); + await this.httpService.putRequest(path, body); this.log.d(`Changed role of participant '${participantIdentity}' to '${newRole}' in room '${roomId}'`); } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/participant.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/participant.service.ts deleted file mode 100644 index d4f2bc54..00000000 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/participant.service.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Injectable } from '@angular/core'; -import { FeatureConfigurationService, GlobalConfigService, HttpService, TokenStorageService } from '../services'; -import { - AuthTransportMode, - MeetTokenMetadata, - ParticipantOptions, - ParticipantPermissions, - ParticipantRole -} from '@openvidu-meet/typings'; -import { getValidDecodedToken } from '../utils'; -import { E2eeService, LoggerService } from 'openvidu-components-angular'; - -@Injectable({ - providedIn: 'root' -}) -export class ParticipantService { - protected readonly PARTICIPANTS_API = `${HttpService.INTERNAL_API_PATH_PREFIX}/participants`; - protected readonly PARTICIPANT_NAME_KEY = 'ovMeet-participantName'; - - protected participantName?: string; - protected participantIdentity?: string; - protected role: ParticipantRole = ParticipantRole.SPEAKER; - protected permissions?: ParticipantPermissions; - - protected log; - - constructor( - protected loggerService: LoggerService, - protected httpService: HttpService, - protected featureConfService: FeatureConfigurationService, - protected globalConfigService: GlobalConfigService, - protected tokenStorageService: TokenStorageService, - protected e2eeService: E2eeService - ) { - this.log = this.loggerService.get('OpenVidu Meet - ParticipantTokenService'); - } - - setParticipantName(participantName: string): void { - this.participantName = participantName; - localStorage.setItem(this.PARTICIPANT_NAME_KEY, participantName); - } - - getParticipantName(): string | undefined { - return this.participantName || localStorage.getItem(this.PARTICIPANT_NAME_KEY) || undefined; - } - - getParticipantIdentity(): string | undefined { - return this.participantIdentity; - } - - /** - * Generates a participant token and extracts role/permissions - * - * @param participantOptions - The options for the participant, including room ID, participant name, and secret - * @return A promise that resolves to the participant token - */ - async generateToken(participantOptions: ParticipantOptions, e2EEKey = ''): Promise { - const path = `${this.PARTICIPANTS_API}/token`; - - if (participantOptions.participantName && !!e2EEKey) { - // Asign E2EE key and encrypt participant name - await this.e2eeService.setE2EEKey(e2EEKey); - participantOptions.participantName = await this.e2eeService.encrypt(participantOptions.participantName); - } - - const { token } = await this.httpService.postRequest<{ token: string }>(path, participantOptions); - - // Store token in sessionStorage for header mode - const authTransportMode = await this.globalConfigService.getAuthTransportMode(); - if (authTransportMode === AuthTransportMode.HEADER) { - this.tokenStorageService.setParticipantToken(token); - } - - await this.updateParticipantTokenInfo(token); - return token; - } - - /** - * Refreshes the participant token using the provided options. - * - * @param participantOptions - The options for the participant, including room ID, participant name, and secret - * @return A promise that resolves to the refreshed participant token - */ - async refreshParticipantToken(participantOptions: ParticipantOptions): Promise { - const path = `${this.PARTICIPANTS_API}/token/refresh`; - const { token } = await this.httpService.postRequest<{ token: string }>(path, participantOptions); - - // Store token in sessionStorage for header mode - const authTransportMode = await this.globalConfigService.getAuthTransportMode(); - if (authTransportMode === AuthTransportMode.HEADER) { - this.tokenStorageService.setParticipantToken(token); - } - - await this.updateParticipantTokenInfo(token); - return token; - } - - /** - * Updates the current participant token information, including role and permissions. - * - * @param token - The JWT token to set. - * @throws Error if the token is invalid or expired. - */ - protected async updateParticipantTokenInfo(token: string): Promise { - try { - const decodedToken = getValidDecodedToken(token); - const metadata = decodedToken.metadata as MeetTokenMetadata; - - if (decodedToken.sub && decodedToken.name) { - const decryptedName = await this.e2eeService.decryptOrMask(decodedToken.name); - this.setParticipantName(decryptedName); - this.participantIdentity = decodedToken.sub; - } - - this.role = metadata.selectedRole; - const openviduPermissions = metadata.roles.find((r) => r.role === this.role)!.permissions; - this.permissions = { - livekit: decodedToken.video, - openvidu: openviduPermissions - }; - - // Update feature configuration - this.featureConfService.setParticipantRole(this.role); - this.featureConfService.setParticipantPermissions(this.permissions); - } catch (error) { - this.log.e('Error setting participant token and associated data', error); - throw new Error('Error setting participant token'); - } - } - - setParticipantRole(participantRole: ParticipantRole): void { - this.role = participantRole; - this.featureConfService.setParticipantRole(this.role); - } - - getParticipantRole(): ParticipantRole { - return this.role; - } - - isModeratorParticipant(): boolean { - return this.getParticipantRole() === ParticipantRole.MODERATOR; - } - - getParticipantPermissions(): ParticipantPermissions | undefined { - return this.permissions; - } - - getParticipantRoleHeader(): Record { - return { 'x-participant-role': this.getParticipantRole() }; - } -} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/recording.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/recording.service.ts index 610e6a03..fb6fb0a7 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/recording.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/recording.service.ts @@ -1,22 +1,15 @@ import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; +import { MeetRecordingFilters, MeetRecordingInfo } from '@openvidu-meet/typings'; +import { LoggerService } from 'openvidu-components-angular'; import { ShareRecordingDialogComponent } from '../components'; import { AuthService, FeatureConfigurationService, GlobalConfigService, HttpService, - TokenStorageService, - ParticipantService + TokenStorageService } from '../services'; -import { - AuthTransportMode, - MeetRecordingFilters, - MeetRecordingInfo, - RecordingPermissions -} from '@openvidu-meet/typings'; -import { getValidDecodedToken } from '../utils'; -import { LoggerService } from 'openvidu-components-angular'; @Injectable({ providedIn: 'root' @@ -25,17 +18,11 @@ export class RecordingService { protected readonly RECORDINGS_API = `${HttpService.API_PATH_PREFIX}/recordings`; protected readonly INTERNAL_RECORDINGS_API = `${HttpService.INTERNAL_API_PATH_PREFIX}/recordings`; - protected recordingPermissions: RecordingPermissions = { - canRetrieveRecordings: false, - canDeleteRecordings: false - }; - protected log; constructor( protected loggerService: LoggerService, private httpService: HttpService, - protected participantService: ParticipantService, protected authService: AuthService, protected featureConfService: FeatureConfigurationService, protected globalConfigService: GlobalConfigService, @@ -53,8 +40,7 @@ export class RecordingService { */ async startRecording(roomId: string): Promise { try { - const headers = this.participantService.getParticipantRoleHeader(); - return this.httpService.postRequest(this.INTERNAL_RECORDINGS_API, { roomId }, headers); + return this.httpService.postRequest(this.INTERNAL_RECORDINGS_API, { roomId }); } catch (error) { console.error('Error starting recording:', error); throw error; @@ -74,8 +60,7 @@ export class RecordingService { try { const path = `${this.INTERNAL_RECORDINGS_API}/${recordingId}/stop`; - const headers = this.participantService.getParticipantRoleHeader(); - return this.httpService.postRequest(path, {}, headers); + return this.httpService.postRequest(path, {}); } catch (error) { console.error('Error stopping recording:', error); throw error; @@ -152,15 +137,15 @@ export class RecordingService { if (secret) { params.append('secret', secret); } else { - // Otherwise, try to use access and/or recording token from sessionStorage (header mode) + // Otherwise, try to use access and/or room member token from sessionStorage const accessToken = this.tokenStorageService.getAccessToken(); if (accessToken) { params.append('accessToken', accessToken); } - const recordingToken = this.tokenStorageService.getRecordingToken(); - if (recordingToken) { - params.append('recordingToken', recordingToken); + const roomMemberToken = this.tokenStorageService.getRoomMemberToken(); + if (roomMemberToken) { + params.append('roomMemberToken', roomMemberToken); } } @@ -181,62 +166,6 @@ export class RecordingService { return this.httpService.getRequest(path); } - /** - * Generates a token for accessing recordings in a room - * - * @param roomId - The ID of the room for which the token is generated - * @param secret - The secret for the room - * @return A promise that resolves to an object containing the recording permissions - */ - async generateRecordingToken(roomId: string, secret: string): Promise { - const path = `${HttpService.INTERNAL_API_PATH_PREFIX}/rooms/${roomId}/recording-token`; - - try { - const { token } = await this.httpService.postRequest<{ token: string }>(path, { secret }); - - // Store token in sessionStorage for header mode - const authTransportMode = await this.globalConfigService.getAuthTransportMode(); - if (authTransportMode === AuthTransportMode.HEADER) { - this.tokenStorageService.setRecordingToken(token); - } - - this.setRecordingPermissionsFromToken(token); - return this.recordingPermissions; - } catch (error) { - this.featureConfService.setRecordingPermissions({ - canRetrieveRecordings: false, - canDeleteRecordings: false - }); - throw error; - } - } - - /** - * Sets recording permissions from a token - * - * @param token - The JWT token containing recording permissions - */ - protected setRecordingPermissionsFromToken(token: string) { - try { - const decodedToken = getValidDecodedToken(token); - this.recordingPermissions = decodedToken.metadata.recordingPermissions; - - // Update feature configuration - this.featureConfService.setRecordingPermissions(this.recordingPermissions); - } catch (error) { - this.log.e('Error setting recording permissions from token', error); - throw new Error('Error setting recording permissions from token'); - } - } - - canRetrieveRecordings(): boolean { - return this.recordingPermissions.canRetrieveRecordings; - } - - canDeleteRecordings(): boolean { - return this.recordingPermissions.canDeleteRecordings; - } - /** * Deletes a recording by ID * @@ -310,9 +239,9 @@ export class RecordingService { params.append('accessToken', accessToken); } - const recordingToken = this.tokenStorageService.getRecordingToken(); - if (recordingToken) { - params.append('recordingToken', recordingToken); + const roomMemberToken = this.tokenStorageService.getRoomMemberToken(); + if (roomMemberToken) { + params.append('roomMemberToken', roomMemberToken); } const downloadUrl = `${this.RECORDINGS_API}/download?${params.toString()}`; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/room-member.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/room-member.service.ts new file mode 100644 index 00000000..ea499ddc --- /dev/null +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/room-member.service.ts @@ -0,0 +1,130 @@ +import { Injectable } from '@angular/core'; +import { + MeetRoomMemberPermissions, + MeetRoomMemberRole, + MeetRoomMemberTokenMetadata, + MeetRoomMemberTokenOptions +} from '@openvidu-meet/typings'; +import { E2eeService, LoggerService } from 'openvidu-components-angular'; +import { FeatureConfigurationService, HttpService, TokenStorageService } from '.'; +import { getValidDecodedToken } from '../utils'; + +@Injectable({ + providedIn: 'root' +}) +export class RoomMemberService { + protected readonly PARTICIPANT_NAME_KEY = 'ovMeet-participantName'; + + protected participantName?: string; + protected participantIdentity?: string; + protected role: MeetRoomMemberRole = MeetRoomMemberRole.SPEAKER; + protected permissions?: MeetRoomMemberPermissions; + + protected log; + + constructor( + protected loggerService: LoggerService, + protected httpService: HttpService, + protected featureConfService: FeatureConfigurationService, + protected tokenStorageService: TokenStorageService, + protected e2eeService: E2eeService + ) { + this.log = this.loggerService.get('OpenVidu Meet - ParticipantTokenService'); + } + + setParticipantName(participantName: string): void { + this.participantName = participantName; + localStorage.setItem(this.PARTICIPANT_NAME_KEY, participantName); + } + + getParticipantName(): string | undefined { + return this.participantName || localStorage.getItem(this.PARTICIPANT_NAME_KEY) || undefined; + } + + getParticipantIdentity(): string | undefined { + return this.participantIdentity; + } + + clearParticipantIdentity(): void { + this.participantIdentity = undefined; + } + + /** + * Generates a room member token and extracts role/permissions + * + * @param tokenOptions - The options for the token generation + * @return A promise that resolves to the room member token + */ + async generateToken(roomId: string, tokenOptions: MeetRoomMemberTokenOptions, e2eeKey?: string): Promise { + if (tokenOptions.participantName && e2eeKey) { + // Assign E2EE key and encrypt participant name + await this.e2eeService.setE2EEKey(e2eeKey); + const encryptedName = await this.e2eeService.encrypt(tokenOptions.participantName); + tokenOptions.participantName = encryptedName; + } + + const path = `${HttpService.INTERNAL_API_PATH_PREFIX}/rooms/${roomId}/token`; + const { token } = await this.httpService.postRequest<{ token: string }>(path, tokenOptions); + + this.tokenStorageService.setRoomMemberToken(token); + await this.updateRoomMemberTokenInfo(token); + return token; + } + + /** + * Updates the current room member token information, including role and permissions. + * + * @param token - The JWT token to set. + * @throws Error if the token is invalid or expired. + */ + protected async updateRoomMemberTokenInfo(token: string): Promise { + try { + const decodedToken = getValidDecodedToken(token); + const metadata = decodedToken.metadata as MeetRoomMemberTokenMetadata; + + if (decodedToken.sub && decodedToken.name) { + const decryptedName = await this.e2eeService.decrypt(decodedToken.name); + this.setParticipantName(decryptedName); + this.participantIdentity = decodedToken.sub; + } + + this.role = metadata.role; + this.permissions = { + livekit: decodedToken.video, + meet: metadata.permissions + }; + + // Update feature configuration + this.featureConfService.setRoomMemberRole(this.role); + this.featureConfService.setRoomMemberPermissions(this.permissions); + } catch (error) { + this.log.e('Error updating room member token info', error); + throw new Error('Error updating room member token info'); + } + } + + setRoomMemberRole(role: MeetRoomMemberRole): void { + this.role = role; + this.featureConfService.setRoomMemberRole(this.role); + } + + getRoomMemberRole(): MeetRoomMemberRole { + return this.role; + } + + isModerator(): boolean { + return this.getRoomMemberRole() === MeetRoomMemberRole.MODERATOR; + } + + getRoomMemberPermissions(): MeetRoomMemberPermissions | undefined { + return this.permissions; + } + + canRetrieveRecordings(): boolean { + return this.permissions?.meet.canRetrieveRecordings ?? false; + } + + canDeleteRecordings(): boolean { + return this.permissions?.meet.canDeleteRecordings ?? false; + } +} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/room.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/room.service.ts index 6e9d2787..fab2ad23 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/room.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/room.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { FeatureConfigurationService, HttpService, ParticipantService, SessionStorageService } from '../services'; import { MeetRoom, MeetRoomConfig, @@ -7,11 +6,12 @@ import { MeetRoomDeletionPolicyWithRecordings, MeetRoomDeletionSuccessCode, MeetRoomFilters, + MeetRoomMemberRoleAndPermissions, MeetRoomOptions, - MeetRoomRoleAndPermissions, MeetRoomStatus } from '@openvidu-meet/typings'; import { LoggerService } from 'openvidu-components-angular'; +import { FeatureConfigurationService, HttpService, SessionStorageService } from '../services'; @Injectable({ providedIn: 'root' @@ -29,7 +29,6 @@ export class RoomService { constructor( protected loggerService: LoggerService, protected httpService: HttpService, - protected participantService: ParticipantService, protected featureConfService: FeatureConfigurationService, protected sessionStorageService: SessionStorageService ) { @@ -51,18 +50,19 @@ export class RoomService { } } + getRoomSecret(): string { + return this.roomSecret; + } + setE2EEKey(e2eeKey: string) { this.e2eeKey = e2eeKey; + this.sessionStorageService.setE2EEKey(e2eeKey); } getE2EEKey(): string { return this.e2eeKey; } - getRoomSecret(): string { - return this.roomSecret; - } - /** * Creates a new room with the specified options. * @@ -118,8 +118,7 @@ export class RoomService { */ async getRoom(roomId: string): Promise { const path = `${this.ROOMS_API}/${roomId}`; - const headers = this.participantService.getParticipantRoleHeader(); - return this.httpService.getRequest(path, headers); + return this.httpService.getRequest(path); } /** @@ -195,8 +194,7 @@ export class RoomService { try { const path = `${this.ROOMS_API}/${roomId}/config`; - const headers = this.participantService.getParticipantRoleHeader(); - const config = await this.httpService.getRequest(path, headers); + const config = await this.httpService.getRequest(path); return config; } catch (error) { this.log.e('Error fetching room config', error); @@ -240,7 +238,7 @@ export class RoomService { * @param secret - The secret parameter for the room * @returns A promise that resolves to an object containing the role and permissions */ - async getRoomRoleAndPermissions(roomId: string, secret: string): Promise { + async getRoomMemberRoleAndPermissions(roomId: string, secret: string): Promise { const path = `${this.INTERNAL_ROOMS_API}/${roomId}/roles/${secret}`; return this.httpService.getRequest(path); } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/session-storage.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/session-storage.service.ts index b2fad357..b10eec60 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/session-storage.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/session-storage.service.ts @@ -10,6 +10,7 @@ import { Injectable } from '@angular/core'; export class SessionStorageService { private readonly ROOM_SECRET_KEY = 'ovMeet-roomSecret'; private readonly REDIRECT_URL_KEY = 'ovMeet-redirectUrl'; + private readonly E2EE_KEY = 'ovMeet-e2eeKey'; /** * Stores the room secret. @@ -54,6 +55,31 @@ export class SessionStorageService { return this.get(this.REDIRECT_URL_KEY); } + /** + * Stores the E2EE key. + * + * @param e2eeKey The E2EE key to store. + */ + public setE2EEKey(e2eeKey: string): void { + this.set(this.E2EE_KEY, e2eeKey); + } + + /** + * Retrieves the E2EE key. + * + * @returns The stored E2EE key or null if not found. + */ + public getE2EEKey(): string | null { + return this.get(this.E2EE_KEY); + } + + /** + * Removes the E2EE key. + */ + public removeE2EEKey(): void { + this.remove(this.E2EE_KEY); + } + /** * Clears all data stored in sessionStorage. */ diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/token-storage.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/token-storage.service.ts index 905791d9..079e775f 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/token-storage.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/token-storage.service.ts @@ -10,8 +10,7 @@ import { Injectable } from '@angular/core'; export class TokenStorageService { private readonly ACCESS_TOKEN_KEY = 'ovMeet-accessToken'; private readonly REFRESH_TOKEN_KEY = 'ovMeet-refreshToken'; - private readonly PARTICIPANT_TOKEN_KEY = 'ovMeet-participantToken'; - private readonly RECORDING_TOKEN_KEY = 'ovMeet-recordingToken'; + private readonly ROOM_MEMBER_TOKEN_KEY = 'ovMeet-roomMemberToken'; // ACCESS AND REFRESH TOKEN METHODS @@ -41,36 +40,21 @@ export class TokenStorageService { localStorage.removeItem(this.REFRESH_TOKEN_KEY); } - // PARTICIPANT AND RECORDING TOKEN METHODS - // Uses sessionStorage instead of localStorage to ensure tokens are not shared across browser tabs + // ROOM MEMBER TOKEN METHODS + // Uses sessionStorage instead of localStorage to ensure token is not shared across browser tabs - // Saves the participant token to sessionStorage - setParticipantToken(token: string): void { - sessionStorage.setItem(this.PARTICIPANT_TOKEN_KEY, token); + // Saves the room member token to sessionStorage + setRoomMemberToken(token: string): void { + sessionStorage.setItem(this.ROOM_MEMBER_TOKEN_KEY, token); } - // Retrieves the participant token from sessionStorage - getParticipantToken(): string | null { - return sessionStorage.getItem(this.PARTICIPANT_TOKEN_KEY); + // Retrieves the room member token from sessionStorage + getRoomMemberToken(): string | null { + return sessionStorage.getItem(this.ROOM_MEMBER_TOKEN_KEY); } - // Removes the participant token from sessionStorage - clearParticipantToken(): void { - sessionStorage.removeItem(this.PARTICIPANT_TOKEN_KEY); - } - - // Saves the recording token to sessionStorage - setRecordingToken(token: string): void { - sessionStorage.setItem(this.RECORDING_TOKEN_KEY, token); - } - - // Retrieves the recording token from sessionStorage - getRecordingToken(): string | null { - return sessionStorage.getItem(this.RECORDING_TOKEN_KEY); - } - - // Removes the recording token from sessionStorage - clearRecordingToken(): void { - sessionStorage.removeItem(this.RECORDING_TOKEN_KEY); + // Removes the room member token from sessionStorage + clearRoomMemberToken(): void { + sessionStorage.removeItem(this.ROOM_MEMBER_TOKEN_KEY); } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/webcomponent-manager.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/webcomponent-manager.service.ts index 53eb590a..abfd95bf 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/webcomponent-manager.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/webcomponent-manager.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { MeetingService, ParticipantService, RoomService } from '../services'; import { WebComponentCommand, WebComponentEvent, @@ -7,6 +6,7 @@ import { WebComponentOutboundEventMessage } from '@openvidu-meet/typings'; import { LoggerService, OpenViduService } from 'openvidu-components-angular'; +import { MeetingService, RoomMemberService, RoomService } from '../services'; /** * Service to manage the commands from OpenVidu Meet WebComponent/Iframe. @@ -26,7 +26,7 @@ export class WebComponentManagerService { constructor( protected loggerService: LoggerService, - protected participantService: ParticipantService, + protected roomMemberService: RoomMemberService, protected openviduService: OpenViduService, protected roomService: RoomService, protected meetingService: MeetingService @@ -115,7 +115,7 @@ export class WebComponentManagerService { switch (command) { case WebComponentCommand.END_MEETING: // Only moderators can end the meeting - if (!this.participantService.isModeratorParticipant()) { + if (!this.roomMemberService.isModerator()) { this.log.w('End meeting command received but participant is not a moderator'); return; } @@ -134,7 +134,7 @@ export class WebComponentManagerService { break; case WebComponentCommand.KICK_PARTICIPANT: // Only moderators can kick participants - if (!this.participantService.isModeratorParticipant()) { + if (!this.roomMemberService.isModerator()) { this.log.w('Kick participant command received but participant is not a moderator'); return; } diff --git a/meet-ce/frontend/src/app/customization/meeting-ce.providers.ts b/meet-ce/frontend/src/app/customization/meeting-ce.providers.ts index 5963359f..aca2fc31 100644 --- a/meet-ce/frontend/src/app/customization/meeting-ce.providers.ts +++ b/meet-ce/frontend/src/app/customization/meeting-ce.providers.ts @@ -1,11 +1,11 @@ import { Provider } from '@angular/core'; import { MEETING_COMPONENTS_TOKEN, - MeetingToolbarButtonsComponent, + MeetingLobbyComponent, MeetingParticipantPanelComponent, - MeetingShareLinkPanelComponent, MeetingShareLinkOverlayComponent, - MeetingLobbyComponent + MeetingShareLinkPanelComponent, + MeetingToolbarButtonsComponent } from '@openvidu-meet/shared-components'; /** @@ -39,7 +39,7 @@ export const MEETING_CE_PROVIDERS: Provider[] = [ }, lobby: MeetingLobbyComponent } - }, + } // { // provide: MEETING_ACTION_HANDLER, // useValue: { diff --git a/meet-ce/typings/src/auth-config.ts b/meet-ce/typings/src/auth-config.ts index 74e86deb..7825d810 100644 --- a/meet-ce/typings/src/auth-config.ts +++ b/meet-ce/typings/src/auth-config.ts @@ -1,24 +1,15 @@ export interface AuthenticationConfig { authMethod: ValidAuthMethod; - authTransportMode: AuthTransportMode; authModeToAccessRoom: AuthMode; } -/** - * Authentication transport modes for JWT tokens. - */ -export enum AuthTransportMode { - COOKIE = 'cookie', // JWT sent via cookies - HEADER = 'header' // JWT sent via Authorization header -} - /** * Authentication modes available to enter a room. */ export enum AuthMode { - NONE = 'none', // No authentication required - MODERATORS_ONLY = 'moderators_only', // Only moderators need authentication - ALL_USERS = 'all_users', // All users need authentication + NONE = 'none', // No authentication required + MODERATORS_ONLY = 'moderators_only', // Only moderators need authentication + ALL_USERS = 'all_users' // All users need authentication } /** @@ -32,13 +23,13 @@ export interface AuthMethod { * Enum for authentication types. */ export enum AuthType { - SINGLE_USER = 'single_user', - // MULTI_USER = 'multi_user', - // OAUTH_ONLY = 'oauth_only' + SINGLE_USER = 'single_user' + // MULTI_USER = 'multi_user', + // OAUTH_ONLY = 'oauth_only' } /** - * Authentication method: Single user with fixed credentials. + * Authentication method: Single user. */ export interface SingleUserAuth extends AuthMethod { type: AuthType.SINGLE_USER; diff --git a/meet-ce/typings/src/event.model.ts b/meet-ce/typings/src/event.model.ts index e3c42fe5..851d8762 100644 --- a/meet-ce/typings/src/event.model.ts +++ b/meet-ce/typings/src/event.model.ts @@ -1,4 +1,4 @@ -import { ParticipantRole } from './participant.js'; +import { MeetRoomMemberRole } from './room-member.js'; import { MeetRoomConfig } from './room-config.js'; export enum MeetSignalType { @@ -15,7 +15,7 @@ export interface MeetRoomConfigUpdatedPayload { export interface MeetParticipantRoleUpdatedPayload { roomId: string; participantIdentity: string; - newRole: ParticipantRole; + newRole: MeetRoomMemberRole; secret?: string; timestamp: number; } diff --git a/meet-ce/typings/src/index.ts b/meet-ce/typings/src/index.ts index e63f6019..69748dbf 100644 --- a/meet-ce/typings/src/index.ts +++ b/meet-ce/typings/src/index.ts @@ -4,8 +4,8 @@ export * from './global-config.js'; export * from './event.model.js'; export * from './permissions/livekit-permissions.js'; -export * from './permissions/openvidu-permissions.js'; -export * from './participant.js'; +export * from './permissions/meet-permissions.js'; +export * from './room-member.js'; export * from './user.js'; export * from './room-config.js'; diff --git a/meet-ce/typings/src/participant.ts b/meet-ce/typings/src/participant.ts deleted file mode 100644 index 9bb51d53..00000000 --- a/meet-ce/typings/src/participant.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { LiveKitPermissions } from './permissions/livekit-permissions.js'; -import { OpenViduMeetPermissions } from './permissions/openvidu-permissions.js'; - -/** - * Options for a participant to join a room. - */ -export interface ParticipantOptions { - /** - * The unique identifier for the room. - */ - roomId: string; - /** - * A secret key for room access. - */ - secret: string; - /** - * The name of the participant. - */ - participantName?: string; - /** - * The identity of the participant. - */ - participantIdentity?: string; -} - -/** - * Represents the permissions for an individual participant. - */ -export interface ParticipantPermissions { - livekit: LiveKitPermissions; - openvidu: OpenViduMeetPermissions; -} - -/** - * Represents the role of a participant in a room. - */ -export enum ParticipantRole { - MODERATOR = 'moderator', - SPEAKER = 'speaker' -} - -export interface MeetTokenMetadata { - livekitUrl: string; - roles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[]; // Array of roles with their permissions - selectedRole: ParticipantRole; -} diff --git a/meet-ce/typings/src/permissions/meet-permissions.ts b/meet-ce/typings/src/permissions/meet-permissions.ts new file mode 100644 index 00000000..27b9b871 --- /dev/null +++ b/meet-ce/typings/src/permissions/meet-permissions.ts @@ -0,0 +1,10 @@ +/** + * Defines Meet-specific permissions for a room member. + */ +export interface MeetPermissions { + canRecord: boolean; // Can start/stop recording the meeting. + canRetrieveRecordings: boolean; // Can list and play recordings. + canDeleteRecordings: boolean; // Can delete recordings. + canChat: boolean; // Can send chat messages in the meeting. + canChangeVirtualBackground: boolean; // Can change the virtual background. +} diff --git a/meet-ce/typings/src/permissions/openvidu-permissions.ts b/meet-ce/typings/src/permissions/openvidu-permissions.ts deleted file mode 100644 index 3f10874f..00000000 --- a/meet-ce/typings/src/permissions/openvidu-permissions.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Defines OpenVidu-specific permissions for a participant. - */ -export interface OpenViduMeetPermissions { - canRecord: boolean; // Can start/stop recording the room. - canChat: boolean; // Can send chat messages in the room. - canChangeVirtualBackground: boolean; // Can change the virtual background. -} - -export interface RecordingPermissions { - canRetrieveRecordings: boolean; // Can list and play recordings. - canDeleteRecordings: boolean; // Can delete recordings. -} diff --git a/meet-ce/typings/src/room-member.ts b/meet-ce/typings/src/room-member.ts new file mode 100644 index 00000000..58e4e3d6 --- /dev/null +++ b/meet-ce/typings/src/room-member.ts @@ -0,0 +1,54 @@ +import { LiveKitPermissions } from './permissions/livekit-permissions.js'; +import { MeetPermissions } from './permissions/meet-permissions.js'; + +/** + * Options for generating a room member token. + * A room member token provides access to room resources (recordings, meetings, etc.) + */ +export interface MeetRoomMemberTokenOptions { + /** + * A secret key for room access. Determines the member's role. + */ + secret: string; + /** + * Whether to include meeting join permissions in the token. + * If true, participantName must be provided. + */ + grantJoinMeetingPermission?: boolean; + /** + * The name of the participant when joining the meeting. + * Required if grantJoinMeetingPermission is true and this is a new token (not a refresh). + */ + participantName?: string; + /** + * The identity of the participant in the meeting. + * Required when refreshing an existing token with meeting permissions. + */ + participantIdentity?: string; +} + +/** + * Represents the permissions for an individual participant. + */ +export interface MeetRoomMemberPermissions { + livekit: LiveKitPermissions; + meet: MeetPermissions; +} + +/** + * Represents the role of a participant in a room. + */ +export enum MeetRoomMemberRole { + MODERATOR = 'moderator', + SPEAKER = 'speaker' +} + +/** + * Metadata stored in room member tokens. + * Contains information about roles and permissions for accessing room resources. + */ +export interface MeetRoomMemberTokenMetadata { + livekitUrl: string; + role: MeetRoomMemberRole; + permissions: MeetPermissions; +} diff --git a/meet-ce/typings/src/room.ts b/meet-ce/typings/src/room.ts index b344f040..0467f514 100644 --- a/meet-ce/typings/src/room.ts +++ b/meet-ce/typings/src/room.ts @@ -1,7 +1,10 @@ -import { ParticipantPermissions, ParticipantRole } from './participant.js'; import { MeetRoomConfig } from './room-config.js'; +import { MeetRoomMemberPermissions, MeetRoomMemberRole } from './room-member.js'; -export interface BaseRoomOptions { +/** + * Options for creating a room. + */ +export interface MeetRoomOptions { roomName?: string; autoDeletionDate?: number; autoDeletionPolicy?: MeetRoomAutoDeletionPolicy; @@ -10,14 +13,9 @@ export interface BaseRoomOptions { } /** - * Options for creating or configuring a room. + * Representation of a room */ -export type MeetRoomOptions = BaseRoomOptions; - -/** - * Interface representing the response received when a room is created. - */ -export interface MeetRoom extends BaseRoomOptions { +export interface MeetRoom extends MeetRoomOptions { roomId: string; roomName: string; creationDate: number; @@ -57,9 +55,9 @@ export enum MeetRoomDeletionPolicyWithRecordings { FAIL = 'fail' // Fail the deletion if there are ongoing or previous recordings } -export interface MeetRoomRoleAndPermissions { - role: ParticipantRole; - permissions: ParticipantPermissions; +export interface MeetRoomMemberRoleAndPermissions { + role: MeetRoomMemberRole; + permissions: MeetRoomMemberPermissions; } export type MeetRoomFilters = { diff --git a/meet-ce/typings/src/user.ts b/meet-ce/typings/src/user.ts index b3e742be..43b1f750 100644 --- a/meet-ce/typings/src/user.ts +++ b/meet-ce/typings/src/user.ts @@ -1,10 +1,10 @@ -export interface User { +export interface MeetUser { username: string; passwordHash: string; - roles: UserRole[]; + roles: MeetUserRole[]; } -export enum UserRole { +export enum MeetUserRole { // Represents a user with administrative privileges ADMIN = 'admin', // Represents a regular user with standard access @@ -13,4 +13,4 @@ export enum UserRole { APP = 'app', } -export type UserDTO = Omit; +export type MeetUserDTO = Omit; diff --git a/testapp/src/controllers/roomController.ts b/testapp/src/controllers/roomController.ts index 1d97e2ce..4afec105 100644 --- a/testapp/src/controllers/roomController.ts +++ b/testapp/src/controllers/roomController.ts @@ -1,10 +1,10 @@ +import { MeetRoomMemberRole, MeetWebhookEvent } from '@openvidu-meet/typings'; import { Request, Response } from 'express'; import { Server as IOServer } from 'socket.io'; -import { ParticipantRole, MeetWebhookEvent } from '@openvidu-meet/typings'; import { configService } from '../services/configService'; interface JoinRoomRequest { - participantRole: ParticipantRole; + participantRole: MeetRoomMemberRole; roomUrl: string; roomId: string; participantName?: string;