refactor: move base-url-from-env-extractor into commons

This is done to use it in next.config.js

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-09-03 21:06:12 +02:00
parent d675cc9ed9
commit f6a6f6b086
No known key found for this signature in database
GPG key ID: FE1CD209E3EA5E85
15 changed files with 268 additions and 180 deletions

View file

@ -0,0 +1,112 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { BaseUrlFromEnvExtractor } from './base-url-from-env-extractor.js'
import { describe, it, expect } from '@jest/globals'
import { NoSubdirectoryAllowedError, NoValidUrlError } from './errors.js'
import { BaseUrls } from './base-urls.types.js'
describe('BaseUrlFromEnvExtractor', () => {
it('should return the correctly parsed values if all are set', () => {
process.env.HD_BASE_URL = 'https://editor.example.org/'
process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/'
process.env.HD_SSR_API_URL = 'https://internal.example.org/'
const sut = new BaseUrlFromEnvExtractor()
const result = sut.extractBaseUrls()
expect(result).toStrictEqual({
editor: 'https://editor.example.org/',
renderer: 'https://renderer.example.org/',
ssrApi: 'https://internal.example.org/'
} as BaseUrls)
})
it('should throw an error if no base url is set', () => {
process.env.HD_BASE_URL = undefined
const sut = new BaseUrlFromEnvExtractor()
expect(() => sut.extractBaseUrls()).toThrow(NoValidUrlError)
})
it("should throw an error if editor base url isn't an URL", () => {
process.env.HD_BASE_URL = 'bibedibabedibu'
process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/'
process.env.HD_SSR_API_URL = 'https://internal.example.org/'
const sut = new BaseUrlFromEnvExtractor()
expect(() => sut.extractBaseUrls()).toThrow(NoValidUrlError)
})
it('should throw an error if renderer base url is set but no valid URL', () => {
process.env.HD_BASE_URL = 'https://editor.example.org/'
process.env.HD_RENDERER_BASE_URL = 'bibedibabedibu'
process.env.HD_SSR_API_URL = 'https://internal.example.org/'
const sut = new BaseUrlFromEnvExtractor()
expect(() => sut.extractBaseUrls()).toThrow(NoValidUrlError)
})
it('should throw an error if ssr api base url is set but no valid URL', () => {
process.env.HD_BASE_URL = 'https://editor.example.org/'
process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/'
process.env.HD_SSR_API_URL = 'bibedibabedibu'
const sut = new BaseUrlFromEnvExtractor()
expect(() => sut.extractBaseUrls()).toThrow(NoValidUrlError)
})
it('should throw an error if editor base url contains a path', () => {
process.env.HD_BASE_URL = 'https://editor.example.org/subpath/'
process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/'
const sut = new BaseUrlFromEnvExtractor()
expect(() => sut.extractBaseUrls()).toThrow(NoSubdirectoryAllowedError)
})
it('should throw an error if renderer base url contains a path', () => {
process.env.HD_BASE_URL = 'https://editor.example.org/'
process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/subpath/'
const sut = new BaseUrlFromEnvExtractor()
expect(() => sut.extractBaseUrls()).toThrow(NoSubdirectoryAllowedError)
})
it('should throw an error if ssr api url contains a path', () => {
process.env.HD_BASE_URL = 'https://editor.example.org/'
process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/'
process.env.HD_SSR_API_URL = 'https://internal.example.org/subpath/'
const sut = new BaseUrlFromEnvExtractor()
expect(() => sut.extractBaseUrls()).toThrow(NoSubdirectoryAllowedError)
})
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
process.env.HD_SSR_API_URL = 'https://internal.example.org/'
const sut = new BaseUrlFromEnvExtractor()
const result = sut.extractBaseUrls()
expect(result).toStrictEqual({
editor: 'https://editor.example.org/',
renderer: 'https://editor.example.org/',
ssrApi: 'https://internal.example.org/'
} as BaseUrls)
})
it('should copy editor base url to ssr api url if ssr api url is omitted', () => {
process.env.HD_BASE_URL = 'https://editor.example.org/'
process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/'
delete process.env.HD_SSR_API_URL
const sut = new BaseUrlFromEnvExtractor()
const result = sut.extractBaseUrls()
expect(result).toStrictEqual({
editor: 'https://editor.example.org/',
renderer: 'https://renderer.example.org/',
ssrApi: 'https://editor.example.org/'
} as BaseUrls)
})
})

View file

@ -0,0 +1,98 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { isTestMode, isBuildTime, Logger } from '../utils/index.js'
import { parseUrl } from './parse-url.js'
import { NoValidUrlError } from './errors.js'
import { Optional } from '@mrdrogdrog/optional'
import { BaseUrls } from './base-urls.types.js'
/**
* Extracts and caches the editor and renderer base urls from the environment variables.
*/
export class BaseUrlFromEnvExtractor {
private baseUrls: BaseUrls | undefined
private readonly logger: Logger | undefined
constructor(withLogging: boolean = true) {
this.logger = withLogging ? new Logger('Base URL Configuration') : undefined
}
private extractEditorBaseUrlFromEnv(): URL {
return parseUrl(process.env.HD_BASE_URL).orElseThrow(
() => new NoValidUrlError('HD_BASE_URL')
)
}
private extractExtraUrlFromEnv(envVarName: string, editorBaseUrl: URL): URL {
if (isTestMode) {
this.logger?.info('Test mode activated. Using editor base url.')
return editorBaseUrl
}
const rendererBaseUrl = Optional.ofNullable(process.env[envVarName])
.filter((value) => value !== '')
.map((value) =>
parseUrl(value).orElseThrow(() => new NoValidUrlError(envVarName))
)
.orElse(undefined)
if (rendererBaseUrl === undefined) {
this.logger?.info(
`${envVarName} is unset. Using editor base url for renderer.`
)
return editorBaseUrl
} else {
return rendererBaseUrl
}
}
private renewBaseUrls(): BaseUrls {
const editorBaseUrl = this.extractEditorBaseUrlFromEnv()
const rendererBaseUrl = this.extractExtraUrlFromEnv(
'HD_RENDERER_BASE_URL',
editorBaseUrl
)
const ssrApiUrl = this.extractExtraUrlFromEnv(
'HD_SSR_API_URL',
editorBaseUrl
)
return {
editor: editorBaseUrl.toString(),
renderer: rendererBaseUrl.toString(),
ssrApi: ssrApiUrl.toString()
}
}
/**
* 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/',
ssrApi: '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())
}
}

View file

@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* Defines the URLs that are returned by the {@link BaseUrlFromEnvExtractor}.
*/
export interface BaseUrls {
renderer: string
editor: string
ssrApi: string
}

View file

@ -4,14 +4,29 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* Thrown if an {@link URL} contains a subdirectory.
*/
export class NoSubdirectoryAllowedError extends Error {
constructor() {
super('Subdirectories are not allowed')
}
}
/**
* Thrown if the protocol of an {@link URL} isn't https or http.
*/
export class WrongProtocolError extends Error {
constructor() {
super('Protocol must be HTTP or HTTPS')
}
}
/**
* Thrown if a value isn't a valid {@link URL}.
*/
export class NoValidUrlError extends Error {
constructor(varName: string) {
super(`${varName} is no valid URL`)
}
}

View file

@ -6,3 +6,5 @@
export { parseUrl } from './parse-url.js'
export { NoSubdirectoryAllowedError, WrongProtocolError } from './errors.js'
export { BaseUrlFromEnvExtractor } from './base-url-from-env-extractor.js'
export type { BaseUrls } from './base-urls.types.js'

View file

@ -33,7 +33,7 @@ describe('validate url', () => {
})
})
describe('trailing slash', () => {
describe('subpaths', () => {
it('accepts urls with just domain with trailing slash', () => {
expect(parseUrl('http://example.org/').get().toString()).toEqual(
'http://example.org/'

View file

@ -45,4 +45,15 @@ rm -f dist/frontend/.env
rm -rf dist/frontend/public/public
rm -rf dist/frontend/src
echo "🦔 > Patching env var check into prod build"
cat << EOF > dist/frontend/server.js.new
const { BaseUrlFromEnvExtractor } = require('@hedgedoc/commons')
new BaseUrlFromEnvExtractor(true).extractBaseUrls()
EOF
cat dist/frontend/server.js >> dist/frontend/server.js.new
rm dist/frontend/server.js
mv dist/frontend/server.js.new dist/frontend/server.js
echo "🦔 > Done! You can run the build by going into the dist directory and executing \`node frontend/server.js\`"

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
const { isMockMode, isTestMode, isProfilingMode, isBuildTime, Logger } = require('@hedgedoc/commons')
const { isMockMode, isTestMode, isProfilingMode, isBuildTime, Logger, BaseUrlFromEnvExtractor } = require('@hedgedoc/commons')
const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const withBundleAnalyzer = require('@next/bundle-analyzer')({
@ -13,6 +13,7 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
const logger = new Logger('Bootstrap')
logger.info('Node environment is', process.env.NODE_ENV)
new BaseUrlFromEnvExtractor(false).extractBaseUrls()
if (isTestMode) {
logger.warn(`This build runs in test mode. This means:

View file

@ -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 { baseUrlFromEnvExtractor } from '../../../utils/base-url-singelton'
interface PageProps {
params: { id: string | undefined }

View file

@ -12,12 +12,12 @@ import { DarkMode } from '../../components/layout/dark-mode/dark-mode'
import { ExpectedOriginBoundary } from '../../components/layout/expected-origin-boundary'
import { UiNotificationBoundary } from '../../components/notifications/ui-notification-boundary'
import { StoreProvider } from '../../redux/store-provider'
import { baseUrlFromEnvExtractor } from '../../utils/base-url-from-env-extractor'
import { configureLuxon } from '../../utils/configure-luxon'
import type { Metadata } from 'next'
import type { PropsWithChildren } from 'react'
import React from 'react'
import { getConfig } from '../../api/config'
import { baseUrlFromEnvExtractor } from '../../utils/base-url-singelton'
configureLuxon()

View file

@ -9,9 +9,9 @@ import { BaseUrlContextProvider } from '../../components/common/base-url/base-ur
import { FrontendConfigContextProvider } from '../../components/common/frontend-config-context/frontend-config-context-provider'
import { ExpectedOriginBoundary } from '../../components/layout/expected-origin-boundary'
import { StoreProvider } from '../../redux/store-provider'
import { baseUrlFromEnvExtractor } from '../../utils/base-url-from-env-extractor'
import React from 'react'
import { getConfig } from '../../api/config'
import { baseUrlFromEnvExtractor } from '../../utils/base-url-singelton'
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const baseUrls = baseUrlFromEnvExtractor.extractBaseUrls()

View file

@ -1,4 +1,5 @@
'use client'
import type { BaseUrls } from '@hedgedoc/commons'
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
@ -7,11 +8,6 @@
import type { PropsWithChildren } from 'react'
import React, { createContext } from 'react'
export interface BaseUrls {
renderer: string
editor: string
}
interface BaseUrlContextProviderProps {
baseUrls?: BaseUrls
}

View file

@ -1,76 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { BaseUrlFromEnvExtractor } from './base-url-from-env-extractor'
describe('BaseUrlFromEnvExtractor', () => {
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(sut.extractBaseUrls()).toStrictEqual({
renderer: 'https://renderer.example.org/',
editor: 'https://editor.example.org/'
})
})
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(() => 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(() => 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(() => 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(sut.extractBaseUrls()).toStrictEqual({
renderer: 'https://renderer.example.org/',
editor: 'https://editor.example.org/'
})
})
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(sut.extractBaseUrls()).toStrictEqual({
renderer: 'https://renderer.example.org/',
editor: 'https://editor.example.org/'
})
})
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(sut.extractBaseUrls()).toStrictEqual({
renderer: 'https://editor.example.org/',
editor: 'https://editor.example.org/'
})
})
})

View file

@ -1,94 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { BaseUrls } from '../components/common/base-url/base-url-context-provider'
import { NoSubdirectoryAllowedError, parseUrl, Logger, isTestMode, isBuildTime } from '@hedgedoc/commons'
import { Optional } from '@mrdrogdrog/optional'
/**
* Extracts and caches the editor and renderer base urls from the environment variables.
*/
export class BaseUrlFromEnvExtractor {
private baseUrls: BaseUrls | undefined
private readonly logger = new Logger('Base URL Configuration')
private extractUrlFromEnvVar(envVarName: string, envVarValue: string | undefined): Optional<URL> {
try {
return parseUrl(envVarValue)
} catch (error) {
if (error instanceof NoSubdirectoryAllowedError) {
this.logger.error(error.message)
return Optional.empty()
} else {
throw error
}
}
}
private extractEditorBaseUrlFromEnv(): Optional<URL> {
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<URL> {
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())
}
}
export const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()

View file

@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { BaseUrlFromEnvExtractor } from '@hedgedoc/commons'
export const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()