diff --git a/frontend/src/app/(editor)/[id]/page.tsx b/frontend/src/app/(editor)/[id]/page.tsx index cbc6ca4e0..29422d35e 100644 --- a/frontend/src/app/(editor)/[id]/page.tsx +++ b/frontend/src/app/(editor)/[id]/page.tsx @@ -5,8 +5,8 @@ */ import { getNote } from '../../../api/notes' import { redirect } from 'next/navigation' +import { baseUrlFromEnvExtractor } from '../../../utils/base-url-from-env-extractor' import { notFound } from 'next/navigation' -import { extractBaseUrls } from '../../../utils/base-url-from-env-extractor' interface PageProps { params: { id: string | undefined } @@ -16,7 +16,7 @@ interface PageProps { * Redirects the user to the editor if the link is a root level direct link to a version 1 note. */ const DirectLinkFallback = async ({ params }: PageProps) => { - const baseUrl = extractBaseUrls().editor + const baseUrl = baseUrlFromEnvExtractor.extractBaseUrls().editor if (params.id === undefined) { notFound() diff --git a/frontend/src/app/(editor)/layout.tsx b/frontend/src/app/(editor)/layout.tsx index 56a660297..b303fb35e 100644 --- a/frontend/src/app/(editor)/layout.tsx +++ b/frontend/src/app/(editor)/layout.tsx @@ -11,7 +11,7 @@ import { MotdModal } from '../../components/global-dialogs/motd-modal/motd-modal import { DarkMode } from '../../components/layout/dark-mode/dark-mode' import { UiNotificationBoundary } from '../../components/notifications/ui-notification-boundary' import { StoreProvider } from '../../redux/store-provider' -import { extractBaseUrls } from '../../utils/base-url-from-env-extractor' +import { baseUrlFromEnvExtractor } from '../../utils/base-url-from-env-extractor' import { configureLuxon } from '../../utils/configure-luxon' import type { Metadata } from 'next' import React from 'react' @@ -20,7 +20,7 @@ import { getConfig } from '../../api/config' configureLuxon() export default async function RootLayout({ children }: { children: React.ReactNode }) { - const baseUrls = extractBaseUrls() + const baseUrls = baseUrlFromEnvExtractor.extractBaseUrls() const frontendConfig = await getConfig(baseUrls.editor) return ( diff --git a/frontend/src/app/(render)/layout.tsx b/frontend/src/app/(render)/layout.tsx index 583930e5d..05cb4e76b 100644 --- a/frontend/src/app/(render)/layout.tsx +++ b/frontend/src/app/(render)/layout.tsx @@ -8,12 +8,12 @@ import { ApplicationLoader } from '../../components/application-loader/applicati import { BaseUrlContextProvider } from '../../components/common/base-url/base-url-context-provider' import { FrontendConfigContextProvider } from '../../components/common/frontend-config-context/frontend-config-context-provider' import { StoreProvider } from '../../redux/store-provider' -import { extractBaseUrls } from '../../utils/base-url-from-env-extractor' +import { baseUrlFromEnvExtractor } from '../../utils/base-url-from-env-extractor' import React from 'react' import { getConfig } from '../../api/config' export default async function RootLayout({ children }: { children: React.ReactNode }) { - const baseUrls = extractBaseUrls() + const baseUrls = baseUrlFromEnvExtractor.extractBaseUrls() const frontendConfig = await getConfig(baseUrls.renderer) return ( diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index f44e50386..14b6a847b 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -6,7 +6,7 @@ import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' -import { extractBaseUrls } from './utils/base-url-from-env-extractor' +import { baseUrlFromEnvExtractor } from './utils/base-url-from-env-extractor' /** * Next.js middleware that checks if the expected and the current origin align. @@ -51,5 +51,9 @@ const determineOriginFromHeaders = (headers: Headers): string | undefined => { * @param request The current request */ const determineExpectedOrigin = (request: NextRequest): string => { - return new URL(request.nextUrl.pathname === '/render' ? extractBaseUrls().renderer : extractBaseUrls().editor).origin + return new URL( + request.nextUrl.pathname === '/render' + ? baseUrlFromEnvExtractor.extractBaseUrls().renderer + : baseUrlFromEnvExtractor.extractBaseUrls().editor + ).origin } diff --git a/frontend/src/utils/base-url-from-env-extractor.spec.ts b/frontend/src/utils/base-url-from-env-extractor.spec.ts index d2f47361b..cbf66b3af 100644 --- a/frontend/src/utils/base-url-from-env-extractor.spec.ts +++ b/frontend/src/utils/base-url-from-env-extractor.spec.ts @@ -3,22 +3,15 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ - -import type { BaseUrls } from '../components/common/base-url/base-url-context-provider' +import { BaseUrlFromEnvExtractor } from './base-url-from-env-extractor' describe('BaseUrlFromEnvExtractor', () => { - let extractBaseUrls: () => BaseUrls - - beforeEach(async () => { - jest.resetModules() - extractBaseUrls = (await import('./base-url-from-env-extractor')).extractBaseUrls - }) - it('should return the base urls if both are valid urls', () => { process.env.HD_BASE_URL = 'https://editor.example.org/' process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/' + const sut = new BaseUrlFromEnvExtractor() - expect(extractBaseUrls()).toStrictEqual({ + expect(sut.extractBaseUrls()).toStrictEqual({ renderer: 'https://renderer.example.org/', editor: 'https://editor.example.org/' }) @@ -27,29 +20,33 @@ describe('BaseUrlFromEnvExtractor', () => { it('should return an empty optional if no var is set', () => { process.env.HD_BASE_URL = undefined process.env.HD_RENDERER_BASE_URL = undefined + const sut = new BaseUrlFromEnvExtractor() - expect(() => extractBaseUrls()).toThrow() + expect(() => sut.extractBaseUrls()).toThrow() }) it("should return an empty optional if editor base url isn't an URL", () => { process.env.HD_BASE_URL = 'bibedibabedibu' process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/' + const sut = new BaseUrlFromEnvExtractor() - expect(() => extractBaseUrls()).toThrow() + expect(() => sut.extractBaseUrls()).toThrow() }) it("should return an empty optional if renderer base url isn't an URL", () => { process.env.HD_BASE_URL = 'https://editor.example.org/' process.env.HD_RENDERER_BASE_URL = 'bibedibabedibu' + const sut = new BaseUrlFromEnvExtractor() - expect(() => extractBaseUrls()).toThrow() + expect(() => sut.extractBaseUrls()).toThrow() }) it("should return an optional if editor base url isn't ending with a slash", () => { process.env.HD_BASE_URL = 'https://editor.example.org' process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/' + const sut = new BaseUrlFromEnvExtractor() - expect(extractBaseUrls()).toStrictEqual({ + expect(sut.extractBaseUrls()).toStrictEqual({ renderer: 'https://renderer.example.org/', editor: 'https://editor.example.org/' }) @@ -58,8 +55,9 @@ describe('BaseUrlFromEnvExtractor', () => { it("should return an optional if renderer base url isn't ending with a slash", () => { process.env.HD_BASE_URL = 'https://editor.example.org/' process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org' + const sut = new BaseUrlFromEnvExtractor() - expect(extractBaseUrls()).toStrictEqual({ + expect(sut.extractBaseUrls()).toStrictEqual({ renderer: 'https://renderer.example.org/', editor: 'https://editor.example.org/' }) @@ -68,8 +66,9 @@ describe('BaseUrlFromEnvExtractor', () => { it('should copy editor base url to renderer base url if renderer base url is omitted', () => { process.env.HD_BASE_URL = 'https://editor.example.org/' delete process.env.HD_RENDERER_BASE_URL + const sut = new BaseUrlFromEnvExtractor() - expect(extractBaseUrls()).toStrictEqual({ + expect(sut.extractBaseUrls()).toStrictEqual({ renderer: 'https://editor.example.org/', editor: 'https://editor.example.org/' }) diff --git a/frontend/src/utils/base-url-from-env-extractor.ts b/frontend/src/utils/base-url-from-env-extractor.ts index 04b11d418..72a74b7f4 100644 --- a/frontend/src/utils/base-url-from-env-extractor.ts +++ b/frontend/src/utils/base-url-from-env-extractor.ts @@ -9,81 +9,88 @@ import { isTestMode, isBuildTime } from './test-modes' import { NoSubdirectoryAllowedError, parseUrl } from '@hedgedoc/commons' import { Optional } from '@mrdrogdrog/optional' -let baseUrls: BaseUrls | undefined = undefined -const logger = new Logger('Base URL Configuration') - -function extractUrlFromEnvVar(envVarName: string, envVarValue: string | undefined): Optional { - try { - return parseUrl(envVarValue) - } catch (error) { - if (error instanceof NoSubdirectoryAllowedError) { - logger.error(error.message) - return Optional.empty() - } else { - throw error - } - } -} - -function extractEditorBaseUrlFromEnv(): Optional { - const envValue = extractUrlFromEnvVar('HD_BASE_URL', process.env.HD_BASE_URL) - if (envValue.isEmpty()) { - logger.error("HD_BASE_URL isn't a valid URL!") - } - return envValue -} - -function extractRendererBaseUrlFromEnv(editorBaseUrl: URL): Optional { - if (isTestMode) { - logger.info('Test mode activated. Using editor base url for renderer.') - return Optional.of(editorBaseUrl) - } - - if (!process.env.HD_RENDERER_BASE_URL) { - logger.info('HD_RENDERER_BASE_URL is unset. Using editor base url for renderer.') - return Optional.of(editorBaseUrl) - } - - return extractUrlFromEnvVar('HD_RENDERER_BASE_URL', process.env.HD_RENDERER_BASE_URL) -} - -function renewBaseUrls(): BaseUrls { - return extractEditorBaseUrlFromEnv() - .flatMap((editorBaseUrl) => - extractRendererBaseUrlFromEnv(editorBaseUrl).map((rendererBaseUrl) => { - return { - editor: editorBaseUrl.toString(), - renderer: rendererBaseUrl.toString() - } - }) - ) - .orElseThrow(() => new Error('couldnt parse env vars')) -} - -function logBaseUrls() { - if (baseUrls === undefined) { - return - } - logger.info('Editor base URL', baseUrls.editor.toString()) - logger.info('Renderer base URL', baseUrls.renderer.toString()) -} - /** - * Extracts the editor and renderer base urls from the environment variables. - * - * @return An {@link Optional} with the base urls. + * Extracts and caches the editor and renderer base urls from the environment variables. */ -export function extractBaseUrls(): BaseUrls { - if (isBuildTime) { - return { - editor: 'https://example.org/', - renderer: 'https://example.org/' +export class BaseUrlFromEnvExtractor { + private baseUrls: BaseUrls | undefined + private readonly logger = new Logger('Base URL Configuration') + + private extractUrlFromEnvVar(envVarName: string, envVarValue: string | undefined): Optional { + try { + return parseUrl(envVarValue) + } catch (error) { + if (error instanceof NoSubdirectoryAllowedError) { + this.logger.error(error.message) + return Optional.empty() + } else { + throw error + } } } - if (baseUrls === undefined) { - baseUrls = renewBaseUrls() - logBaseUrls() + private extractEditorBaseUrlFromEnv(): Optional { + const envValue = this.extractUrlFromEnvVar('HD_BASE_URL', process.env.HD_BASE_URL) + if (envValue.isEmpty()) { + this.logger.error("HD_BASE_URL isn't a valid URL!") + } + return envValue + } + + private extractRendererBaseUrlFromEnv(editorBaseUrl: URL): Optional { + if (isTestMode) { + this.logger.info('Test mode activated. Using editor base url for renderer.') + return Optional.of(editorBaseUrl) + } + + if (!process.env.HD_RENDERER_BASE_URL) { + this.logger.info('HD_RENDERER_BASE_URL is unset. Using editor base url for renderer.') + return Optional.of(editorBaseUrl) + } + + return this.extractUrlFromEnvVar('HD_RENDERER_BASE_URL', process.env.HD_RENDERER_BASE_URL) + } + + private renewBaseUrls(): BaseUrls { + return this.extractEditorBaseUrlFromEnv() + .flatMap((editorBaseUrl) => + this.extractRendererBaseUrlFromEnv(editorBaseUrl).map((rendererBaseUrl) => { + return { + editor: editorBaseUrl.toString(), + renderer: rendererBaseUrl.toString() + } + }) + ) + .orElseThrow(() => new Error('couldnt parse env vars')) + } + + /** + * Extracts the editor and renderer base urls from the environment variables. + * + * @return An {@link Optional} with the base urls. + */ + public extractBaseUrls(): BaseUrls { + if (isBuildTime) { + return { + editor: 'https://example.org/', + renderer: 'https://example.org/' + } + } + + if (this.baseUrls === undefined) { + this.baseUrls = this.renewBaseUrls() + this.logBaseUrls() + } + return this.baseUrls + } + + private logBaseUrls() { + if (this.baseUrls === undefined) { + return + } + this.logger.info('Editor base URL', this.baseUrls.editor.toString()) + this.logger.info('Renderer base URL', this.baseUrls.renderer.toString()) } - return baseUrls } + +export const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()