Refactor handling of environment variables (#2303)

* Refactor environment variables

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-09-16 11:03:29 +02:00 committed by GitHub
parent e412115a78
commit 39a4125cb0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
85 changed files with 624 additions and 461 deletions

View file

@ -1,13 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { isMockMode } from './test-modes'
import { backendUrl } from './backend-url'
/**
* Generates the url to the api.
*/
export const apiUrl = isMockMode ? `/api/mock-backend/private/` : `${backendUrl}api/private/`

View file

@ -1,30 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { isMockMode } from './test-modes'
/**
* Generates the backend URL from the environment variable `NEXT_PUBLIC_BACKEND_BASE_URL` or the mock default if mock mode is activated.
*
* @throws Error if the environment variable is unset or doesn't end with "/"
* @return the backend url that should be used in the app
*/
const generateBackendUrl = (): string => {
if (!isMockMode) {
const backendUrl = process.env.NEXT_PUBLIC_BACKEND_BASE_URL
if (backendUrl === undefined) {
throw new Error('NEXT_PUBLIC_BACKEND_BASE_URL is unset and mock mode is disabled')
} else if (!backendUrl.endsWith('/')) {
throw new Error("NEXT_PUBLIC_BACKEND_BASE_URL must end with an '/'")
} else {
return backendUrl
}
} else {
return '/'
}
}
export const backendUrl = generateBackendUrl()

View file

@ -0,0 +1,68 @@
/*
* SPDX-FileCopyrightText: 2022 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_EDITOR_BASE_URL = 'https://editor.example.org/'
process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/'
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
const result = baseUrlFromEnvExtractor.extractBaseUrls()
expect(result.isPresent()).toBeTruthy()
expect(result.get()).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_EDITOR_BASE_URL = undefined
process.env.HD_RENDERER_BASE_URL = undefined
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy()
})
it("should return an empty optional if editor base url isn't an URL", () => {
process.env.HD_EDITOR_BASE_URL = 'bibedibabedibu'
process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/'
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy()
})
it("should return an empty optional if renderer base url isn't an URL", () => {
process.env.HD_EDITOR_BASE_URL = 'https://editor.example.org/'
process.env.HD_RENDERER_BASE_URL = 'bibedibabedibu'
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy()
})
it("should return an empty optional if editor base url isn't ending with a slash", () => {
process.env.HD_EDITOR_BASE_URL = 'https://editor.example.org'
process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/'
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy()
})
it("should return an empty optional if renderer base url isn't ending with a slash", () => {
process.env.HD_EDITOR_BASE_URL = 'https://editor.example.org/'
process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org'
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy()
})
it('should copy editor base url to renderer base url if renderer base url is omitted', () => {
process.env.HD_EDITOR_BASE_URL = 'https://editor.example.org/'
delete process.env.HD_RENDERER_BASE_URL
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
const result = baseUrlFromEnvExtractor.extractBaseUrls()
expect(result.isPresent()).toBeTruthy()
expect(result.get()).toStrictEqual({
renderer: 'https://editor.example.org/',
editor: 'https://editor.example.org/'
})
})
})

View file

@ -0,0 +1,89 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Optional } from '@mrdrogdrog/optional'
import type { BaseUrls } from '../components/common/base-url/base-url-context-provider'
import { Logger } from './logger'
import { isTestMode } from './test-modes'
/**
* Extracts the editor and renderer base urls from the environment variables.
*/
export class BaseUrlFromEnvExtractor {
private baseUrls: Optional<BaseUrls> | undefined
private logger = new Logger('Base URL Configuration')
private extractUrlFromEnvVar(envVarName: string, envVarValue: string | undefined): Optional<URL> {
return Optional.ofNullable(envVarValue)
.filter((value) => {
const endsWithSlash = value.endsWith('/')
if (!endsWithSlash) {
this.logger.error(`${envVarName} must end with an '/'`)
}
return endsWithSlash
})
.map((value) => {
try {
return new URL(value)
} catch (error) {
return null
}
})
}
private extractEditorBaseUrlFromEnv(): Optional<URL> {
const envValue = this.extractUrlFromEnvVar('HD_EDITOR_BASE_URL', process.env.HD_EDITOR_BASE_URL)
if (envValue.isEmpty()) {
this.logger.error("HD_EDITOR_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(): void {
this.baseUrls = this.extractEditorBaseUrlFromEnv().flatMap((editorBaseUrl) =>
this.extractRendererBaseUrlFromEnv(editorBaseUrl).map((rendererBaseUrl) => {
return {
editor: editorBaseUrl.toString(),
renderer: rendererBaseUrl.toString()
}
})
)
this.baseUrls.ifPresent((urls) => {
this.logger.info('Editor base URL', urls.editor.toString())
this.logger.info('Renderer base URL', urls.renderer.toString())
})
}
private isEnvironmentExtractDone(): boolean {
return this.baseUrls !== undefined
}
/**
* Extracts the editor and renderer base urls from the environment variables.
*
* @return An {@link Optional} with the base urls.
*/
public extractBaseUrls(): Optional<BaseUrls> {
if (!this.isEnvironmentExtractDone()) {
this.renewBaseUrls()
}
return Optional.ofNullable(this.baseUrls).flatMap((value) => value)
}
}

View file

@ -1,15 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { backendUrl } from './backend-url'
import { isMockMode } from './test-modes'
/**
* Generates the url to the assets.
*/
export const customizeAssetsUrl = isMockMode
? `/mock-public/`
: process.env.NEXT_PUBLIC_CUSTOMIZE_ASSETS_URL || `${backendUrl}public/`

View file

@ -1,15 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* Checks if the given string is a positive answer (yes, true or 1).
*
* @param value The value to check
*/
export const isPositiveAnswer = (value: string) => {
const lowerValue = value.toLowerCase()
return lowerValue === 'yes' || lowerValue === '1' || lowerValue === 'true'
}

44
src/utils/test-modes.js Normal file
View file

@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* This file is intentionally a js and not a ts file because it is used in `next.config.js`
*/
/**
* Checks if the given string is a positive answer (yes, true or 1).
*
* @param {string} value The value to check
* @return {boolean} {@code true} if the value describes a positive answer string
*/
const isPositiveAnswer = (value) => {
const lowerValue = value.toLowerCase()
return lowerValue === 'yes' || lowerValue === '1' || lowerValue === 'true'
}
/**
* Defines if the current runtime is built in e2e test mode.
* @type boolean
*/
const isTestMode = !!process.env.NEXT_PUBLIC_TEST_MODE && isPositiveAnswer(process.env.NEXT_PUBLIC_TEST_MODE)
/**
* Defines if the current runtime should use the mocked backend.
* @type boolean
*/
const isMockMode = !!process.env.NEXT_PUBLIC_USE_MOCK_API && isPositiveAnswer(process.env.NEXT_PUBLIC_USE_MOCK_API)
/**
* Defines if the current runtime was built in development mode.
* @type boolean
*/
const isDevMode = process.env.NODE_ENV === 'development'
module.exports = {
isTestMode,
isMockMode,
isDevMode
}

View file

@ -1,23 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { isPositiveAnswer } from './is-positive-answer'
/**
* Checks if the current runtime is built in e2e test mode.
*/
export const isTestMode = !!process.env.NEXT_PUBLIC_TEST_MODE && isPositiveAnswer(process.env.NEXT_PUBLIC_TEST_MODE)
/**
* Checks if the current runtime should use the mocked backend.
*/
export const isMockMode =
!!process.env.NEXT_PUBLIC_USE_MOCK_API && isPositiveAnswer(process.env.NEXT_PUBLIC_USE_MOCK_API)
/**
* Checks if the current runtime was built in development mode.
*/
export const isDevMode = process.env.NODE_ENV === 'development'

View file

@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment, useMemo } from 'react'
import type { PropsWithChildren } from 'react'
import { isClientSideRendering } from './is-client-side-rendering'
import { useBaseUrl } from '../hooks/common/use-base-url'
/**
* Checks if the url of the current browser window matches the expected origin.
* This is necessary to ensure that the render endpoint is only opened from the rendering origin.
*
* @param children The children react element that should be rendered if the origin is correct
*/
export const ExpectedOriginBoundary: React.FC<PropsWithChildren> = ({ children }) => {
const baseUrl = useBaseUrl()
const expectedOrigin = useMemo(() => new URL(baseUrl).origin, [baseUrl])
if (isClientSideRendering() && window.location.origin !== expectedOrigin) {
return <span>{`You can't open this page using this URL. For this endpoint "${expectedOrigin}" is expected.`}</span>
} else {
return <Fragment>{children}</Fragment>
}
}