diff --git a/meet-ce/backend/src/repositories/room.repository.ts b/meet-ce/backend/src/repositories/room.repository.ts index 7d828bb0..b8ee452c 100644 --- a/meet-ce/backend/src/repositories/room.repository.ts +++ b/meet-ce/backend/src/repositories/room.repository.ts @@ -2,7 +2,7 @@ import { MeetRoom, MeetRoomFilters, MeetRoomStatus } from '@openvidu-meet/typing import { inject, injectable } from 'inversify'; import { MeetRoomDocument, MeetRoomModel } from '../models/mongoose-schemas/room.schema.js'; import { LoggerService } from '../services/logger.service.js'; -import { getBasePath } from '../utils/html-injection.utils.js'; +import { getBasePath } from '../utils/html-dynamic-base-path.utils.js'; import { getBaseUrl } from '../utils/url.utils.js'; import { BaseRepository } from './base.repository.js'; diff --git a/meet-ce/backend/src/server.ts b/meet-ce/backend/src/server.ts index a3a62f31..5e6f2da5 100644 --- a/meet-ce/backend/src/server.ts +++ b/meet-ce/backend/src/server.ts @@ -17,7 +17,7 @@ import { internalMeetingRouter } from './routes/meeting.routes.js'; import { recordingRouter } from './routes/recording.routes.js'; import { internalRoomRouter, roomRouter } from './routes/room.routes.js'; import { userRouter } from './routes/user.routes.js'; -import { getBasePath, getInjectedHtml } from './utils/html-injection.utils.js'; +import { getBasePath, getHtmlWithBasePath, getOpenApiHtmlWithBasePath } from './utils/html-dynamic-base-path.utils.js'; import { frontendDirectoryPath, frontendHtmlPath, @@ -76,7 +76,7 @@ const createApp = () => { // Public API routes appRouter.use(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/docs`, (_req: Request, res: Response) => - res.sendFile(publicApiHtmlFilePath) + res.type('html').send(getOpenApiHtmlWithBasePath(publicApiHtmlFilePath, INTERNAL_CONFIG.API_BASE_PATH_V1)) ); appRouter.use(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms`, /*mediaTypeValidatorMiddleware,*/ roomRouter); appRouter.use(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings`, /*mediaTypeValidatorMiddleware,*/ recordingRouter); @@ -85,7 +85,7 @@ const createApp = () => { if (process.env.NODE_ENV === 'development') { // Serve internal API docs only in development mode appRouter.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/docs`, (_req: Request, res: Response) => - res.sendFile(internalApiHtmlFilePath) + res.type('html').send(getOpenApiHtmlWithBasePath(internalApiHtmlFilePath, INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1)) ); } @@ -104,7 +104,7 @@ const createApp = () => { appRouter.get('/v1/openvidu-meet.js', (_req: Request, res: Response) => res.sendFile(webcomponentBundlePath)); // Serve OpenVidu Meet index.html file for all non-API routes (with dynamic base path injection) appRouter.get(/^(?!.*\/(api|internal-api)\/).*$/, (_req: Request, res: Response) => { - res.type('html').send(getInjectedHtml(frontendHtmlPath)); + res.type('html').send(getHtmlWithBasePath(frontendHtmlPath)); }); // Catch all other routes and return 404 appRouter.use((_req: Request, res: Response) => diff --git a/meet-ce/backend/src/utils/html-injection.utils.ts b/meet-ce/backend/src/utils/html-dynamic-base-path.utils.ts similarity index 67% rename from meet-ce/backend/src/utils/html-injection.utils.ts rename to meet-ce/backend/src/utils/html-dynamic-base-path.utils.ts index 200c1489..1bbbe722 100644 --- a/meet-ce/backend/src/utils/html-injection.utils.ts +++ b/meet-ce/backend/src/utils/html-dynamic-base-path.utils.ts @@ -3,6 +3,7 @@ import fs from 'fs'; import { MEET_ENV } from '../environment.js'; let cachedHtml: string | null = null; +const cachedOpenApiHtml = new Map(); let configValidated = false; /** @@ -73,14 +74,14 @@ export function getBasePath(): string { } /** - * Injects runtime configuration into the index.html + * Applies runtime base path configuration to the index.html * - Replaces the tag with the configured base path - * - Injects a script with window.__OPENVIDU_MEET_CONFIG__ for frontend access + * - Adds a script with window.__OPENVIDU_MEET_CONFIG__ for frontend access * * @param htmlPath Path to the index.html file * @returns The modified HTML content */ -export function getInjectedHtml(htmlPath: string): string { +export function getHtmlWithBasePath(htmlPath: string): string { // In production, cache the result for performance if (process.env.NODE_ENV === 'production' && cachedHtml) { return cachedHtml; @@ -103,9 +104,44 @@ export function getInjectedHtml(htmlPath: string): string { return html; } +/** + * Applies the runtime base path to the OpenAPI documentation HTML. + * Replaces the servers URL in the embedded OpenAPI spec so that "Try It" requests + * use the correct path when deployed under a base path (e.g. /meet/api/v1). + * + * @param htmlPath Path to the OpenAPI HTML file + * @param apiBasePath The API base path (e.g. /api/v1 or /internal-api/v1) + * @returns The modified HTML content + */ +export function getOpenApiHtmlWithBasePath(htmlPath: string, apiBasePath: string): string { + if (process.env.NODE_ENV === 'production' && cachedOpenApiHtml.has(htmlPath)) { + return cachedOpenApiHtml.get(htmlPath)!; + } + + const basePath = getBasePath(); + // Build full server URL: strip trailing slash from basePath to avoid double slashes + const fullServerUrl = basePath.replace(/\/$/, '') + apiBasePath; + + let html = fs.readFileSync(htmlPath, 'utf-8'); + + // Replace the servers URL in the embedded OpenAPI JSON + // Matches "servers":[{"url":"" and replaces the URL with the full path + html = html.replace( + /("servers":\[\{"url":")[^"]*(")/, + `$1${fullServerUrl}$2` + ); + + if (process.env.NODE_ENV === 'production') { + cachedOpenApiHtml.set(htmlPath, html); + } + + return html; +} + /** * Clears the cached HTML (useful for testing or config changes) */ export function clearHtmlCache(): void { cachedHtml = null; + cachedOpenApiHtml.clear(); } diff --git a/meet-ce/backend/src/utils/url.utils.ts b/meet-ce/backend/src/utils/url.utils.ts index 1d0ac9cb..86ac3d25 100644 --- a/meet-ce/backend/src/utils/url.utils.ts +++ b/meet-ce/backend/src/utils/url.utils.ts @@ -1,7 +1,7 @@ import { container } from '../config/dependency-injector.config.js'; import { MEET_ENV } from '../environment.js'; import { BaseUrlService } from '../services/base-url.service.js'; -import { getBasePath } from './html-injection.utils.js'; +import { getBasePath } from './html-dynamic-base-path.utils.js'; /** * Returns the base URL for the application, including the configured base path. diff --git a/meet-ce/backend/tests/helpers/request-helpers.ts b/meet-ce/backend/tests/helpers/request-helpers.ts index 3f6106b8..93b1a003 100644 --- a/meet-ce/backend/tests/helpers/request-helpers.ts +++ b/meet-ce/backend/tests/helpers/request-helpers.ts @@ -31,7 +31,7 @@ import { ApiKeyService } from '../../src/services/api-key.service.js'; import { GlobalConfigService } from '../../src/services/global-config.service.js'; import { RecordingService } from '../../src/services/recording.service.js'; import { RoomScheduledTasksService } from '../../src/services/room-scheduled-tasks.service.js'; -import { getBasePath } from '../../src/utils/html-injection.utils.js'; +import { getBasePath } from '../../src/utils/html-dynamic-base-path.utils.js'; /** * Constructs the full API path by prepending the base path.