backend: use env var MEET_BASE_URL to set base URL in moderatorUrl and speakerUrl of room objects dynamically instead of storing it

This commit is contained in:
juancarmore 2025-09-18 17:52:01 +02:00
parent fa1582bee0
commit 0abbaddaf9
5 changed files with 99 additions and 18 deletions

View File

@ -3,6 +3,7 @@ import { Request, Response } from 'express';
import { Readable } from 'stream';
import { container } from '../config/index.js';
import INTERNAL_CONFIG from '../config/internal-config.js';
import { getBaseUrl } from '../environment.js';
import { RecordingHelper } from '../helpers/index.js';
import {
errorRecordingNotFound,
@ -23,7 +24,7 @@ export const startRecording = async (req: Request, res: Response) => {
const recordingInfo = await recordingService.startRecording(roomId);
res.setHeader(
'Location',
`${req.protocol}://${req.get('host')}${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${recordingInfo.recordingId}`
`${getBaseUrl()}${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${recordingInfo.recordingId}`
);
return res.status(201).json(recordingInfo);
@ -126,7 +127,7 @@ export const stopRecording = async (req: Request, res: Response) => {
const recordingInfo = await recordingService.stopRecording(recordingId);
res.setHeader(
'Location',
`${req.protocol}://${req.get('host')}${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${recordingId}`
`${getBaseUrl()}${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${recordingId}`
);
return res.status(202).json(recordingInfo);
} catch (error) {
@ -250,7 +251,7 @@ export const getRecordingUrl = async (req: Request, res: Response) => {
}
const secret = privateAccess ? recordingSecrets.privateAccessSecret : recordingSecrets.publicAccessSecret;
const recordingUrl = `${req.protocol}://${req.get('host')}/recording/${recordingId}?secret=${secret}`;
const recordingUrl = `${getBaseUrl()}/recording/${recordingId}?secret=${secret}`;
return res.status(200).json({ url: recordingUrl });
} catch (error) {

View File

@ -10,6 +10,7 @@ import {
import { Request, Response } from 'express';
import { container } from '../config/index.js';
import INTERNAL_CONFIG from '../config/internal-config.js';
import { getBaseUrl } from '../environment.js';
import { handleError } from '../models/error.model.js';
import { LoggerService, ParticipantService, RoomService } from '../services/index.js';
import { getCookieOptions } from '../utils/cookie-utils.js';
@ -21,10 +22,9 @@ export const createRoom = async (req: Request, res: Response) => {
try {
logger.verbose(`Creating room with options '${JSON.stringify(options)}'`);
const baseUrl = `${req.protocol}://${req.get('host')}`;
const room = await roomService.createMeetRoom(baseUrl, options);
res.set('Location', `${baseUrl}${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${room.roomId}`);
const room = await roomService.createMeetRoom(options);
res.set('Location', `${getBaseUrl()}${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${room.roomId}`);
return res.status(201).json(room);
} catch (error) {
handleError(res, error, 'creating room');

View File

@ -20,6 +20,7 @@ export const {
SERVER_CORS_ORIGIN = '*',
MEET_LOG_LEVEL = 'info',
MEET_NAME_ID = 'openviduMeet',
MEET_BASE_URL = `http://localhost:${SERVER_PORT}`,
/**
* Authentication configuration
@ -85,6 +86,13 @@ export const {
ENABLED_MODULES = ''
} = process.env;
/**
* Gets the base URL without trailing slash
*/
export const getBaseUrl = (): string => {
return MEET_BASE_URL.endsWith('/') ? MEET_BASE_URL.slice(0, -1) : MEET_BASE_URL;
};
export function checkModuleEnabled() {
if (MODULES_FILE) {
const moduleName = MODULE_NAME;

View File

@ -72,14 +72,13 @@ export class RoomService {
/**
* Creates an OpenVidu Meet room with the specified options.
*
* @param {string} baseUrl - The base URL for the room.
* @param {MeetRoomOptions} options - The options for creating the OpenVidu room.
* @returns {Promise<MeetRoom>} A promise that resolves to the created OpenVidu room.
*
* @throws {Error} If the room creation fails.
*
*/
async createMeetRoom(baseUrl: string, roomOptions: MeetRoomOptions): Promise<MeetRoom> {
async createMeetRoom(roomOptions: MeetRoomOptions): Promise<MeetRoom> {
const { roomName, autoDeletionDate, autoDeletionPolicy, config } = roomOptions;
const roomIdPrefix = roomName!.replace(/\s+/g, ''); // Remove all spaces
const roomId = `${roomIdPrefix}-${uid(15)}`; // Generate a unique room ID based on the room name
@ -92,14 +91,12 @@ export class RoomService {
autoDeletionDate,
autoDeletionPolicy,
config: config!,
moderatorUrl: `${baseUrl}/room/${roomId}?secret=${secureUid(10)}`,
speakerUrl: `${baseUrl}/room/${roomId}?secret=${secureUid(10)}`,
moderatorUrl: `/room/${roomId}?secret=${secureUid(10)}`,
speakerUrl: `/room/${roomId}?secret=${secureUid(10)}`,
status: MeetRoomStatus.OPEN,
meetingEndAction: MeetingEndAction.NONE
};
await this.storageService.saveMeetRoom(meetRoom);
return meetRoom;
return await this.storageService.saveMeetRoom(meetRoom);
}
/**

View File

@ -3,6 +3,7 @@ import { inject, injectable } from 'inversify';
import ms from 'ms';
import { Readable } from 'stream';
import {
getBaseUrl,
MEET_INITIAL_ADMIN_PASSWORD,
MEET_INITIAL_ADMIN_USER,
MEET_INITIAL_API_KEY,
@ -168,7 +169,12 @@ export class MeetStorageService<
const redisKey = RedisKeyName.ROOM + roomId;
const storageKey = this.keyBuilder.buildMeetRoomKey(roomId);
return await this.saveCacheAndStorage<MRoom>(redisKey, storageKey, meetRoom);
// Normalize room data for storage (ensure only paths are stored)
const normalizedRoom = this.normalizeRoomForStorage(meetRoom);
await this.saveCacheAndStorage<MRoom>(redisKey, storageKey, normalizedRoom);
// Return room with full URLs
return this.enrichRoomWithBaseUrls(normalizedRoom);
}
/**
@ -206,7 +212,13 @@ export class MeetStorageService<
if (item.Key && item.Key.endsWith('.json')) {
try {
const room = await this.storageProvider.getObject<MRoom>(item.Key);
return room;
if (!room) {
return null;
}
// Add base URL to moderator and speaker URLs
return this.enrichRoomWithBaseUrls(room);
} catch (error) {
this.logger.warn(`Failed to load room from ${item.Key}: ${error}`);
return null;
@ -235,7 +247,14 @@ export class MeetStorageService<
const redisKey = RedisKeyName.ROOM + roomId;
const storageKey = this.keyBuilder.buildMeetRoomKey(roomId);
return await this.getFromCacheAndStorage<MRoom>(redisKey, storageKey);
const room = await this.getFromCacheAndStorage<MRoom>(redisKey, storageKey);
if (!room) {
return null;
}
// Add base URL to moderator and speaker URLs
return this.enrichRoomWithBaseUrls(room);
}
async deleteMeetRooms(roomIds: string[]): Promise<void> {
@ -253,7 +272,14 @@ export class MeetStorageService<
const redisKey = RedisKeyName.ARCHIVED_ROOM + roomId;
const storageKey = this.keyBuilder.buildArchivedMeetRoomKey(roomId);
return await this.getFromCacheAndStorage<Partial<MRoom>>(redisKey, storageKey);
const archivedRoom = await this.getFromCacheAndStorage<Partial<MRoom>>(redisKey, storageKey);
if (!archivedRoom) {
return null;
}
// Add base URL to moderator and speaker URLs
return this.enrichRoomWithBaseUrls(archivedRoom as MRoom);
}
/**
@ -298,7 +324,9 @@ export class MeetStorageService<
}
} as Partial<MRoom>;
await this.saveCacheAndStorage<Partial<MRoom>>(redisKey, storageKey, archivedRoom);
// Normalize room data for storage (ensure only paths are stored)
const normalizedRoom = this.normalizeRoomForStorage(archivedRoom as MRoom);
await this.saveCacheAndStorage<Partial<MRoom>>(redisKey, storageKey, normalizedRoom);
}
async deleteArchivedRoomMetadata(roomId: string): Promise<void> {
@ -731,6 +759,53 @@ export class MeetStorageService<
this.logger.info('API key initialized');
}
/**
* Normalizes room data for storage by ensuring URLs contain only paths
* @param room - The room object to normalize
* @returns The room object with path-only URLs for storage
*/
private normalizeRoomForStorage(room: MRoom): MRoom {
return {
...room,
moderatorUrl: this.extractPathFromUrl(room.moderatorUrl),
speakerUrl: this.extractPathFromUrl(room.speakerUrl)
};
}
/**
* Extracts path from URL, handling both full URLs and path-only strings
* @param url - The URL or path to process
* @returns The path portion of the URL
*/
private extractPathFromUrl(url: string): string {
// If it's already a path (starts with /), return as-is
if (url.startsWith('/')) {
return url;
}
// If it's a full URL, extract the path
try {
const urlObj = new URL(url);
return urlObj.pathname + urlObj.search + urlObj.hash;
} catch (error) {
this.logger.warn(`Failed to parse URL for path extraction: ${url}. Treating as path.`);
return url;
}
}
/**
* Enriches a room object with base URLs for moderator and speaker URLs
* @param room - The room object to enrich
* @returns The room object with full URLs
*/
protected enrichRoomWithBaseUrls(room: MRoom): MRoom {
return {
...room,
moderatorUrl: `${getBaseUrl()}${room.moderatorUrl}`,
speakerUrl: `${getBaseUrl()}${room.speakerUrl}`
};
}
protected async getRecordingFileSize(key: string, recordingId: string): Promise<number> {
const { contentLength: fileSize } = await this.storageProvider.getObjectHeaders(key);