From 0abbaddaf9043b7d8471a0193bcf9dbad6e42c5d Mon Sep 17 00:00:00 2001 From: juancarmore Date: Thu, 18 Sep 2025 17:52:01 +0200 Subject: [PATCH] backend: use env var MEET_BASE_URL to set base URL in moderatorUrl and speakerUrl of room objects dynamically instead of storing it --- .../src/controllers/recording.controller.ts | 7 +- backend/src/controllers/room.controller.ts | 6 +- backend/src/environment.ts | 8 ++ backend/src/services/room.service.ts | 11 +-- .../src/services/storage/storage.service.ts | 85 +++++++++++++++++-- 5 files changed, 99 insertions(+), 18 deletions(-) diff --git a/backend/src/controllers/recording.controller.ts b/backend/src/controllers/recording.controller.ts index daae042..fb0d3f3 100644 --- a/backend/src/controllers/recording.controller.ts +++ b/backend/src/controllers/recording.controller.ts @@ -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) { diff --git a/backend/src/controllers/room.controller.ts b/backend/src/controllers/room.controller.ts index 93e7d05..3428c21 100644 --- a/backend/src/controllers/room.controller.ts +++ b/backend/src/controllers/room.controller.ts @@ -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'); diff --git a/backend/src/environment.ts b/backend/src/environment.ts index 9d9213a..e3db85d 100644 --- a/backend/src/environment.ts +++ b/backend/src/environment.ts @@ -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; diff --git a/backend/src/services/room.service.ts b/backend/src/services/room.service.ts index dfb2835..1703c0b 100644 --- a/backend/src/services/room.service.ts +++ b/backend/src/services/room.service.ts @@ -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} A promise that resolves to the created OpenVidu room. * * @throws {Error} If the room creation fails. * */ - async createMeetRoom(baseUrl: string, roomOptions: MeetRoomOptions): Promise { + async createMeetRoom(roomOptions: MeetRoomOptions): Promise { 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); } /** diff --git a/backend/src/services/storage/storage.service.ts b/backend/src/services/storage/storage.service.ts index d8bce00..e688f98 100644 --- a/backend/src/services/storage/storage.service.ts +++ b/backend/src/services/storage/storage.service.ts @@ -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(redisKey, storageKey, meetRoom); + // Normalize room data for storage (ensure only paths are stored) + const normalizedRoom = this.normalizeRoomForStorage(meetRoom); + await this.saveCacheAndStorage(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(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(redisKey, storageKey); + const room = await this.getFromCacheAndStorage(redisKey, storageKey); + + if (!room) { + return null; + } + + // Add base URL to moderator and speaker URLs + return this.enrichRoomWithBaseUrls(room); } async deleteMeetRooms(roomIds: string[]): Promise { @@ -253,7 +272,14 @@ export class MeetStorageService< const redisKey = RedisKeyName.ARCHIVED_ROOM + roomId; const storageKey = this.keyBuilder.buildArchivedMeetRoomKey(roomId); - return await this.getFromCacheAndStorage>(redisKey, storageKey); + const archivedRoom = await this.getFromCacheAndStorage>(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; - await this.saveCacheAndStorage>(redisKey, storageKey, archivedRoom); + // Normalize room data for storage (ensure only paths are stored) + const normalizedRoom = this.normalizeRoomForStorage(archivedRoom as MRoom); + await this.saveCacheAndStorage>(redisKey, storageKey, normalizedRoom); } async deleteArchivedRoomMetadata(roomId: string): Promise { @@ -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 { const { contentLength: fileSize } = await this.storageProvider.getObjectHeaders(key);