From 5e1fdbe81d62c9daffca9109a02edce1d50d7150 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Sun, 5 Feb 2023 09:31:33 +0100 Subject: [PATCH] fix(config): Replace HD_DOMAIN and HD_EDITOR_BASE_URL with HD_BASE_URL Signed-off-by: Tilman Vatteroth --- .../frontend-netlify-deploy-main.yml | 2 +- .../workflows/frontend-netlify-deploy-pr.yml | 2 +- backend/.env.example | 2 +- backend/docker/README.md | 2 +- backend/src/config/app.config.spec.ts | 68 ++++++++++++------- backend/src/config/app.config.ts | 46 ++++++++++--- backend/src/config/mock/app.config.mock.ts | 2 +- .../frontend-config.service.spec.ts | 4 +- backend/test/app.e2e-spec.ts | 2 +- commons/src/index.ts | 6 ++ commons/src/utils/errors.ts | 17 +++++ commons/src/utils/parse-url.test.ts | 57 ++++++++++++++++ commons/src/utils/parse-url.ts | 35 ++++++++++ docs/content/config/index.md | 20 +++--- docs/content/dev/getting-started.md | 10 +-- docs/content/dev/setup/frontend.md | 4 +- frontend/.env.development | 2 +- frontend/.env.test | 2 +- frontend/package.json | 2 +- .../utils/base-url-from-env-extractor.test.ts | 32 ++++++--- .../src/utils/base-url-from-env-extractor.ts | 30 ++++---- 21 files changed, 255 insertions(+), 92 deletions(-) create mode 100644 commons/src/utils/errors.ts create mode 100644 commons/src/utils/parse-url.test.ts create mode 100644 commons/src/utils/parse-url.ts diff --git a/.github/workflows/frontend-netlify-deploy-main.yml b/.github/workflows/frontend-netlify-deploy-main.yml index a9e5c4085..99a635a15 100644 --- a/.github/workflows/frontend-netlify-deploy-main.yml +++ b/.github/workflows/frontend-netlify-deploy-main.yml @@ -73,7 +73,7 @@ jobs: - name: Patch base URL if: needs.changes.outputs.changed == 'true' - run: echo "HD_EDITOR_BASE_URL=\"https://hedgedoc.dev/\"" >> .env.production + run: echo "HD_BASE_URL=\"https://hedgedoc.dev/\"" >> .env.production - name: Build app if: needs.changes.outputs.changed == 'true' diff --git a/.github/workflows/frontend-netlify-deploy-pr.yml b/.github/workflows/frontend-netlify-deploy-pr.yml index ff84af293..e83859eea 100644 --- a/.github/workflows/frontend-netlify-deploy-pr.yml +++ b/.github/workflows/frontend-netlify-deploy-pr.yml @@ -96,7 +96,7 @@ jobs: - name: Patch base URL if: needs.changes.outputs.changed == 'true' - run: echo "HD_EDITOR_BASE_URL=\"${{ env.DEPLOY_URL }}\"" >> .env.production + run: echo "HD_BASE_URL=\"${{ env.DEPLOY_URL }}\"" >> .env.production - name: Build app if: needs.changes.outputs.changed == 'true' diff --git a/backend/.env.example b/backend/.env.example index 59ff66658..962e2935a 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: CC0-1.0 -HD_DOMAIN="http://localhost" +HD_BASE_URL="http://localhost/" HD_MEDIA_BACKEND="filesystem" HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH="uploads/" HD_DATABASE_TYPE="sqlite" diff --git a/backend/docker/README.md b/backend/docker/README.md index a9c27d773..240de7577 100644 --- a/backend/docker/README.md +++ b/backend/docker/README.md @@ -21,7 +21,7 @@ To build a production image, run the following command *from the root of the rep When you run the image, you need to provide environment variables to configure HedgeDoc. See [the config docs](../../docs/content/config/index.md) for more information. This example starts HedgeDoc on localhost, with non-persistent storage: -`docker run -e HD_DOMAIN=http://localhost -e HD_MEDIA_BACKEND=filesystem -e HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH=uploads -e HD_DATABASE_TYPE=sqlite -e HD_DATABASE_NAME=hedgedoc.sqlite -e HD_SESSION_SECRET=foobar -e HD_LOGLEVEL=debug -p 3000:3000 hedgedoc-prod` +`docker run -e HD_BASE_URL=http://localhost -e HD_MEDIA_BACKEND=filesystem -e HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH=uploads -e HD_DATABASE_TYPE=sqlite -e HD_DATABASE_NAME=hedgedoc.sqlite -e HD_SESSION_SECRET=foobar -e HD_LOGLEVEL=debug -p 3000:3000 hedgedoc-prod` ## Build a development image diff --git a/backend/src/config/app.config.spec.ts b/backend/src/config/app.config.spec.ts index 37e350d26..1ca04e061 100644 --- a/backend/src/config/app.config.spec.ts +++ b/backend/src/config/app.config.spec.ts @@ -9,9 +9,9 @@ import appConfig from './app.config'; import { Loglevel } from './loglevel.enum'; describe('appConfig', () => { - const domain = 'https://example.com'; - const invalidDomain = 'localhost'; - const rendererBaseUrl = 'https://render.example.com'; + const baseUrl = 'https://example.com/'; + const invalidBaseUrl = 'localhost'; + const rendererBaseUrl = 'https://render.example.com/'; const port = 3333; const negativePort = -9000; const floatPort = 3.14; @@ -26,7 +26,7 @@ describe('appConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_DOMAIN: domain, + HD_BASE_URL: baseUrl, HD_RENDERER_BASE_URL: rendererBaseUrl, PORT: port.toString(), HD_LOGLEVEL: loglevel, @@ -38,7 +38,7 @@ describe('appConfig', () => { }, ); const config = appConfig(); - expect(config.domain).toEqual(domain); + expect(config.baseUrl).toEqual(baseUrl); expect(config.rendererBaseUrl).toEqual(rendererBaseUrl); expect(config.port).toEqual(port); expect(config.loglevel).toEqual(loglevel); @@ -50,7 +50,7 @@ describe('appConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_DOMAIN: domain, + HD_BASE_URL: baseUrl, PORT: port.toString(), HD_LOGLEVEL: loglevel, HD_PERSIST_INTERVAL: '100', @@ -61,8 +61,8 @@ describe('appConfig', () => { }, ); const config = appConfig(); - expect(config.domain).toEqual(domain); - expect(config.rendererBaseUrl).toEqual(domain); + expect(config.baseUrl).toEqual(baseUrl); + expect(config.rendererBaseUrl).toEqual(baseUrl); expect(config.port).toEqual(port); expect(config.loglevel).toEqual(loglevel); expect(config.persistInterval).toEqual(100); @@ -73,7 +73,7 @@ describe('appConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_DOMAIN: domain, + HD_BASE_URL: baseUrl, HD_RENDERER_BASE_URL: rendererBaseUrl, HD_LOGLEVEL: loglevel, HD_PERSIST_INTERVAL: '100', @@ -84,7 +84,7 @@ describe('appConfig', () => { }, ); const config = appConfig(); - expect(config.domain).toEqual(domain); + expect(config.baseUrl).toEqual(baseUrl); expect(config.rendererBaseUrl).toEqual(rendererBaseUrl); expect(config.port).toEqual(3000); expect(config.loglevel).toEqual(loglevel); @@ -96,7 +96,7 @@ describe('appConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_DOMAIN: domain, + HD_BASE_URL: baseUrl, HD_RENDERER_BASE_URL: rendererBaseUrl, PORT: port.toString(), HD_PERSIST_INTERVAL: '100', @@ -107,7 +107,7 @@ describe('appConfig', () => { }, ); const config = appConfig(); - expect(config.domain).toEqual(domain); + expect(config.baseUrl).toEqual(baseUrl); expect(config.rendererBaseUrl).toEqual(rendererBaseUrl); expect(config.port).toEqual(port); expect(config.loglevel).toEqual(Loglevel.WARN); @@ -119,7 +119,7 @@ describe('appConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_DOMAIN: domain, + HD_BASE_URL: baseUrl, HD_RENDERER_BASE_URL: rendererBaseUrl, HD_LOGLEVEL: loglevel, PORT: port.toString(), @@ -130,7 +130,7 @@ describe('appConfig', () => { }, ); const config = appConfig(); - expect(config.domain).toEqual(domain); + expect(config.baseUrl).toEqual(baseUrl); expect(config.rendererBaseUrl).toEqual(rendererBaseUrl); expect(config.port).toEqual(port); expect(config.loglevel).toEqual(Loglevel.TRACE); @@ -142,7 +142,7 @@ describe('appConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_DOMAIN: domain, + HD_BASE_URL: baseUrl, HD_RENDERER_BASE_URL: rendererBaseUrl, HD_LOGLEVEL: loglevel, PORT: port.toString(), @@ -154,7 +154,7 @@ describe('appConfig', () => { }, ); const config = appConfig(); - expect(config.domain).toEqual(domain); + expect(config.baseUrl).toEqual(baseUrl); expect(config.rendererBaseUrl).toEqual(rendererBaseUrl); expect(config.port).toEqual(port); expect(config.loglevel).toEqual(Loglevel.TRACE); @@ -163,11 +163,11 @@ describe('appConfig', () => { }); }); describe('throws error', () => { - it('when given a non-valid HD_DOMAIN', async () => { + it('when given a non-valid HD_BASE_URL', async () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_DOMAIN: invalidDomain, + HD_BASE_URL: invalidBaseUrl, PORT: port.toString(), HD_LOGLEVEL: loglevel, /* eslint-enable @typescript-eslint/naming-convention */ @@ -176,7 +176,25 @@ describe('appConfig', () => { clear: true, }, ); - expect(() => appConfig()).toThrow('HD_DOMAIN'); + expect(() => appConfig()).toThrow('HD_BASE_URL'); + restore(); + }); + + it('when given a base url with path but no trailing slash in HD_BASE_URL', async () => { + const restore = mockedEnv( + { + /* eslint-disable @typescript-eslint/naming-convention */ + HD_BASE_URL: 'https://example.org/a', + HD_LOGLEVEL: loglevel, + /* eslint-enable @typescript-eslint/naming-convention */ + }, + { + clear: true, + }, + ); + expect(() => appConfig()).toThrow( + '"HD_BASE_URL" must end with a trailing slash', + ); restore(); }); @@ -184,7 +202,7 @@ describe('appConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_DOMAIN: domain, + HD_BASE_URL: baseUrl, PORT: negativePort.toString(), HD_LOGLEVEL: loglevel, /* eslint-enable @typescript-eslint/naming-convention */ @@ -201,7 +219,7 @@ describe('appConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_DOMAIN: domain, + HD_BASE_URL: baseUrl, PORT: outOfRangePort.toString(), HD_LOGLEVEL: loglevel, /* eslint-enable @typescript-eslint/naming-convention */ @@ -220,7 +238,7 @@ describe('appConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_DOMAIN: domain, + HD_BASE_URL: baseUrl, PORT: floatPort.toString(), HD_LOGLEVEL: loglevel, /* eslint-enable @typescript-eslint/naming-convention */ @@ -237,7 +255,7 @@ describe('appConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_DOMAIN: domain, + HD_BASE_URL: baseUrl, PORT: invalidPort, HD_LOGLEVEL: loglevel, /* eslint-enable @typescript-eslint/naming-convention */ @@ -254,7 +272,7 @@ describe('appConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_DOMAIN: domain, + HD_BASE_URL: baseUrl, PORT: port.toString(), HD_LOGLEVEL: invalidLoglevel, /* eslint-enable @typescript-eslint/naming-convention */ @@ -271,7 +289,7 @@ describe('appConfig', () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_DOMAIN: domain, + HD_BASE_URL: baseUrl, PORT: port.toString(), HD_LOGLEVEL: invalidLoglevel, HD_PERSIST_INTERVAL: invalidPersistInterval.toString(), diff --git a/backend/src/config/app.config.ts b/backend/src/config/app.config.ts index b032a7265..2d546bbc7 100644 --- a/backend/src/config/app.config.ts +++ b/backend/src/config/app.config.ts @@ -3,31 +3,50 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { + MissingTrailingSlashError, + parseUrl, + WrongProtocolError, +} from '@hedgedoc/commons'; import { registerAs } from '@nestjs/config'; import * as Joi from 'joi'; +import { CustomHelpers, ErrorReport } from 'joi'; import { Loglevel } from './loglevel.enum'; import { buildErrorMessage, parseOptionalNumber } from './utils'; export interface AppConfig { - domain: string; + baseUrl: string; rendererBaseUrl: string; port: number; loglevel: Loglevel; persistInterval: number; } +function validateUrlWithTrailingSlash( + value: string, + helpers: CustomHelpers, +): string | ErrorReport { + try { + return parseUrl(value).isPresent() ? value : helpers.error('string.uri'); + } catch (error) { + if (error instanceof MissingTrailingSlashError) { + return helpers.error('url.missingTrailingSlash'); + } else if (error instanceof WrongProtocolError) { + return helpers.error('url.wrongProtocol'); + } else { + throw error; + } + } +} + const schema = Joi.object({ - domain: Joi.string() - .uri({ - scheme: /https?/, - }) - .label('HD_DOMAIN'), + baseUrl: Joi.string() + .custom(validateUrlWithTrailingSlash) + .label('HD_BASE_URL'), rendererBaseUrl: Joi.string() - .uri({ - scheme: /https?/, - }) - .default(Joi.ref('domain')) + .custom(validateUrlWithTrailingSlash) + .default(Joi.ref('baseUrl')) .optional() .label('HD_RENDERER_BASE_URL'), port: Joi.number() @@ -48,12 +67,17 @@ const schema = Joi.object({ .default(10) .optional() .label('HD_PERSIST_INTERVAL'), +}).messages({ + // eslint-disable-next-line @typescript-eslint/naming-convention + 'url.missingTrailingSlash': '{{#label}} must end with a trailing slash', + // eslint-disable-next-line @typescript-eslint/naming-convention + 'url.wrongProtocol': '{{#label}} protocol must be HTTP or HTTPS', }); export default registerAs('appConfig', () => { const appConfig = schema.validate( { - domain: process.env.HD_DOMAIN, + baseUrl: process.env.HD_BASE_URL, rendererBaseUrl: process.env.HD_RENDERER_BASE_URL, port: parseOptionalNumber(process.env.PORT), loglevel: process.env.HD_LOGLEVEL, diff --git a/backend/src/config/mock/app.config.mock.ts b/backend/src/config/mock/app.config.mock.ts index 3718a8583..2e0432754 100644 --- a/backend/src/config/mock/app.config.mock.ts +++ b/backend/src/config/mock/app.config.mock.ts @@ -11,7 +11,7 @@ import { Loglevel } from '../loglevel.enum'; export function createDefaultMockAppConfig(): AppConfig { return { - domain: 'md.example.com', + baseUrl: 'md.example.com', rendererBaseUrl: 'md-renderer.example.com', port: 3000, loglevel: Loglevel.ERROR, diff --git a/backend/src/frontend-config/frontend-config.service.spec.ts b/backend/src/frontend-config/frontend-config.service.spec.ts index 849c0a40d..025412ea8 100644 --- a/backend/src/frontend-config/frontend-config.service.spec.ts +++ b/backend/src/frontend-config/frontend-config.service.spec.ts @@ -167,7 +167,7 @@ describe('FrontendConfigService', () => { ]) { it(`works with ${JSON.stringify(authConfigConfigured)}`, async () => { const appConfig: AppConfig = { - domain: domain, + baseUrl: domain, rendererBaseUrl: 'https://renderer.example.org', port: 3000, loglevel: Loglevel.ERROR, @@ -325,7 +325,7 @@ describe('FrontendConfigService', () => { ]) { it(`combination #${index} works`, async () => { const appConfig: AppConfig = { - domain: domain, + baseUrl: domain, rendererBaseUrl: 'https://renderer.example.org', port: 3000, loglevel: Loglevel.ERROR, diff --git a/backend/test/app.e2e-spec.ts b/backend/test/app.e2e-spec.ts index 1aae4f2e3..cf96c2497 100644 --- a/backend/test/app.e2e-spec.ts +++ b/backend/test/app.e2e-spec.ts @@ -18,7 +18,7 @@ describe('App', () => { }) .overrideProvider(getConfigToken('appConfig')) .useValue({ - domain: 'localhost', + baseUrl: 'localhost', port: 3333, }) .overrideProvider(getConfigToken('mediaConfig')) diff --git a/commons/src/index.ts b/commons/src/index.ts index 43a13b828..456944ea3 100644 --- a/commons/src/index.ts +++ b/commons/src/index.ts @@ -24,6 +24,12 @@ export { encodeServerVersionUpdatedMessage } from './messages/server-version-upd export { WebsocketTransporter } from './websocket-transporter.js' +export { parseUrl } from './utils/parse-url.js' +export { + MissingTrailingSlashError, + WrongProtocolError +} from './utils/errors.js' + export type { MessageTransporterEvents } from './y-doc-message-transporter.js' export { waitForOtherPromisesToFinish } from './utils/wait-for-other-promises-to-finish.js' diff --git a/commons/src/utils/errors.ts b/commons/src/utils/errors.ts new file mode 100644 index 000000000..48632b00e --- /dev/null +++ b/commons/src/utils/errors.ts @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class MissingTrailingSlashError extends Error { + constructor() { + super("Path doesn't end with a trailing slash") + } +} + +export class WrongProtocolError extends Error { + constructor() { + super('Protocol must be HTTP or HTTPS') + } +} diff --git a/commons/src/utils/parse-url.test.ts b/commons/src/utils/parse-url.test.ts new file mode 100644 index 000000000..2884d60e6 --- /dev/null +++ b/commons/src/utils/parse-url.test.ts @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { MissingTrailingSlashError, WrongProtocolError } from './errors.js' +import { parseUrl } from './parse-url.js' + +describe('validate url', () => { + it("doesn't accept non-urls", () => { + expect(parseUrl('noUrl').isEmpty()).toBeTruthy() + }) + + describe('protocols', () => { + it('works with http', () => { + expect(parseUrl('http://example.org').get().toString()).toEqual( + 'http://example.org/' + ) + }) + it('works with https', () => { + expect(parseUrl('https://example.org').get().toString()).toEqual( + 'https://example.org/' + ) + }) + it("doesn't work without protocol", () => { + expect(() => parseUrl('example.org').isEmpty()).toBeTruthy() + }) + it("doesn't work any other protocol", () => { + expect(() => parseUrl('git://example.org').get()).toThrowError( + WrongProtocolError + ) + }) + }) + + describe('trailing slash', () => { + it('accepts urls with just domain with trailing slash', () => { + expect(parseUrl('http://example.org/').get().toString()).toEqual( + 'http://example.org/' + ) + }) + it('accepts urls with just domain without trailing slash', () => { + expect(parseUrl('http://example.org').get().toString()).toEqual( + 'http://example.org/' + ) + }) + it('accepts urls with with subpath and trailing slash', () => { + expect(parseUrl('http://example.org/asd/').get().toString()).toEqual( + 'http://example.org/asd/' + ) + }) + it("doesn't accept urls with with subpath and without trailing slash", () => { + expect(() => parseUrl('http://example.org/asd').get().toString()).toThrow( + MissingTrailingSlashError + ) + }) + }) +}) diff --git a/commons/src/utils/parse-url.ts b/commons/src/utils/parse-url.ts new file mode 100644 index 000000000..81d9c428e --- /dev/null +++ b/commons/src/utils/parse-url.ts @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { MissingTrailingSlashError, WrongProtocolError } from './errors.js' +import { Optional } from '@mrdrogdrog/optional' + +/** + * Parses the given string as URL + * + * @param {String | undefined} url the raw url + * @return An {@link Optional} that contains the parsed URL or is empty if the raw value isn't a valid URL + * @throws WrongProtocolError if the protocol of the URL isn't either http nor https + * @throws MissingTrailingSlashError if the URL has a path that doesn't end with a trailing slash + */ +export function parseUrl(url: string | undefined): Optional { + return createOptionalUrl(url) + .guard( + (value) => value.protocol === 'https:' || value.protocol === 'http:', + () => new WrongProtocolError() + ) + .guard( + (value) => value.pathname.endsWith('/'), + () => new MissingTrailingSlashError() + ) +} + +function createOptionalUrl(url: string | undefined): Optional { + try { + return Optional.ofNullable(url).map((value) => new URL(value)) + } catch (error) { + return Optional.empty() + } +} diff --git a/docs/content/config/index.md b/docs/content/config/index.md index ee1b06d1b..1bc32b24c 100644 --- a/docs/content/config/index.md +++ b/docs/content/config/index.md @@ -5,7 +5,7 @@ HedgeDoc can be configured via environment variables either directly or via an ` The `.env` file should be placed in the root of the project and contains key-value pairs of environment variables and their corresponding value. This can for example look like this: ```ini -HD_DOMAIN="http://localhost" +HD_BASE_URL="http://localhost" HD_MEDIA_BACKEND="filesystem" HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH="uploads/" HD_DATABASE_TYPE="sqlite" @@ -19,15 +19,15 @@ We also provide an `.env.example` file containing a minimal configuration in the ## General -| environment variable | default | example | description | -|--------------------------|-----------|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| -| `HD_DOMAIN` | - | `https://md.example.com` | The URL the HedgeDoc instance runs on. | -| `PORT` | 3000 | | The port the HedgeDoc instance runs on. | -| `HD_RENDERER_BASE_URL` | HD_DOMAIN | | The URL the renderer runs on. If omitted this will be same as `HD_DOMAIN`. | -| `HD_LOGLEVEL` | warn | | The loglevel that should be used. Options are `error`, `warn`, `info`, `debug` or `trace`. | -| `HD_FORBIDDEN_NOTE_IDS` | - | `notAllowed,alsoNotAllowed` | A list of note ids (separated by `,`), that are not allowed to be created or requested by anyone. | -| `HD_MAX_DOCUMENT_LENGTH` | 100000 | | The maximum length of any one document. Changes to this will impact performance for your users. | -| `HD_PERSIST_INTERVAL` | 10 | `0`, `5`, `10`, `20` | The time interval in **minutes** for the periodic note revision creation during realtime editing. `0` deactivates the periodic note revision creation. | +| environment variable | default | example | description | +|--------------------------|------------------------|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| `HD_BASE_URL` | - | `https://md.example.com` | The URL the HedgeDoc instance runs on. | +| `PORT` | 3000 | | The port the HedgeDoc instance runs on. | +| `HD_RENDERER_BASE_URL` | Content of HD_BASE_URL | | The URL the renderer runs on. If omitted this will be the same as `HD_BASE_URL`. | +| `HD_LOGLEVEL` | warn | | The loglevel that should be used. Options are `error`, `warn`, `info`, `debug` or `trace`. | +| `HD_FORBIDDEN_NOTE_IDS` | - | `notAllowed,alsoNotAllowed` | A list of note ids (separated by `,`), that are not allowed to be created or requested by anyone. | +| `HD_MAX_DOCUMENT_LENGTH` | 100000 | | The maximum length of any one document. Changes to this will impact performance for your users. | +| `HD_PERSIST_INTERVAL` | 10 | `0`, `5`, `10`, `20` | The time interval in **minutes** for the periodic note revision creation during realtime editing. `0` deactivates the periodic note revision creation. | ### Why should I want to run my renderer on a different (sub-)domain? diff --git a/docs/content/dev/getting-started.md b/docs/content/dev/getting-started.md index edf8920f2..dde939666 100644 --- a/docs/content/dev/getting-started.md +++ b/docs/content/dev/getting-started.md @@ -78,7 +78,7 @@ This only needs to be done once, except if you've changed code in the commons pa 3. Make sure that you've set `HD_SESSION_SECRET` in your `.env` file. Otherwise, the backend won't start. > In dev mode you don't need a secure secret. So use any value. If you want to generate a secure session secret you can use e.g. `openssl rand -hex 16 | sed -E 's/(.*)/HD_SESSION_SECRET=\1/' >> .env`. -4. Make sure that `HD_DOMAIN` in `.env` is set to the domain where Hedgedoc should be available. In local dev +4. Make sure that `HD_BASE_URL` in `.env` is set to the base url where HedgeDoc should be available. In local dev environment this is most likely `http://localhost:8080`. 5. Start the backend by running `yarn start:dev` for dev mode or `yarn start` for production. @@ -99,7 +99,7 @@ In development mode the app will autoload changes you make to the code. ### With local backend To start the development mode with an actual HedgeDoc backend use `yarn run dev:with-local-backend` instead. -This task will automatically set `HD_EDITOR_BASE_URL` to `http://localhost:8080`. +This task will automatically set `HD_BASE_URL` to `http://localhost:8080`. ### Production mode @@ -107,11 +107,11 @@ Use `yarn build` to build the app in production mode and save it into the `.next minimized and optimized for best performance. Don't edit the generated files in the `.next` folder in any way! You can run the production build using the built-in server with `yarn start`. -You MUST provide the environment variable `HD_EDITOR_BASE_URL` with protocol, domain and (if needed) subdirectory path ( +You MUST provide the environment variable `HD_BASE_URL` with protocol, domain and (if needed) subdirectory path ( e.g. `http://localhost:3001/`) so the app knows under which URL the frontend is available in the browser. -If you use the production build then make sure that you set the environment variable `HD_EDITOR_BASE_URL` to the same -value as `HD_DOMAIN` in the backend. +If you use the production build then make sure that you set the environment variable `HD_BASE_URL` to the same +value as `HD_BASE_URL` in the backend. ### Production mock build diff --git a/docs/content/dev/setup/frontend.md b/docs/content/dev/setup/frontend.md index 783a41545..902c7e68c 100644 --- a/docs/content/dev/setup/frontend.md +++ b/docs/content/dev/setup/frontend.md @@ -11,8 +11,8 @@ The following environment variables are recognized by the frontend process. | Name | Possible Values | Description | |--------------------------|----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| HD_EDITOR_BASE_URL | Any URL with protocol, domain and optionally directory and port. Must end with a trailing slash. (e.g. `http://localhost:3001/`) | The URL under which the frontend is expected. Setting this is mandatory so the server side rendering can generate assets URLs. You only need to set this yourself if you use the production mode. | -| HD_RENDERER_BASE_URL | Same as `HD_EDITOR_BASE_URL` | You can provide this variable if the renderer should use another domain than the editor. This is recommended for security reasons but not mandatory. This variable is optional and will fallback to `HD_EDITOR_BASE_URL` | +| HD_BASE_URL | Any URL with protocol, domain and optionally directory and port. Must end with a trailing slash. (e.g. `http://localhost:3001/`) | The URL under which the frontend is expected. Setting this is mandatory so the server side rendering can generate assets URLs. You only need to set this yourself if you use the production mode. | +| HD_RENDERER_BASE_URL | Same as `HD_BASE_URL` | You can provide this variable if the renderer should use another domain than the editor. This is recommended for security reasons but not mandatory. This variable is optional and will fallback to `HD_BASE_URL` | | NEXT_PUBLIC_USE_MOCK_API | `true`, `false` | Will activate the mocked backend | | NEXT_PUBLIC_TEST_MODE | `true`, `false` | Will activate additional HTML attributes that are used to identify elements for test suits. | diff --git a/frontend/.env.development b/frontend/.env.development index 5e63b4028..86efdca8a 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -1,3 +1,3 @@ NEXT_PUBLIC_USE_MOCK_API=true -HD_EDITOR_BASE_URL="http://localhost:3001/" +HD_BASE_URL="http://localhost:3001/" HD_RENDERER_BASE_URL="http://127.0.0.1:3001/" diff --git a/frontend/.env.test b/frontend/.env.test index 3a8a4f65d..e49956e4a 100644 --- a/frontend/.env.test +++ b/frontend/.env.test @@ -1,3 +1,3 @@ NEXT_PUBLIC_USE_MOCK_API=true -HD_EDITOR_BASE_URL="http://127.0.0.1:3001/" +HD_BASE_URL="http://127.0.0.1:3001/" HD_RENDERER_BASE_URL="http://127.0.0.1:3001/" diff --git a/frontend/package.json b/frontend/package.json index e68a810af..c6c7914cb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,7 @@ "analyze": "cross-env ANALYZE=true yarn build", "dev": "cross-env PORT=3001 next dev", "dev:test": "cross-env PORT=3001 NODE_ENV=test NEXT_PUBLIC_TEST_MODE=true next dev", - "dev:with-local-backend": "cross-env PORT=3001 NEXT_PUBLIC_USE_MOCK_API=false HD_EDITOR_BASE_URL=http://localhost:8080/ HD_RENDERER_BASE_URL=http://localhost:8080/ next dev", + "dev:with-local-backend": "cross-env PORT=3001 NEXT_PUBLIC_USE_MOCK_API=false HD_BASE_URL=http://localhost:8080/ HD_RENDERER_BASE_URL=http://localhost:8080/ next dev", "format": "prettier -c \"src/**/*.{ts,tsx,js}\" \"cypress/**/*.{ts,tsx}\"", "format:fix": "prettier -w \"src/**/*.{ts,tsx,js}\" \"cypress/**/*.{ts,tsx}\"", "lint": "eslint --max-warnings=0 --ext .ts,.tsx src", diff --git a/frontend/src/utils/base-url-from-env-extractor.test.ts b/frontend/src/utils/base-url-from-env-extractor.test.ts index 0bcf65673..530c16d3a 100644 --- a/frontend/src/utils/base-url-from-env-extractor.test.ts +++ b/frontend/src/utils/base-url-from-env-extractor.test.ts @@ -7,7 +7,7 @@ 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_BASE_URL = 'https://editor.example.org/' process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/' const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() const result = baseUrlFromEnvExtractor.extractBaseUrls() @@ -19,42 +19,52 @@ describe('BaseUrlFromEnvExtractor', () => { }) it('should return an empty optional if no var is set', () => { - process.env.HD_EDITOR_BASE_URL = undefined + process.env.HD_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_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_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' + 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 baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() - expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy() + 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 renderer base url isn't ending with a slash", () => { - process.env.HD_EDITOR_BASE_URL = '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 baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() - expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy() + const result = baseUrlFromEnvExtractor.extractBaseUrls() + expect(result.isPresent()).toBeTruthy() + expect(result.get()).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_EDITOR_BASE_URL = 'https://editor.example.org/' + process.env.HD_BASE_URL = 'https://editor.example.org/' delete process.env.HD_RENDERER_BASE_URL const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() const result = baseUrlFromEnvExtractor.extractBaseUrls() diff --git a/frontend/src/utils/base-url-from-env-extractor.ts b/frontend/src/utils/base-url-from-env-extractor.ts index f1664deda..9ee57d0a0 100644 --- a/frontend/src/utils/base-url-from-env-extractor.ts +++ b/frontend/src/utils/base-url-from-env-extractor.ts @@ -6,6 +6,7 @@ import type { BaseUrls } from '../components/common/base-url/base-url-context-provider' import { Logger } from './logger' import { isTestMode } from './test-modes' +import { MissingTrailingSlashError, parseUrl } from '@hedgedoc/commons' import { Optional } from '@mrdrogdrog/optional' /** @@ -16,27 +17,22 @@ export class BaseUrlFromEnvExtractor { private logger = new Logger('Base URL Configuration') private extractUrlFromEnvVar(envVarName: string, envVarValue: string | undefined): Optional { - 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 - } - }) + try { + return parseUrl(envVarValue) + } catch (error) { + if (error instanceof MissingTrailingSlashError) { + this.logger.error(`The path in ${envVarName} must end with an '/'`) + return Optional.empty() + } else { + throw error + } + } } private extractEditorBaseUrlFromEnv(): Optional { - const envValue = this.extractUrlFromEnvVar('HD_EDITOR_BASE_URL', process.env.HD_EDITOR_BASE_URL) + const envValue = this.extractUrlFromEnvVar('HD_BASE_URL', process.env.HD_BASE_URL) if (envValue.isEmpty()) { - this.logger.error("HD_EDITOR_BASE_URL isn't a valid URL!") + this.logger.error("HD_BASE_URL isn't a valid URL!") } return envValue }