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;