fix(config): Replace HD_DOMAIN and HD_EDITOR_BASE_URL with HD_BASE_URL

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-02-05 09:31:33 +01:00
parent 65ac00913b
commit 5e1fdbe81d
21 changed files with 255 additions and 92 deletions

View file

@ -73,7 +73,7 @@ jobs:
- name: Patch base URL - name: Patch base URL
if: needs.changes.outputs.changed == 'true' 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 - name: Build app
if: needs.changes.outputs.changed == 'true' if: needs.changes.outputs.changed == 'true'

View file

@ -96,7 +96,7 @@ jobs:
- name: Patch base URL - name: Patch base URL
if: needs.changes.outputs.changed == 'true' 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 - name: Build app
if: needs.changes.outputs.changed == 'true' if: needs.changes.outputs.changed == 'true'

View file

@ -2,7 +2,7 @@
# #
# SPDX-License-Identifier: CC0-1.0 # SPDX-License-Identifier: CC0-1.0
HD_DOMAIN="http://localhost" HD_BASE_URL="http://localhost/"
HD_MEDIA_BACKEND="filesystem" HD_MEDIA_BACKEND="filesystem"
HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH="uploads/" HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH="uploads/"
HD_DATABASE_TYPE="sqlite" HD_DATABASE_TYPE="sqlite"

View file

@ -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. 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. See [the config docs](../../docs/content/config/index.md) for more information.
This example starts HedgeDoc on localhost, with non-persistent storage: 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 ## Build a development image

View file

@ -9,9 +9,9 @@ import appConfig from './app.config';
import { Loglevel } from './loglevel.enum'; import { Loglevel } from './loglevel.enum';
describe('appConfig', () => { describe('appConfig', () => {
const domain = 'https://example.com'; const baseUrl = 'https://example.com/';
const invalidDomain = 'localhost'; const invalidBaseUrl = 'localhost';
const rendererBaseUrl = 'https://render.example.com'; const rendererBaseUrl = 'https://render.example.com/';
const port = 3333; const port = 3333;
const negativePort = -9000; const negativePort = -9000;
const floatPort = 3.14; const floatPort = 3.14;
@ -26,7 +26,7 @@ describe('appConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain, HD_BASE_URL: baseUrl,
HD_RENDERER_BASE_URL: rendererBaseUrl, HD_RENDERER_BASE_URL: rendererBaseUrl,
PORT: port.toString(), PORT: port.toString(),
HD_LOGLEVEL: loglevel, HD_LOGLEVEL: loglevel,
@ -38,7 +38,7 @@ describe('appConfig', () => {
}, },
); );
const config = appConfig(); const config = appConfig();
expect(config.domain).toEqual(domain); expect(config.baseUrl).toEqual(baseUrl);
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl); expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
expect(config.port).toEqual(port); expect(config.port).toEqual(port);
expect(config.loglevel).toEqual(loglevel); expect(config.loglevel).toEqual(loglevel);
@ -50,7 +50,7 @@ describe('appConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain, HD_BASE_URL: baseUrl,
PORT: port.toString(), PORT: port.toString(),
HD_LOGLEVEL: loglevel, HD_LOGLEVEL: loglevel,
HD_PERSIST_INTERVAL: '100', HD_PERSIST_INTERVAL: '100',
@ -61,8 +61,8 @@ describe('appConfig', () => {
}, },
); );
const config = appConfig(); const config = appConfig();
expect(config.domain).toEqual(domain); expect(config.baseUrl).toEqual(baseUrl);
expect(config.rendererBaseUrl).toEqual(domain); expect(config.rendererBaseUrl).toEqual(baseUrl);
expect(config.port).toEqual(port); expect(config.port).toEqual(port);
expect(config.loglevel).toEqual(loglevel); expect(config.loglevel).toEqual(loglevel);
expect(config.persistInterval).toEqual(100); expect(config.persistInterval).toEqual(100);
@ -73,7 +73,7 @@ describe('appConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain, HD_BASE_URL: baseUrl,
HD_RENDERER_BASE_URL: rendererBaseUrl, HD_RENDERER_BASE_URL: rendererBaseUrl,
HD_LOGLEVEL: loglevel, HD_LOGLEVEL: loglevel,
HD_PERSIST_INTERVAL: '100', HD_PERSIST_INTERVAL: '100',
@ -84,7 +84,7 @@ describe('appConfig', () => {
}, },
); );
const config = appConfig(); const config = appConfig();
expect(config.domain).toEqual(domain); expect(config.baseUrl).toEqual(baseUrl);
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl); expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
expect(config.port).toEqual(3000); expect(config.port).toEqual(3000);
expect(config.loglevel).toEqual(loglevel); expect(config.loglevel).toEqual(loglevel);
@ -96,7 +96,7 @@ describe('appConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain, HD_BASE_URL: baseUrl,
HD_RENDERER_BASE_URL: rendererBaseUrl, HD_RENDERER_BASE_URL: rendererBaseUrl,
PORT: port.toString(), PORT: port.toString(),
HD_PERSIST_INTERVAL: '100', HD_PERSIST_INTERVAL: '100',
@ -107,7 +107,7 @@ describe('appConfig', () => {
}, },
); );
const config = appConfig(); const config = appConfig();
expect(config.domain).toEqual(domain); expect(config.baseUrl).toEqual(baseUrl);
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl); expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
expect(config.port).toEqual(port); expect(config.port).toEqual(port);
expect(config.loglevel).toEqual(Loglevel.WARN); expect(config.loglevel).toEqual(Loglevel.WARN);
@ -119,7 +119,7 @@ describe('appConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain, HD_BASE_URL: baseUrl,
HD_RENDERER_BASE_URL: rendererBaseUrl, HD_RENDERER_BASE_URL: rendererBaseUrl,
HD_LOGLEVEL: loglevel, HD_LOGLEVEL: loglevel,
PORT: port.toString(), PORT: port.toString(),
@ -130,7 +130,7 @@ describe('appConfig', () => {
}, },
); );
const config = appConfig(); const config = appConfig();
expect(config.domain).toEqual(domain); expect(config.baseUrl).toEqual(baseUrl);
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl); expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
expect(config.port).toEqual(port); expect(config.port).toEqual(port);
expect(config.loglevel).toEqual(Loglevel.TRACE); expect(config.loglevel).toEqual(Loglevel.TRACE);
@ -142,7 +142,7 @@ describe('appConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain, HD_BASE_URL: baseUrl,
HD_RENDERER_BASE_URL: rendererBaseUrl, HD_RENDERER_BASE_URL: rendererBaseUrl,
HD_LOGLEVEL: loglevel, HD_LOGLEVEL: loglevel,
PORT: port.toString(), PORT: port.toString(),
@ -154,7 +154,7 @@ describe('appConfig', () => {
}, },
); );
const config = appConfig(); const config = appConfig();
expect(config.domain).toEqual(domain); expect(config.baseUrl).toEqual(baseUrl);
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl); expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
expect(config.port).toEqual(port); expect(config.port).toEqual(port);
expect(config.loglevel).toEqual(Loglevel.TRACE); expect(config.loglevel).toEqual(Loglevel.TRACE);
@ -163,11 +163,11 @@ describe('appConfig', () => {
}); });
}); });
describe('throws error', () => { 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( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: invalidDomain, HD_BASE_URL: invalidBaseUrl,
PORT: port.toString(), PORT: port.toString(),
HD_LOGLEVEL: loglevel, HD_LOGLEVEL: loglevel,
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
@ -176,7 +176,25 @@ describe('appConfig', () => {
clear: true, 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(); restore();
}); });
@ -184,7 +202,7 @@ describe('appConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain, HD_BASE_URL: baseUrl,
PORT: negativePort.toString(), PORT: negativePort.toString(),
HD_LOGLEVEL: loglevel, HD_LOGLEVEL: loglevel,
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
@ -201,7 +219,7 @@ describe('appConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain, HD_BASE_URL: baseUrl,
PORT: outOfRangePort.toString(), PORT: outOfRangePort.toString(),
HD_LOGLEVEL: loglevel, HD_LOGLEVEL: loglevel,
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
@ -220,7 +238,7 @@ describe('appConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain, HD_BASE_URL: baseUrl,
PORT: floatPort.toString(), PORT: floatPort.toString(),
HD_LOGLEVEL: loglevel, HD_LOGLEVEL: loglevel,
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
@ -237,7 +255,7 @@ describe('appConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain, HD_BASE_URL: baseUrl,
PORT: invalidPort, PORT: invalidPort,
HD_LOGLEVEL: loglevel, HD_LOGLEVEL: loglevel,
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
@ -254,7 +272,7 @@ describe('appConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain, HD_BASE_URL: baseUrl,
PORT: port.toString(), PORT: port.toString(),
HD_LOGLEVEL: invalidLoglevel, HD_LOGLEVEL: invalidLoglevel,
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
@ -271,7 +289,7 @@ describe('appConfig', () => {
const restore = mockedEnv( const restore = mockedEnv(
{ {
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain, HD_BASE_URL: baseUrl,
PORT: port.toString(), PORT: port.toString(),
HD_LOGLEVEL: invalidLoglevel, HD_LOGLEVEL: invalidLoglevel,
HD_PERSIST_INTERVAL: invalidPersistInterval.toString(), HD_PERSIST_INTERVAL: invalidPersistInterval.toString(),

View file

@ -3,31 +3,50 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import {
MissingTrailingSlashError,
parseUrl,
WrongProtocolError,
} from '@hedgedoc/commons';
import { registerAs } from '@nestjs/config'; import { registerAs } from '@nestjs/config';
import * as Joi from 'joi'; import * as Joi from 'joi';
import { CustomHelpers, ErrorReport } from 'joi';
import { Loglevel } from './loglevel.enum'; import { Loglevel } from './loglevel.enum';
import { buildErrorMessage, parseOptionalNumber } from './utils'; import { buildErrorMessage, parseOptionalNumber } from './utils';
export interface AppConfig { export interface AppConfig {
domain: string; baseUrl: string;
rendererBaseUrl: string; rendererBaseUrl: string;
port: number; port: number;
loglevel: Loglevel; loglevel: Loglevel;
persistInterval: number; 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({ const schema = Joi.object({
domain: Joi.string() baseUrl: Joi.string()
.uri({ .custom(validateUrlWithTrailingSlash)
scheme: /https?/, .label('HD_BASE_URL'),
})
.label('HD_DOMAIN'),
rendererBaseUrl: Joi.string() rendererBaseUrl: Joi.string()
.uri({ .custom(validateUrlWithTrailingSlash)
scheme: /https?/, .default(Joi.ref('baseUrl'))
})
.default(Joi.ref('domain'))
.optional() .optional()
.label('HD_RENDERER_BASE_URL'), .label('HD_RENDERER_BASE_URL'),
port: Joi.number() port: Joi.number()
@ -48,12 +67,17 @@ const schema = Joi.object({
.default(10) .default(10)
.optional() .optional()
.label('HD_PERSIST_INTERVAL'), .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', () => { export default registerAs('appConfig', () => {
const appConfig = schema.validate( const appConfig = schema.validate(
{ {
domain: process.env.HD_DOMAIN, baseUrl: process.env.HD_BASE_URL,
rendererBaseUrl: process.env.HD_RENDERER_BASE_URL, rendererBaseUrl: process.env.HD_RENDERER_BASE_URL,
port: parseOptionalNumber(process.env.PORT), port: parseOptionalNumber(process.env.PORT),
loglevel: process.env.HD_LOGLEVEL, loglevel: process.env.HD_LOGLEVEL,

View file

@ -11,7 +11,7 @@ import { Loglevel } from '../loglevel.enum';
export function createDefaultMockAppConfig(): AppConfig { export function createDefaultMockAppConfig(): AppConfig {
return { return {
domain: 'md.example.com', baseUrl: 'md.example.com',
rendererBaseUrl: 'md-renderer.example.com', rendererBaseUrl: 'md-renderer.example.com',
port: 3000, port: 3000,
loglevel: Loglevel.ERROR, loglevel: Loglevel.ERROR,

View file

@ -167,7 +167,7 @@ describe('FrontendConfigService', () => {
]) { ]) {
it(`works with ${JSON.stringify(authConfigConfigured)}`, async () => { it(`works with ${JSON.stringify(authConfigConfigured)}`, async () => {
const appConfig: AppConfig = { const appConfig: AppConfig = {
domain: domain, baseUrl: domain,
rendererBaseUrl: 'https://renderer.example.org', rendererBaseUrl: 'https://renderer.example.org',
port: 3000, port: 3000,
loglevel: Loglevel.ERROR, loglevel: Loglevel.ERROR,
@ -325,7 +325,7 @@ describe('FrontendConfigService', () => {
]) { ]) {
it(`combination #${index} works`, async () => { it(`combination #${index} works`, async () => {
const appConfig: AppConfig = { const appConfig: AppConfig = {
domain: domain, baseUrl: domain,
rendererBaseUrl: 'https://renderer.example.org', rendererBaseUrl: 'https://renderer.example.org',
port: 3000, port: 3000,
loglevel: Loglevel.ERROR, loglevel: Loglevel.ERROR,

View file

@ -18,7 +18,7 @@ describe('App', () => {
}) })
.overrideProvider(getConfigToken('appConfig')) .overrideProvider(getConfigToken('appConfig'))
.useValue({ .useValue({
domain: 'localhost', baseUrl: 'localhost',
port: 3333, port: 3333,
}) })
.overrideProvider(getConfigToken('mediaConfig')) .overrideProvider(getConfigToken('mediaConfig'))

View file

@ -24,6 +24,12 @@ export { encodeServerVersionUpdatedMessage } from './messages/server-version-upd
export { WebsocketTransporter } from './websocket-transporter.js' 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 type { MessageTransporterEvents } from './y-doc-message-transporter.js'
export { waitForOtherPromisesToFinish } from './utils/wait-for-other-promises-to-finish.js' export { waitForOtherPromisesToFinish } from './utils/wait-for-other-promises-to-finish.js'

View file

@ -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')
}
}

View file

@ -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
)
})
})
})

View file

@ -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<URL> {
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<URL> {
try {
return Optional.ofNullable(url).map((value) => new URL(value))
} catch (error) {
return Optional.empty()
}
}

View file

@ -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: 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 ```ini
HD_DOMAIN="http://localhost" HD_BASE_URL="http://localhost"
HD_MEDIA_BACKEND="filesystem" HD_MEDIA_BACKEND="filesystem"
HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH="uploads/" HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH="uploads/"
HD_DATABASE_TYPE="sqlite" HD_DATABASE_TYPE="sqlite"
@ -19,15 +19,15 @@ We also provide an `.env.example` file containing a minimal configuration in the
## General ## General
| environment variable | default | example | description | | environment variable | default | example | description |
|--------------------------|-----------|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| |--------------------------|------------------------|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| `HD_DOMAIN` | - | `https://md.example.com` | The URL the HedgeDoc instance runs on. | | `HD_BASE_URL` | - | `https://md.example.com` | The URL the HedgeDoc instance runs on. |
| `PORT` | 3000 | | The port 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_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_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_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_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. | | `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? ### Why should I want to run my renderer on a different (sub-)domain?

View file

@ -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. 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 > 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`. 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`. 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. 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 ### With local backend
To start the development mode with an actual HedgeDoc backend use `yarn run dev:with-local-backend` instead. 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 ### 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! 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 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. 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 If you use the production build then make sure that you set the environment variable `HD_BASE_URL` to the same
value as `HD_DOMAIN` in the backend. value as `HD_BASE_URL` in the backend.
### Production mock build ### Production mock build

View file

@ -11,8 +11,8 @@ The following environment variables are recognized by the frontend process.
| Name | Possible Values | Description | | 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_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_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_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. | | NEXT_PUBLIC_TEST_MODE | `true`, `false` | Will activate additional HTML attributes that are used to identify elements for test suits. |

View file

@ -1,3 +1,3 @@
NEXT_PUBLIC_USE_MOCK_API=true 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/" HD_RENDERER_BASE_URL="http://127.0.0.1:3001/"

View file

@ -1,3 +1,3 @@
NEXT_PUBLIC_USE_MOCK_API=true 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/" HD_RENDERER_BASE_URL="http://127.0.0.1:3001/"

View file

@ -10,7 +10,7 @@
"analyze": "cross-env ANALYZE=true yarn build", "analyze": "cross-env ANALYZE=true yarn build",
"dev": "cross-env PORT=3001 next dev", "dev": "cross-env PORT=3001 next dev",
"dev:test": "cross-env PORT=3001 NODE_ENV=test NEXT_PUBLIC_TEST_MODE=true 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": "prettier -c \"src/**/*.{ts,tsx,js}\" \"cypress/**/*.{ts,tsx}\"",
"format:fix": "prettier -w \"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", "lint": "eslint --max-warnings=0 --ext .ts,.tsx src",

View file

@ -7,7 +7,7 @@ import { BaseUrlFromEnvExtractor } from './base-url-from-env-extractor'
describe('BaseUrlFromEnvExtractor', () => { describe('BaseUrlFromEnvExtractor', () => {
it('should return the base urls if both are valid urls', () => { 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/' process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/'
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
const result = baseUrlFromEnvExtractor.extractBaseUrls() const result = baseUrlFromEnvExtractor.extractBaseUrls()
@ -19,42 +19,52 @@ describe('BaseUrlFromEnvExtractor', () => {
}) })
it('should return an empty optional if no var is set', () => { 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 process.env.HD_RENDERER_BASE_URL = undefined
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy() expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy()
}) })
it("should return an empty optional if editor base url isn't an URL", () => { 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/' process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/'
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy() expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy()
}) })
it("should return an empty optional if renderer base url isn't an URL", () => { 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' process.env.HD_RENDERER_BASE_URL = 'bibedibabedibu'
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy() expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy()
}) })
it("should return an empty optional if editor base url isn't ending with a slash", () => { it("should return an optional if editor base url isn't ending with a slash", () => {
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/' process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/'
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() 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", () => { it("should return an optional if renderer base url isn't ending with a slash", () => {
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' process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org'
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() 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', () => { 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 delete process.env.HD_RENDERER_BASE_URL
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
const result = baseUrlFromEnvExtractor.extractBaseUrls() const result = baseUrlFromEnvExtractor.extractBaseUrls()

View file

@ -6,6 +6,7 @@
import type { BaseUrls } from '../components/common/base-url/base-url-context-provider' import type { BaseUrls } from '../components/common/base-url/base-url-context-provider'
import { Logger } from './logger' import { Logger } from './logger'
import { isTestMode } from './test-modes' import { isTestMode } from './test-modes'
import { MissingTrailingSlashError, parseUrl } from '@hedgedoc/commons'
import { Optional } from '@mrdrogdrog/optional' import { Optional } from '@mrdrogdrog/optional'
/** /**
@ -16,27 +17,22 @@ export class BaseUrlFromEnvExtractor {
private logger = new Logger('Base URL Configuration') private logger = new Logger('Base URL Configuration')
private extractUrlFromEnvVar(envVarName: string, envVarValue: string | undefined): Optional<URL> { private extractUrlFromEnvVar(envVarName: string, envVarValue: string | undefined): Optional<URL> {
return Optional.ofNullable(envVarValue) try {
.filter((value) => { return parseUrl(envVarValue)
const endsWithSlash = value.endsWith('/') } catch (error) {
if (!endsWithSlash) { if (error instanceof MissingTrailingSlashError) {
this.logger.error(`${envVarName} must end with an '/'`) this.logger.error(`The path in ${envVarName} must end with an '/'`)
} return Optional.empty()
return endsWithSlash } else {
}) throw error
.map((value) => { }
try { }
return new URL(value)
} catch (error) {
return null
}
})
} }
private extractEditorBaseUrlFromEnv(): Optional<URL> { private extractEditorBaseUrlFromEnv(): Optional<URL> {
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()) { 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 return envValue
} }