Delete participant and recording tokens and implement room member token. Remove unused cookie transport mode for tokens
This commit is contained in:
parent
a56a119993
commit
6eb33c6198
@ -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'
|
||||
|
||||
@ -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'
|
||||
@ -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'
|
||||
@ -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'
|
||||
@ -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'
|
||||
@ -1,7 +0,0 @@
|
||||
name: participantIdentity
|
||||
in: path
|
||||
required: true
|
||||
description: The identity of the participant.
|
||||
schema:
|
||||
type: string
|
||||
example: 'Alice'
|
||||
@ -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
|
||||
|
||||
@ -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']
|
||||
@ -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:
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
description: Participant details
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../../schemas/internal/meet-participant-options.yaml'
|
||||
@ -0,0 +1,6 @@
|
||||
description: Room member token options
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../../schemas/internal/room-member-token-options.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.
|
||||
@ -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'
|
||||
@ -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"'
|
||||
@ -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'
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -2,4 +2,4 @@ description: Successfully retrieved user profile
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../../schemas/internal/user.yaml'
|
||||
$ref: '../../schemas/internal/meet-user.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'
|
||||
@ -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
|
||||
@ -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'
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
description: Successfully refreshed the access token
|
||||
# headers:
|
||||
# Set-Cookie:
|
||||
# $ref: '../../headers/set-cookie-access-token.yaml'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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'
|
||||
@ -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
|
||||
@ -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'
|
||||
@ -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:
|
||||
|
||||
@ -58,6 +58,6 @@ MeetE2EEConfig:
|
||||
default: false
|
||||
example: false
|
||||
description: >
|
||||
If true, the room will have End-to-End Encryption (E2EE) enabled.<br/>
|
||||
This ensures that the media streams are encrypted from the sender to the receiver, providing enhanced privacy and security for the participants.<br/>
|
||||
**Enabling E2EE will disable the recording feature for the room**.
|
||||
If true, the room will have End-to-End Encryption (E2EE) enabled.<br/>
|
||||
This ensures that the media streams are encrypted from the sender to the receiver, providing enhanced privacy and security for the participants.<br/>
|
||||
**Enabling E2EE will disable the recording feature for the room**.
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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'
|
||||
@ -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':
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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}'`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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}'`);
|
||||
}
|
||||
};
|
||||
@ -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);
|
||||
|
||||
@ -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}'`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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<boolean> {
|
||||
const token = await getAccessToken(req);
|
||||
const token = getAccessToken(req);
|
||||
return !!token;
|
||||
},
|
||||
|
||||
async validate(req: Request): Promise<void> {
|
||||
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<boolean> {
|
||||
const token = await getParticipantToken(req);
|
||||
const token = getRoomMemberToken(req);
|
||||
return !!token;
|
||||
},
|
||||
|
||||
async validate(req: Request): Promise<void> {
|
||||
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<boolean> {
|
||||
const token = await getRecordingToken(req);
|
||||
return !!token;
|
||||
},
|
||||
|
||||
async validate(req: Request): Promise<void> {
|
||||
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<void> {
|
||||
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<User> => {
|
||||
const getAuthenticatedUserOrAnonymous = async (req: Request): Promise<MeetUser> => {
|
||||
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 {
|
||||
|
||||
14
meet-ce/backend/src/middlewares/base-url.middleware.ts
Normal file
14
meet-ce/backend/src/middlewares/base-url.middleware.ts
Normal file
@ -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();
|
||||
};
|
||||
@ -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();
|
||||
};
|
||||
@ -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';
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
};
|
||||
@ -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<AuthTransportMode> = z.nativeEnum(AuthTransportMode);
|
||||
|
||||
const AuthModeSchema: z.ZodType<AuthMode> = z.nativeEnum(AuthMode);
|
||||
|
||||
const AuthTypeSchema: z.ZodType<AuthType> = z.nativeEnum(AuthType);
|
||||
@ -54,7 +51,6 @@ const ValidAuthMethodSchema: z.ZodType<ValidAuthMethod> = SingleUserAuthSchema;
|
||||
|
||||
const AuthenticationConfigSchema: z.ZodType<AuthenticationConfig> = z.object({
|
||||
authMethod: ValidAuthMethodSchema,
|
||||
authTransportMode: AuthTransportModeSchema,
|
||||
authModeToAccessRoom: AuthModeSchema
|
||||
});
|
||||
|
||||
|
||||
@ -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<ParticipantOptions> = 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<OpenViduMeetPermissions> = z.object({
|
||||
const MeetPermissionsSchema: z.ZodType<MeetPermissions> = z.object({
|
||||
canRecord: z.boolean(),
|
||||
canRetrieveRecordings: z.boolean(),
|
||||
canDeleteRecordings: z.boolean(),
|
||||
canChat: z.boolean(),
|
||||
canChangeVirtualBackground: z.boolean()
|
||||
});
|
||||
|
||||
const MeetTokenMetadataSchema: z.ZodType<MeetTokenMetadata> = z.object({
|
||||
const RoomMemberTokenMetadataSchema: z.ZodType<MeetRoomMemberTokenMetadata> = 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}`);
|
||||
|
||||
@ -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<RecordingPermissions> = z.object({
|
||||
canRetrieveRecordings: z.boolean(),
|
||||
canDeleteRecordings: z.boolean()
|
||||
});
|
||||
|
||||
const RecordingTokenMetadataSchema = z.object({
|
||||
role: z.nativeEnum(ParticipantRole),
|
||||
recordingPermissions: RecordingPermissionsSchema
|
||||
});
|
||||
const RoomMemberTokenRequestSchema: z.ZodType<MeetRoomMemberTokenOptions> = 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;
|
||||
};
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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<MeetUserDocument>(
|
||||
},
|
||||
roles: {
|
||||
type: [String],
|
||||
enum: Object.values(UserRole),
|
||||
enum: Object.values(MeetUserRole),
|
||||
required: true,
|
||||
default: [UserRole.USER]
|
||||
default: [MeetUserRole.USER]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -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<TUser extends User = User> extends BaseRepository<TUser, MeetUserDocument> {
|
||||
export class UserRepository<TUser extends MeetUser = MeetUser> extends BaseRepository<TUser, MeetUserDocument> {
|
||||
constructor(@inject(LoggerService) logger: LoggerService) {
|
||||
super(logger, MeetUserModel);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
@ -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
|
||||
);
|
||||
@ -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
|
||||
);
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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<void> {
|
||||
this.logger.debug(
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -182,22 +182,11 @@ export class LiveKitService {
|
||||
}
|
||||
}
|
||||
|
||||
async participantExists(
|
||||
roomName: string,
|
||||
participantNameOrIdentity: string,
|
||||
participantField: 'name' | 'identity' = 'identity'
|
||||
): Promise<boolean> {
|
||||
async participantExists(roomName: string, participantIdentity: string): Promise<boolean> {
|
||||
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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<string> {
|
||||
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<string> {
|
||||
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<ParticipantInfo> {
|
||||
this.logger.verbose(`Fetching participant '${participantIdentity}'`);
|
||||
return this.livekitService.getParticipant(roomId, participantIdentity);
|
||||
}
|
||||
|
||||
async participantExists(
|
||||
roomId: string,
|
||||
participantNameOrIdentity: string,
|
||||
participantField: 'name' | 'identity' = 'identity'
|
||||
): Promise<boolean> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string[]> - Array of reserved participant names
|
||||
*/
|
||||
async getReservedNames(roomId: string): Promise<string[]> {
|
||||
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<void> {
|
||||
await this.participantNameService.cleanupExpiredReservations(roomId);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
137
meet-ce/backend/src/services/request-session.service.ts
Normal file
137
meet-ce/backend/src/services/request-session.service.ts
Normal file
@ -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<RequestContext>();
|
||||
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<T>(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;
|
||||
}
|
||||
}
|
||||
359
meet-ce/backend/src/services/room-member.service.ts
Normal file
359
meet-ce/backend/src/services/room-member.service.ts
Normal file
@ -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<MeetRoomMemberRole> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
// 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<string> {
|
||||
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<MeetRoomMemberPermissions> {
|
||||
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<void> {
|
||||
this.logger.verbose(`Kicking participant '${participantIdentity}' from room '${roomId}'`);
|
||||
return this.livekitService.deleteParticipant(roomId, participantIdentity);
|
||||
}
|
||||
|
||||
async updateParticipantRole(
|
||||
roomId: string,
|
||||
participantIdentity: string,
|
||||
newRole: MeetRoomMemberRole
|
||||
): Promise<void> {
|
||||
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<boolean> {
|
||||
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<ParticipantInfo> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
await this.participantNameService.cleanupExpiredReservations(roomId);
|
||||
}
|
||||
}
|
||||
@ -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<MeetRoom> {
|
||||
async getMeetRoom(roomId: string, fields?: string): Promise<MeetRoom> {
|
||||
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<ParticipantRole> {
|
||||
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<string> {
|
||||
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<RecordingPermissions> {
|
||||
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.
|
||||
|
||||
@ -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<User | null> {
|
||||
async getUser(username: string): Promise<MeetUser | null> {
|
||||
const redisKey = RedisKeyName.USER + username;
|
||||
const storageKey = this.keyBuilder.buildUserKey(username);
|
||||
|
||||
const user = await this.getFromCacheAndStorage<User>(redisKey, storageKey);
|
||||
const user = await this.getFromCacheAndStorage<MeetUser>(redisKey, storageKey);
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
@ -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<string> {
|
||||
async generateAccessToken(user: MeetUser): Promise<string> {
|
||||
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<string> {
|
||||
async generateRefreshToken(user: MeetUser): Promise<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
|
||||
@ -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<void> {
|
||||
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<User | null> {
|
||||
async authenticateUser(username: string, password: string): Promise<MeetUser | null> {
|
||||
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<User | null> {
|
||||
async getUser(username: string): Promise<MeetUser | null> {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
};
|
||||
};
|
||||
@ -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';
|
||||
|
||||
@ -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<AuthTransportMode> => {
|
||||
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<AuthTransportMode> => {
|
||||
* @param req - Express request object
|
||||
* @returns The JWT token string or undefined if not found
|
||||
*/
|
||||
export const getAccessToken = async (req: Request): Promise<string | undefined> => {
|
||||
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<string | undefined>
|
||||
* @param req - Express request object
|
||||
* @returns The JWT refresh token string or undefined if not found
|
||||
*/
|
||||
export const getRefreshToken = async (req: Request): Promise<string | undefined> => {
|
||||
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<string | undefined> => {
|
||||
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<string | undefined> => {
|
||||
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<string | undefine
|
||||
*
|
||||
* @param req - Express request object
|
||||
* @param headerName - Name of the header to check
|
||||
* @param cookieName - Name of the cookie to check
|
||||
* @param queryParamName - (Optional) Name of the query parameter to check (for access and recording tokens)
|
||||
* @param queryParamName - (Optional) Name of the query parameter to check (for access and room member tokens)
|
||||
* @returns The JWT token string or undefined if not found
|
||||
*/
|
||||
const getTokenFromRequest = async (
|
||||
req: Request,
|
||||
headerName: string,
|
||||
cookieName: string,
|
||||
queryParamName?: string
|
||||
): Promise<string | undefined> => {
|
||||
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) {
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user