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

View File

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

View File

@ -20,6 +20,7 @@ export const {
SERVER_CORS_ORIGIN = '*', SERVER_CORS_ORIGIN = '*',
MEET_LOG_LEVEL = 'info', MEET_LOG_LEVEL = 'info',
MEET_NAME_ID = 'openviduMeet', MEET_NAME_ID = 'openviduMeet',
MEET_BASE_URL = `http://localhost:${SERVER_PORT}`,
/** /**
* Authentication configuration * Authentication configuration
@ -85,6 +86,13 @@ export const {
ENABLED_MODULES = '' ENABLED_MODULES = ''
} = process.env; } = 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() { export function checkModuleEnabled() {
if (MODULES_FILE) { if (MODULES_FILE) {
const moduleName = MODULE_NAME; const moduleName = MODULE_NAME;

View File

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

View File

@ -3,6 +3,7 @@ import { inject, injectable } from 'inversify';
import ms from 'ms'; import ms from 'ms';
import { Readable } from 'stream'; import { Readable } from 'stream';
import { import {
getBaseUrl,
MEET_INITIAL_ADMIN_PASSWORD, MEET_INITIAL_ADMIN_PASSWORD,
MEET_INITIAL_ADMIN_USER, MEET_INITIAL_ADMIN_USER,
MEET_INITIAL_API_KEY, MEET_INITIAL_API_KEY,
@ -168,7 +169,12 @@ export class MeetStorageService<
const redisKey = RedisKeyName.ROOM + roomId; const redisKey = RedisKeyName.ROOM + roomId;
const storageKey = this.keyBuilder.buildMeetRoomKey(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')) { if (item.Key && item.Key.endsWith('.json')) {
try { try {
const room = await this.storageProvider.getObject<MRoom>(item.Key); 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) { } catch (error) {
this.logger.warn(`Failed to load room from ${item.Key}: ${error}`); this.logger.warn(`Failed to load room from ${item.Key}: ${error}`);
return null; return null;
@ -235,7 +247,14 @@ export class MeetStorageService<
const redisKey = RedisKeyName.ROOM + roomId; const redisKey = RedisKeyName.ROOM + roomId;
const storageKey = this.keyBuilder.buildMeetRoomKey(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> { async deleteMeetRooms(roomIds: string[]): Promise<void> {
@ -253,7 +272,14 @@ export class MeetStorageService<
const redisKey = RedisKeyName.ARCHIVED_ROOM + roomId; const redisKey = RedisKeyName.ARCHIVED_ROOM + roomId;
const storageKey = this.keyBuilder.buildArchivedMeetRoomKey(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>; } 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> { async deleteArchivedRoomMetadata(roomId: string): Promise<void> {
@ -731,6 +759,53 @@ export class MeetStorageService<
this.logger.info('API key initialized'); 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> { protected async getRecordingFileSize(key: string, recordingId: string): Promise<number> {
const { contentLength: fileSize } = await this.storageProvider.getObjectHeaders(key); const { contentLength: fileSize } = await this.storageProvider.getObjectHeaders(key);