mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-12-22 15:21:34 +00:00
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:
parent
65ac00913b
commit
5e1fdbe81d
21 changed files with 255 additions and 92 deletions
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -18,7 +18,7 @@ describe('App', () => {
|
|||
})
|
||||
.overrideProvider(getConfigToken('appConfig'))
|
||||
.useValue({
|
||||
domain: 'localhost',
|
||||
baseUrl: 'localhost',
|
||||
port: 3333,
|
||||
})
|
||||
.overrideProvider(getConfigToken('mediaConfig'))
|
||||
|
|
|
@ -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'
|
||||
|
|
17
commons/src/utils/errors.ts
Normal file
17
commons/src/utils/errors.ts
Normal 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')
|
||||
}
|
||||
}
|
57
commons/src/utils/parse-url.test.ts
Normal file
57
commons/src/utils/parse-url.test.ts
Normal 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
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
35
commons/src/utils/parse-url.ts
Normal file
35
commons/src/utils/parse-url.ts
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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. |
|
||||
|
||||
|
|
|
@ -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/"
|
||||
|
|
|
@ -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/"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<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
|
||||
}
|
||||
})
|
||||
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<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()) {
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue