From dccd58f0c15f53abad637ca456a36e1652763b19 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Sat, 12 Aug 2023 20:07:38 +0200 Subject: [PATCH] fix: remove subpath support for HD_BASE_URL With this commit we drop the subpath support which results in the constraint that HedgeDoc must always run on the root of a domain. This makes a lot of things in testing, rendering and security much easier. Signed-off-by: Tilman Vatteroth --- backend/src/config/app.config.spec.ts | 6 ++--- backend/src/config/app.config.ts | 16 ++++++------- commons/src/parse-url/errors.ts | 4 ++-- commons/src/parse-url/index.ts | 2 +- commons/src/parse-url/parse-url.spec.ts | 14 +++++------ commons/src/parse-url/parse-url.ts | 8 +++---- frontend/cypress/e2e/fileUpload.spec.ts | 4 ++-- frontend/cypress/e2e/history.spec.ts | 10 ++++---- frontend/cypress/e2e/intro.spec.ts | 4 ++-- frontend/cypress/e2e/motd.spec.ts | 4 ++-- frontend/cypress/e2e/profile.spec.ts | 6 ++--- frontend/cypress/support/config.ts | 8 +++---- frontend/netlify/intro.md | 2 +- frontend/public/public/intro.md | 2 +- .../api-request-builder.ts | 2 +- .../delete-api-request-builder.spec.ts | 24 +++++++++---------- .../get-api-request-builder.spec.ts | 20 ++++++++-------- .../post-api-request-builder.spec.ts | 24 +++++++++---------- .../put-api-request-builder.spec.ts | 24 +++++++++---------- .../motd-modal/fetch-motd.spec.ts | 2 +- .../global-dialogs/motd-modal/fetch-motd.ts | 2 +- .../src/components/intro-page/requests.ts | 2 +- frontend/src/components/layout/base-head.tsx | 3 --- frontend/src/components/layout/fav-icon.tsx | 14 +++++------ frontend/src/pages/api/private/config.ts | 2 +- frontend/src/pages/api/private/me/index.ts | 2 +- frontend/src/pages/api/private/media.ts | 2 +- .../pages/api/private/notes/features/index.ts | 2 +- frontend/src/pages/api/private/users/erik.ts | 2 +- frontend/src/pages/api/private/users/molly.ts | 2 +- .../src/pages/api/private/users/tilman.ts | 2 +- .../src/utils/base-url-from-env-extractor.ts | 6 ++--- 32 files changed, 111 insertions(+), 116 deletions(-) diff --git a/backend/src/config/app.config.spec.ts b/backend/src/config/app.config.spec.ts index ff70781f7..7c5c79207 100644 --- a/backend/src/config/app.config.spec.ts +++ b/backend/src/config/app.config.spec.ts @@ -180,11 +180,11 @@ describe('appConfig', () => { restore(); }); - it('when given a base url with path but no trailing slash in HD_BASE_URL', async () => { + it('when given a base url with subdirectory in HD_BASE_URL', async () => { const restore = mockedEnv( { /* eslint-disable @typescript-eslint/naming-convention */ - HD_BASE_URL: 'https://example.org/a', + HD_BASE_URL: 'https://example.org/subdirectory/', HD_LOGLEVEL: loglevel, /* eslint-enable @typescript-eslint/naming-convention */ }, @@ -193,7 +193,7 @@ describe('appConfig', () => { }, ); expect(() => appConfig()).toThrow( - '"HD_BASE_URL" must end with a trailing slash', + '"HD_BASE_URL" must not contain a subdirectory', ); restore(); }); diff --git a/backend/src/config/app.config.ts b/backend/src/config/app.config.ts index 961eec60a..1f4c6e5b9 100644 --- a/backend/src/config/app.config.ts +++ b/backend/src/config/app.config.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { - MissingTrailingSlashError, + NoSubdirectoryAllowedError, parseUrl, WrongProtocolError, } from '@hedgedoc/commons'; @@ -23,15 +23,15 @@ export interface AppConfig { persistInterval: number; } -function validateUrlWithTrailingSlash( +function validateUrl( 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'); + if (error instanceof NoSubdirectoryAllowedError) { + return helpers.error('url.noSubDirectoryAllowed'); } else if (error instanceof WrongProtocolError) { return helpers.error('url.wrongProtocol'); } else { @@ -41,11 +41,9 @@ function validateUrlWithTrailingSlash( } const schema = Joi.object({ - baseUrl: Joi.string() - .custom(validateUrlWithTrailingSlash) - .label('HD_BASE_URL'), + baseUrl: Joi.string().custom(validateUrl).label('HD_BASE_URL'), rendererBaseUrl: Joi.string() - .custom(validateUrlWithTrailingSlash) + .custom(validateUrl) .default(Joi.ref('baseUrl')) .optional() .label('HD_RENDERER_BASE_URL'), @@ -69,7 +67,7 @@ const schema = Joi.object({ .label('HD_PERSIST_INTERVAL'), }).messages({ // eslint-disable-next-line @typescript-eslint/naming-convention - 'url.missingTrailingSlash': '{{#label}} must end with a trailing slash', + 'url.noSubDirectoryAllowed': '{{#label}} must not contain a subdirectory', // eslint-disable-next-line @typescript-eslint/naming-convention 'url.wrongProtocol': '{{#label}} protocol must be HTTP or HTTPS', }); diff --git a/commons/src/parse-url/errors.ts b/commons/src/parse-url/errors.ts index 48632b00e..3a8079bde 100644 --- a/commons/src/parse-url/errors.ts +++ b/commons/src/parse-url/errors.ts @@ -4,9 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export class MissingTrailingSlashError extends Error { +export class NoSubdirectoryAllowedError extends Error { constructor() { - super("Path doesn't end with a trailing slash") + super('Subdirectories are not allowed') } } diff --git a/commons/src/parse-url/index.ts b/commons/src/parse-url/index.ts index 84629aa6b..bf1668895 100644 --- a/commons/src/parse-url/index.ts +++ b/commons/src/parse-url/index.ts @@ -5,4 +5,4 @@ */ export { parseUrl } from './parse-url.js' -export { MissingTrailingSlashError, WrongProtocolError } from './errors.js' +export { NoSubdirectoryAllowedError, WrongProtocolError } from './errors.js' diff --git a/commons/src/parse-url/parse-url.spec.ts b/commons/src/parse-url/parse-url.spec.ts index 4f412340a..28a69f209 100644 --- a/commons/src/parse-url/parse-url.spec.ts +++ b/commons/src/parse-url/parse-url.spec.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { MissingTrailingSlashError, WrongProtocolError } from './errors.js' +import { NoSubdirectoryAllowedError, WrongProtocolError } from './errors.js' import { parseUrl } from './parse-url.js' import { describe, expect, it } from '@jest/globals' @@ -44,14 +44,14 @@ describe('validate url', () => { '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('declines urls with with subpath and trailing slash', () => { + expect(() => + parseUrl('http://example.org/asd/').get().toString() + ).toThrow(NoSubdirectoryAllowedError) }) - it("doesn't accept urls with with subpath and without trailing slash", () => { + it('declines urls with with subpath and without trailing slash', () => { expect(() => parseUrl('http://example.org/asd').get().toString()).toThrow( - MissingTrailingSlashError + NoSubdirectoryAllowedError ) }) }) diff --git a/commons/src/parse-url/parse-url.ts b/commons/src/parse-url/parse-url.ts index 81d9c428e..3d6e9b0db 100644 --- a/commons/src/parse-url/parse-url.ts +++ b/commons/src/parse-url/parse-url.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { MissingTrailingSlashError, WrongProtocolError } from './errors.js' +import { NoSubdirectoryAllowedError, WrongProtocolError } from './errors.js' import { Optional } from '@mrdrogdrog/optional' /** @@ -12,7 +12,7 @@ import { Optional } from '@mrdrogdrog/optional' * @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 + * @throws NoSubdirectoryAllowedError if the URL has a path that doesn't end with a trailing slash */ export function parseUrl(url: string | undefined): Optional { return createOptionalUrl(url) @@ -21,8 +21,8 @@ export function parseUrl(url: string | undefined): Optional { () => new WrongProtocolError() ) .guard( - (value) => value.pathname.endsWith('/'), - () => new MissingTrailingSlashError() + (value) => value.pathname === '/', + () => new NoSubdirectoryAllowedError() ) } diff --git a/frontend/cypress/e2e/fileUpload.spec.ts b/frontend/cypress/e2e/fileUpload.spec.ts index 9dfb469bc..978d6f31e 100644 --- a/frontend/cypress/e2e/fileUpload.spec.ts +++ b/frontend/cypress/e2e/fileUpload.spec.ts @@ -17,7 +17,7 @@ describe('File upload', () => { cy.intercept( { method: 'POST', - url: 'api/private/media' + url: '/api/private/media' }, { statusCode: 201, @@ -74,7 +74,7 @@ describe('File upload', () => { cy.intercept( { method: 'POST', - url: 'api/private/media' + url: '/api/private/media' }, { statusCode: 400 diff --git a/frontend/cypress/e2e/history.spec.ts b/frontend/cypress/e2e/history.spec.ts index 96e343655..1afda8a82 100644 --- a/frontend/cypress/e2e/history.spec.ts +++ b/frontend/cypress/e2e/history.spec.ts @@ -25,7 +25,7 @@ describe('History', () => { describe('is as given when not empty', () => { beforeEach(() => { cy.clearLocalStorage('history') - cy.intercept('GET', 'api/private/me/history', { + cy.intercept('GET', '/api/private/me/history', { body: [ { identifier: 'cypress', @@ -52,7 +52,7 @@ describe('History', () => { describe('is untitled when not empty', () => { beforeEach(() => { cy.clearLocalStorage('history') - cy.intercept('GET', 'api/private/me/history', { + cy.intercept('GET', '/api/private/me/history', { body: [ { identifier: 'cypress-no-title', @@ -85,7 +85,7 @@ describe('History', () => { describe('working', () => { beforeEach(() => { - cy.intercept('PUT', 'api/private/me/history/features', (req) => { + cy.intercept('PUT', '/api/private/me/history/features', (req) => { req.reply(200, req.body) }) }) @@ -115,7 +115,7 @@ describe('History', () => { describe('failing', () => { beforeEach(() => { - cy.intercept('PUT', 'api/private/me/history/features', { + cy.intercept('PUT', '/api/private/me/history/features', { statusCode: 401 }) }) @@ -143,7 +143,7 @@ describe('History', () => { describe('Import', () => { beforeEach(() => { cy.clearLocalStorage('history') - cy.intercept('GET', 'api/private/me/history', { + cy.intercept('GET', '/api/private/me/history', { body: [] }) cy.visitHistory() diff --git a/frontend/cypress/e2e/intro.spec.ts b/frontend/cypress/e2e/intro.spec.ts index c63905845..993bbd289 100644 --- a/frontend/cypress/e2e/intro.spec.ts +++ b/frontend/cypress/e2e/intro.spec.ts @@ -7,7 +7,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ describe('Intro page', () => { beforeEach(() => { - cy.intercept('public/intro.md', 'test content') + cy.intercept('/public/intro.md', 'test content') cy.visitHome() }) @@ -17,7 +17,7 @@ describe('Intro page', () => { }) it("won't show anything if no content was found", () => { - cy.intercept('public/intro.md', { + cy.intercept('/public/intro.md', { statusCode: 404 }) cy.visitHome() diff --git a/frontend/cypress/e2e/motd.spec.ts b/frontend/cypress/e2e/motd.spec.ts index 5ae507ba4..6cf7dfd33 100644 --- a/frontend/cypress/e2e/motd.spec.ts +++ b/frontend/cypress/e2e/motd.spec.ts @@ -12,13 +12,13 @@ const motdMockHtml = 'This is the mock Motd call' describe('Motd', () => { it("shows, dismisses and won't show again a motd modal", () => { localStorage.removeItem(MOTD_LOCAL_STORAGE_KEY) - cy.intercept('GET', 'public/motd.md', { + cy.intercept('GET', '/public/motd.md', { statusCode: 200, headers: { 'Last-Modified': MOCK_LAST_MODIFIED }, body: motdMockContent }) - cy.intercept('HEAD', 'public/motd.md', { + cy.intercept('HEAD', '/public/motd.md', { statusCode: 200, headers: { 'Last-Modified': MOCK_LAST_MODIFIED } }) diff --git a/frontend/cypress/e2e/profile.spec.ts b/frontend/cypress/e2e/profile.spec.ts index 644da1d3f..0369f69de 100644 --- a/frontend/cypress/e2e/profile.spec.ts +++ b/frontend/cypress/e2e/profile.spec.ts @@ -8,7 +8,7 @@ describe('profile page', () => { beforeEach(() => { cy.intercept( { - url: 'api/private/tokens', + url: '/api/private/tokens', method: 'GET' }, { @@ -25,7 +25,7 @@ describe('profile page', () => { ) cy.intercept( { - url: 'api/private/tokens', + url: '/api/private/tokens', method: 'POST' }, { @@ -42,7 +42,7 @@ describe('profile page', () => { ) cy.intercept( { - url: 'api/private/tokens/cypress', + url: '/api/private/tokens/cypress', method: 'DELETE' }, { diff --git a/frontend/cypress/support/config.ts b/frontend/cypress/support/config.ts index e978c9136..b9647c834 100644 --- a/frontend/cypress/support/config.ts +++ b/frontend/cypress/support/config.ts @@ -13,7 +13,7 @@ declare namespace Cypress { export const branding = { name: 'DEMO Corp', - logo: 'public/img/demo.png' + logo: '/public/img/demo.png' } export const authProviders = [ @@ -80,7 +80,7 @@ export const config = { } Cypress.Commands.add('loadConfig', (additionalConfig?: Partial) => { - return cy.intercept('api/private/config', { + return cy.intercept('/api/private/config', { statusCode: 200, body: { ...config, @@ -92,11 +92,11 @@ Cypress.Commands.add('loadConfig', (additionalConfig?: Partial) = beforeEach(() => { cy.loadConfig() - cy.intercept('GET', 'public/motd.md', { + cy.intercept('GET', '/public/motd.md', { body: '404 Not Found!', statusCode: 404 }) - cy.intercept('HEAD', 'public/motd.md', { + cy.intercept('HEAD', '/public/motd.md', { statusCode: 404 }) }) diff --git a/frontend/netlify/intro.md b/frontend/netlify/intro.md index 546a7b80e..3c81547f5 100644 --- a/frontend/netlify/intro.md +++ b/frontend/netlify/intro.md @@ -2,6 +2,6 @@ What you see is an UI-Test! It's filled with dummy data, not connected to a backend and no data will be saved. ::: -![HedgeDoc Screenshot](public/screenshot.png) +![HedgeDoc Screenshot](/public/screenshot.png) [![Deployed using netlify](https://www.netlify.com/img/global/badges/netlify-color-accent.svg)](https://www.netlify.com) diff --git a/frontend/public/public/intro.md b/frontend/public/public/intro.md index 5455fab79..ce1fc5fd2 100644 --- a/frontend/public/public/intro.md +++ b/frontend/public/public/intro.md @@ -2,4 +2,4 @@ What you see is an UI-Test! It's filled with dummy data, not connected to a backend and no data will be saved. ::: -![HedgeDoc Screenshot](public/screenshot.png) +![HedgeDoc Screenshot](/public/screenshot.png) diff --git a/frontend/src/api/common/api-request-builder/api-request-builder.ts b/frontend/src/api/common/api-request-builder/api-request-builder.ts index 920e24d1a..7c57242cb 100644 --- a/frontend/src/api/common/api-request-builder/api-request-builder.ts +++ b/frontend/src/api/common/api-request-builder/api-request-builder.ts @@ -27,7 +27,7 @@ export abstract class ApiRequestBuilder { * @param baseUrl An optional base URL that is used for the endpoint */ constructor(endpoint: string, baseUrl?: string) { - this.targetUrl = `${baseUrl ?? ''}api/private/${endpoint}` + this.targetUrl = `${baseUrl ?? '/'}api/private/${endpoint}` } protected async sendRequestAndVerifyResponse(httpMethod: RequestInit['method']): Promise> { diff --git a/frontend/src/api/common/api-request-builder/delete-api-request-builder.spec.ts b/frontend/src/api/common/api-request-builder/delete-api-request-builder.spec.ts index a98623ece..4d46143a6 100644 --- a/frontend/src/api/common/api-request-builder/delete-api-request-builder.spec.ts +++ b/frontend/src/api/common/api-request-builder/delete-api-request-builder.spec.ts @@ -26,14 +26,14 @@ describe('DeleteApiRequestBuilder', () => { describe('sendRequest without body', () => { it('without headers', async () => { - expectFetch('api/private/test', 204, { method: 'DELETE' }) + expectFetch('/api/private/test', 204, { method: 'DELETE' }) await new DeleteApiRequestBuilder('test').sendRequest() }) it('with single header', async () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'true') - expectFetch('api/private/test', 204, { + expectFetch('/api/private/test', 204, { method: 'DELETE', headers: expectedHeaders }) @@ -43,7 +43,7 @@ describe('DeleteApiRequestBuilder', () => { it('with overriding single header', async () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'false') - expectFetch('api/private/test', 204, { + expectFetch('/api/private/test', 204, { method: 'DELETE', headers: expectedHeaders }) @@ -57,7 +57,7 @@ describe('DeleteApiRequestBuilder', () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'true') expectedHeaders.append('test2', 'false') - expectFetch('api/private/test', 204, { + expectFetch('/api/private/test', 204, { method: 'DELETE', headers: expectedHeaders }) @@ -72,7 +72,7 @@ describe('DeleteApiRequestBuilder', () => { const expectedHeaders = new Headers() expectedHeaders.append('Content-Type', 'application/json') - expectFetch('api/private/test', 204, { + expectFetch('/api/private/test', 204, { method: 'DELETE', headers: expectedHeaders, body: '{"test":true,"foo":"bar"}' @@ -86,7 +86,7 @@ describe('DeleteApiRequestBuilder', () => { }) it('sendRequest with other body', async () => { - expectFetch('api/private/test', 204, { + expectFetch('/api/private/test', 204, { method: 'DELETE', body: 'HedgeDoc' }) @@ -95,7 +95,7 @@ describe('DeleteApiRequestBuilder', () => { describe('sendRequest with custom options', () => { it('with one option', async () => { - expectFetch('api/private/test', 204, { + expectFetch('/api/private/test', 204, { method: 'DELETE', cache: 'force-cache' }) @@ -107,7 +107,7 @@ describe('DeleteApiRequestBuilder', () => { }) it('overriding single option', async () => { - expectFetch('api/private/test', 204, { + expectFetch('/api/private/test', 204, { method: 'DELETE', cache: 'no-store' }) @@ -122,7 +122,7 @@ describe('DeleteApiRequestBuilder', () => { }) it('with multiple options', async () => { - expectFetch('api/private/test', 204, { + expectFetch('/api/private/test', 204, { method: 'DELETE', cache: 'force-cache', integrity: 'test' @@ -138,13 +138,13 @@ describe('DeleteApiRequestBuilder', () => { describe('failing sendRequest', () => { it('without backend provided error name or error message', async () => { - expectFetch('api/private/test', 400, { method: 'DELETE' }) + expectFetch('/api/private/test', 400, { method: 'DELETE' }) const request = new DeleteApiRequestBuilder('test').sendRequest() await expect(request).rejects.toEqual(new ApiError(400, undefined, undefined)) }) it('with backend error name and error message', async () => { - expectFetch('api/private/test', 400, { method: 'DELETE' }, { + expectFetch('/api/private/test', 400, { method: 'DELETE' }, { message: 'The API has exploded!', name: 'testExplosion' } as ApiErrorResponse) @@ -153,7 +153,7 @@ describe('DeleteApiRequestBuilder', () => { }) it('with another status code than 400', async () => { - expectFetch('api/private/test', 401, { method: 'DELETE' }, { + expectFetch('/api/private/test', 401, { method: 'DELETE' }, { message: 'The API has exploded!', name: 'testExplosion' } as ApiErrorResponse) diff --git a/frontend/src/api/common/api-request-builder/get-api-request-builder.spec.ts b/frontend/src/api/common/api-request-builder/get-api-request-builder.spec.ts index 5e08fb151..ca7576df7 100644 --- a/frontend/src/api/common/api-request-builder/get-api-request-builder.spec.ts +++ b/frontend/src/api/common/api-request-builder/get-api-request-builder.spec.ts @@ -26,14 +26,14 @@ describe('GetApiRequestBuilder', () => { describe('sendRequest', () => { it('without headers', async () => { - expectFetch('api/private/test', 200, { method: 'GET' }) + expectFetch('/api/private/test', 200, { method: 'GET' }) await new GetApiRequestBuilder('test').sendRequest() }) it('with single header', async () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'true') - expectFetch('api/private/test', 200, { + expectFetch('/api/private/test', 200, { method: 'GET', headers: expectedHeaders }) @@ -43,7 +43,7 @@ describe('GetApiRequestBuilder', () => { it('with overriding single header', async () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'false') - expectFetch('api/private/test', 200, { + expectFetch('/api/private/test', 200, { method: 'GET', headers: expectedHeaders }) @@ -57,7 +57,7 @@ describe('GetApiRequestBuilder', () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'true') expectedHeaders.append('test2', 'false') - expectFetch('api/private/test', 200, { + expectFetch('/api/private/test', 200, { method: 'GET', headers: expectedHeaders }) @@ -70,7 +70,7 @@ describe('GetApiRequestBuilder', () => { describe('sendRequest with custom options', () => { it('with one option', async () => { - expectFetch('api/private/test', 200, { + expectFetch('/api/private/test', 200, { method: 'GET', cache: 'force-cache' }) @@ -82,7 +82,7 @@ describe('GetApiRequestBuilder', () => { }) it('overriding single option', async () => { - expectFetch('api/private/test', 200, { + expectFetch('/api/private/test', 200, { method: 'GET', cache: 'no-store' }) @@ -97,7 +97,7 @@ describe('GetApiRequestBuilder', () => { }) it('with multiple options', async () => { - expectFetch('api/private/test', 200, { + expectFetch('/api/private/test', 200, { method: 'GET', cache: 'force-cache', integrity: 'test' @@ -113,13 +113,13 @@ describe('GetApiRequestBuilder', () => { describe('failing sendRequest', () => { it('without backend provided error name or error message', async () => { - expectFetch('api/private/test', 400, { method: 'GET' }) + expectFetch('/api/private/test', 400, { method: 'GET' }) const request = new GetApiRequestBuilder('test').sendRequest() await expect(request).rejects.toEqual(new ApiError(400, undefined, undefined)) }) it('with backend error name and error message', async () => { - expectFetch('api/private/test', 400, { method: 'GET' }, { + expectFetch('/api/private/test', 400, { method: 'GET' }, { message: 'The API has exploded!', name: 'testExplosion' } as ApiErrorResponse) @@ -128,7 +128,7 @@ describe('GetApiRequestBuilder', () => { }) it('with another status code than 400', async () => { - expectFetch('api/private/test', 401, { method: 'GET' }, { + expectFetch('/api/private/test', 401, { method: 'GET' }, { message: 'The API has exploded!', name: 'testExplosion' } as ApiErrorResponse) diff --git a/frontend/src/api/common/api-request-builder/post-api-request-builder.spec.ts b/frontend/src/api/common/api-request-builder/post-api-request-builder.spec.ts index f370b97dd..11ec1856d 100644 --- a/frontend/src/api/common/api-request-builder/post-api-request-builder.spec.ts +++ b/frontend/src/api/common/api-request-builder/post-api-request-builder.spec.ts @@ -26,14 +26,14 @@ describe('PostApiRequestBuilder', () => { describe('sendRequest without body', () => { it('without headers', async () => { - expectFetch('api/private/test', 201, { method: 'POST' }) + expectFetch('/api/private/test', 201, { method: 'POST' }) await new PostApiRequestBuilder('test').sendRequest() }) it('with single header', async () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'true') - expectFetch('api/private/test', 201, { + expectFetch('/api/private/test', 201, { method: 'POST', headers: expectedHeaders }) @@ -43,7 +43,7 @@ describe('PostApiRequestBuilder', () => { it('with overriding single header', async () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'false') - expectFetch('api/private/test', 201, { + expectFetch('/api/private/test', 201, { method: 'POST', headers: expectedHeaders }) @@ -57,7 +57,7 @@ describe('PostApiRequestBuilder', () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'true') expectedHeaders.append('test2', 'false') - expectFetch('api/private/test', 201, { + expectFetch('/api/private/test', 201, { method: 'POST', headers: expectedHeaders }) @@ -72,7 +72,7 @@ describe('PostApiRequestBuilder', () => { const expectedHeaders = new Headers() expectedHeaders.append('Content-Type', 'application/json') - expectFetch('api/private/test', 201, { + expectFetch('/api/private/test', 201, { method: 'POST', headers: expectedHeaders, body: '{"test":true,"foo":"bar"}' @@ -86,7 +86,7 @@ describe('PostApiRequestBuilder', () => { }) it('sendRequest with other body', async () => { - expectFetch('api/private/test', 201, { + expectFetch('/api/private/test', 201, { method: 'POST', body: 'HedgeDoc' }) @@ -95,7 +95,7 @@ describe('PostApiRequestBuilder', () => { describe('sendRequest with custom options', () => { it('with one option', async () => { - expectFetch('api/private/test', 201, { + expectFetch('/api/private/test', 201, { method: 'POST', cache: 'force-cache' }) @@ -107,7 +107,7 @@ describe('PostApiRequestBuilder', () => { }) it('overriding single option', async () => { - expectFetch('api/private/test', 201, { + expectFetch('/api/private/test', 201, { method: 'POST', cache: 'no-store' }) @@ -122,7 +122,7 @@ describe('PostApiRequestBuilder', () => { }) it('with multiple options', async () => { - expectFetch('api/private/test', 201, { + expectFetch('/api/private/test', 201, { method: 'POST', cache: 'force-cache', integrity: 'test' @@ -138,13 +138,13 @@ describe('PostApiRequestBuilder', () => { describe('failing sendRequest', () => { it('without backend provided error name or error message', async () => { - expectFetch('api/private/test', 400, { method: 'POST' }) + expectFetch('/api/private/test', 400, { method: 'POST' }) const request = new PostApiRequestBuilder('test').sendRequest() await expect(request).rejects.toEqual(new ApiError(400, undefined, undefined)) }) it('with backend error name and error message', async () => { - expectFetch('api/private/test', 400, { method: 'POST' }, { + expectFetch('/api/private/test', 400, { method: 'POST' }, { message: 'The API has exploded!', name: 'testExplosion' } as ApiErrorResponse) @@ -153,7 +153,7 @@ describe('PostApiRequestBuilder', () => { }) it('with another status code than 400', async () => { - expectFetch('api/private/test', 401, { method: 'POST' }, { + expectFetch('/api/private/test', 401, { method: 'POST' }, { message: 'The API has exploded!', name: 'testExplosion' } as ApiErrorResponse) diff --git a/frontend/src/api/common/api-request-builder/put-api-request-builder.spec.ts b/frontend/src/api/common/api-request-builder/put-api-request-builder.spec.ts index fc0e85d53..4c4d9b8d6 100644 --- a/frontend/src/api/common/api-request-builder/put-api-request-builder.spec.ts +++ b/frontend/src/api/common/api-request-builder/put-api-request-builder.spec.ts @@ -26,14 +26,14 @@ describe('PutApiRequestBuilder', () => { describe('sendRequest without body', () => { it('without headers', async () => { - expectFetch('api/private/test', 200, { method: 'PUT' }) + expectFetch('/api/private/test', 200, { method: 'PUT' }) await new PutApiRequestBuilder('test').sendRequest() }) it('with single header', async () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'true') - expectFetch('api/private/test', 200, { + expectFetch('/api/private/test', 200, { method: 'PUT', headers: expectedHeaders }) @@ -43,7 +43,7 @@ describe('PutApiRequestBuilder', () => { it('with overriding single header', async () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'false') - expectFetch('api/private/test', 200, { + expectFetch('/api/private/test', 200, { method: 'PUT', headers: expectedHeaders }) @@ -57,7 +57,7 @@ describe('PutApiRequestBuilder', () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'true') expectedHeaders.append('test2', 'false') - expectFetch('api/private/test', 200, { + expectFetch('/api/private/test', 200, { method: 'PUT', headers: expectedHeaders }) @@ -72,7 +72,7 @@ describe('PutApiRequestBuilder', () => { const expectedHeaders = new Headers() expectedHeaders.append('Content-Type', 'application/json') - expectFetch('api/private/test', 200, { + expectFetch('/api/private/test', 200, { method: 'PUT', headers: expectedHeaders, body: '{"test":true,"foo":"bar"}' @@ -86,7 +86,7 @@ describe('PutApiRequestBuilder', () => { }) it('sendRequest with other body', async () => { - expectFetch('api/private/test', 200, { + expectFetch('/api/private/test', 200, { method: 'PUT', body: 'HedgeDoc' }) @@ -95,7 +95,7 @@ describe('PutApiRequestBuilder', () => { describe('sendRequest with custom options', () => { it('with one option', async () => { - expectFetch('api/private/test', 200, { + expectFetch('/api/private/test', 200, { method: 'PUT', cache: 'force-cache' }) @@ -107,7 +107,7 @@ describe('PutApiRequestBuilder', () => { }) it('overriding single option', async () => { - expectFetch('api/private/test', 200, { + expectFetch('/api/private/test', 200, { method: 'PUT', cache: 'no-store' }) @@ -122,7 +122,7 @@ describe('PutApiRequestBuilder', () => { }) it('with multiple options', async () => { - expectFetch('api/private/test', 200, { + expectFetch('/api/private/test', 200, { method: 'PUT', cache: 'force-cache', integrity: 'test' @@ -138,13 +138,13 @@ describe('PutApiRequestBuilder', () => { describe('failing sendRequest', () => { it('without backend provided error name or error message', async () => { - expectFetch('api/private/test', 400, { method: 'PUT' }) + expectFetch('/api/private/test', 400, { method: 'PUT' }) const request = new PutApiRequestBuilder('test').sendRequest() await expect(request).rejects.toEqual(new ApiError(400, undefined, undefined)) }) it('with backend error name and error message', async () => { - expectFetch('api/private/test', 400, { method: 'PUT' }, { + expectFetch('/api/private/test', 400, { method: 'PUT' }, { message: 'The API has exploded!', name: 'testExplosion' } as ApiErrorResponse) @@ -153,7 +153,7 @@ describe('PutApiRequestBuilder', () => { }) it('with another status code than 400', async () => { - expectFetch('api/private/test', 401, { method: 'PUT' }, { + expectFetch('/api/private/test', 401, { method: 'PUT' }, { message: 'The API has exploded!', name: 'testExplosion' } as ApiErrorResponse) diff --git a/frontend/src/components/global-dialogs/motd-modal/fetch-motd.spec.ts b/frontend/src/components/global-dialogs/motd-modal/fetch-motd.spec.ts index 04a18b8e4..4cc5426b2 100644 --- a/frontend/src/components/global-dialogs/motd-modal/fetch-motd.spec.ts +++ b/frontend/src/components/global-dialogs/motd-modal/fetch-motd.spec.ts @@ -7,7 +7,7 @@ import { fetchMotd } from './fetch-motd' import { Mock } from 'ts-mockery' describe('fetch motd', () => { - const motdUrl = 'public/motd.md' + const motdUrl = '/public/motd.md' beforeEach(() => { window.localStorage.clear() diff --git a/frontend/src/components/global-dialogs/motd-modal/fetch-motd.ts b/frontend/src/components/global-dialogs/motd-modal/fetch-motd.ts index 4c438d7d0..6c2f6f018 100644 --- a/frontend/src/components/global-dialogs/motd-modal/fetch-motd.ts +++ b/frontend/src/components/global-dialogs/motd-modal/fetch-motd.ts @@ -23,7 +23,7 @@ export interface MotdApiResponse { */ export const fetchMotd = async (): Promise => { const cachedLastModified = window.localStorage.getItem(MOTD_LOCAL_STORAGE_KEY) - const motdUrl = `public/motd.md` + const motdUrl = `/public/motd.md` if (cachedLastModified) { const response = await fetch(motdUrl, { diff --git a/frontend/src/components/intro-page/requests.ts b/frontend/src/components/intro-page/requests.ts index 627be3c9a..50b34e345 100644 --- a/frontend/src/components/intro-page/requests.ts +++ b/frontend/src/components/intro-page/requests.ts @@ -12,7 +12,7 @@ import { defaultConfig } from '../../api/common/default-config' * @throws {Error} if the content can't be fetched */ export const fetchFrontPageContent = async (): Promise => { - const response = await fetch('public/intro.md', { + const response = await fetch('/public/intro.md', { ...defaultConfig, method: 'GET' }) diff --git a/frontend/src/components/layout/base-head.tsx b/frontend/src/components/layout/base-head.tsx index 59f2e9f89..78fe90946 100644 --- a/frontend/src/components/layout/base-head.tsx +++ b/frontend/src/components/layout/base-head.tsx @@ -4,7 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { useAppTitle } from '../../hooks/common/use-app-title' -import { useBaseUrl } from '../../hooks/common/use-base-url' import { FavIcon } from './fav-icon' import Head from 'next/head' import React from 'react' @@ -14,12 +13,10 @@ import React from 'react' */ export const BaseHead: React.FC = () => { const appTitle = useAppTitle() - const baseUrl = useBaseUrl() return ( {appTitle} - ) diff --git a/frontend/src/components/layout/fav-icon.tsx b/frontend/src/components/layout/fav-icon.tsx index 2128e0a2d..e8e7fc2e4 100644 --- a/frontend/src/components/layout/fav-icon.tsx +++ b/frontend/src/components/layout/fav-icon.tsx @@ -11,17 +11,17 @@ import React, { Fragment } from 'react' export const FavIcon: React.FC = () => { return ( - - - - - - + + + + + + - + ) diff --git a/frontend/src/pages/api/private/config.ts b/frontend/src/pages/api/private/config.ts index a1e0d4b57..3692d1204 100644 --- a/frontend/src/pages/api/private/config.ts +++ b/frontend/src/pages/api/private/config.ts @@ -54,7 +54,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse) => { ], branding: { name: 'DEMO Corp', - logo: 'public/img/demo.png' + logo: '/public/img/demo.png' }, useImageProxy: false, specialUrls: { diff --git a/frontend/src/pages/api/private/me/index.ts b/frontend/src/pages/api/private/me/index.ts index c56278ecc..19f96cf33 100644 --- a/frontend/src/pages/api/private/me/index.ts +++ b/frontend/src/pages/api/private/me/index.ts @@ -10,7 +10,7 @@ import type { NextApiRequest, NextApiResponse } from 'next' const handler = (req: NextApiRequest, res: NextApiResponse): void => { respondToMatchingRequest(HttpMethod.GET, req, res, { username: 'mock', - photo: 'public/img/avatar.png', + photo: '/public/img/avatar.png', displayName: 'Mock User', authProvider: 'local', email: 'mock@hedgedoc.test' diff --git a/frontend/src/pages/api/private/media.ts b/frontend/src/pages/api/private/media.ts index 07dca311f..0b1b6f543 100644 --- a/frontend/src/pages/api/private/media.ts +++ b/frontend/src/pages/api/private/media.ts @@ -20,7 +20,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse): Promise req, res, { - url: 'public/img/avatar.png', + url: '/public/img/avatar.png', noteId: null, username: 'test', createdAt: '2022-02-27T21:54:23.856Z' diff --git a/frontend/src/pages/api/private/notes/features/index.ts b/frontend/src/pages/api/private/notes/features/index.ts index 8e5445b96..ef76d20ed 100644 --- a/frontend/src/pages/api/private/notes/features/index.ts +++ b/frontend/src/pages/api/private/notes/features/index.ts @@ -10,7 +10,7 @@ import type { NextApiRequest, NextApiResponse } from 'next' const handler = (req: NextApiRequest, res: NextApiResponse): void => { respondToMatchingRequest(HttpMethod.GET, req, res, { content: - '---\ntitle: Features\ndescription: Many features, such wow!\nrobots: noindex\ntags:\n - hedgedoc\n - demo\n - react\nopengraph:\n title: Features\n---\n# Embedding demo\n[TOC]\n\n## Vega-Lite\n\n```vega-lite\n\n{\n "$schema": "https://vega.github.io/schema/vega-lite/v5.json",\n "description": "Reproducing http://robslink.com/SAS/democd91/pyramid_pie.htm",\n "data": {\n "values": [\n {"category": "Sky", "value": 75, "order": 3},\n {"category": "Shady side of a pyramid", "value": 10, "order": 1},\n {"category": "Sunny side of a pyramid", "value": 15, "order": 2}\n ]\n },\n "mark": {"type": "arc", "outerRadius": 80},\n "encoding": {\n "theta": {\n "field": "value", "type": "quantitative",\n "scale": {"range": [2.35619449, 8.639379797]},\n "stack": true\n },\n "color": {\n "field": "category", "type": "nominal",\n "scale": {\n "domain": ["Sky", "Shady side of a pyramid", "Sunny side of a pyramid"],\n "range": ["#416D9D", "#674028", "#DEAC58"]\n },\n "legend": {\n "orient": "none",\n "title": null,\n "columns": 1,\n "legendX": 200,\n "legendY": 80\n }\n },\n "order": {\n "field": "order"\n }\n },\n "view": {"stroke": null}\n}\n\n\n```\n\n## GraphViz\n\n```graphviz\ngraph {\n a -- b\n a -- b\n b -- a [color=blue]\n}\n```\n\n```graphviz\ndigraph structs {\n node [shape=record];\n struct1 [label=" left| mid\ dle| right"];\n struct2 [label=" one| two"];\n struct3 [label="hello\nworld |{ b |{c| d|e}| f}| g | h"];\n struct1:f1 -> struct2:f0;\n struct1:f2 -> struct3:here;\n}\n```\n\n```graphviz\ndigraph G {\n main -> parse -> execute;\n main -> init;\n main -> cleanup;\n execute -> make_string;\n execute -> printf\n init -> make_string;\n main -> printf;\n execute -> compare;\n}\n```\n\n```graphviz\ndigraph D {\n node [fontname="Arial"];\n node_A [shape=record label="shape=record|{above|middle|below}|right"];\n node_B [shape=plaintext label="shape=plaintext|{curly|braces and|bars without}|effect"];\n}\n```\n\n```graphviz\ndigraph D {\n A -> {B, C, D} -> {F}\n}\n```\n\n## High Res Image\n\n![Wheat Field with Cypresses](public/img/highres.jpg)\n\n## Sequence Diagram (deprecated)\n\n```sequence\nTitle: Here is a title\nnote over A: asdd\nA->B: Normal line\nB-->C: Dashed line\nC->>D: Open arrow\nD-->>A: Dashed open arrow\nparticipant IOOO\n```\n\n## Mermaid\n\n```mermaid\ngantt\n title A Gantt Diagram\n\n section Section\n A task: a1, 2014-01-01, 30d\n Another task: after a1, 20d\n\n section Another\n Task in sec: 2014-01-12, 12d\n Another task: 24d\n```\n\n## Flowchart\n\n```flow\nst=>start: Start\ne=>end: End\nop=>operation: My Operation\nop2=>operation: lalala\ncond=>condition: Yes or No?\n\nst->op->op2->cond\ncond(yes)->e\ncond(no)->op2\n```\n\n## ABC\n\n```abc\nX:1\nT:Speed the Plough\nM:4/4\nC:Trad.\nK:G\n|:GABc dedB|dedB dedB|c2ec B2dB|c2A2 A2BA|\nGABc dedB|dedB dedB|c2ec B2dB|A2F2 G4:|\n|:g2gf gdBd|g2f2 e2d2|c2ec B2dB|c2A2 A2df|\ng2gf g2Bd|g2f2 e2d2|c2ec B2dB|A2F2 G4:|\n```\n\n## CSV\n\n```csv delimiter=; header\nUsername; Identifier;First name;Last name\n"booker12; rbooker";9012;Rachel;Booker\ngrey07;2070;Laura;Grey\njohnson81;4081;Craig;Johnson\njenkins46;9346;Mary;Jenkins\nsmith79;5079;Jamie;Smith\n```\n\n## some plain text\n\nLorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.\n\n## KaTeX\nYou can render *LaTeX* mathematical expressions using **KaTeX**, as on [math.stackexchange.com](https://math.stackexchange.com/):\n\nThe *Gamma function* satisfying $\\Gamma(n) = (n-1)!\\quad\\forall n\\in\\mathbb N$ is via the Euler integral\n\n$$\nx = {-b \\pm \\sqrt{b^2-4ac} \\over 2a}.\n$$\n\n$$\n\\Gamma(z) = \\int_0^\\infty t^{z-1}e^{-t}dt\\,.\n$$\n\n> More information about **LaTeX** mathematical expressions [here](https://meta.math.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference).\n\n## Blockquote\n> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n> [color=red] [name=John Doe] [time=2020-06-21 22:50]\n\n## Slideshare\n{%slideshare mazlan1/internet-of-things-the-tip-of-an-iceberg %}\n\n## Gist\nhttps://gist.github.com/schacon/1\n\n## YouTube\nhttps://www.youtube.com/watch?v=YE7VzlLtp-4\n\n## Vimeo\nhttps://vimeo.com/23237102\n\n## Asciinema\nhttps://asciinema.org/a/117928\n\n## PDF\n{%pdf https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf %}\n\n## Code highlighting\n```js=\nvar s = "JavaScript syntax highlighting";\nalert(s);\nfunction $initHighlight(block, cls) {\n try {\n if (cls.search(/\\bno\\-highlight\\b/) != -1)\n return process(block, true, 0x0F) +\n \' class=""\';\n } catch (e) {\n /* handle exception */\n }\n for (var i = 0 / 2; i < classes.length; i++) {\n if (checkCondition(classes[i]) === undefined)\n return /\\d+[\\s/]/g;\n }\n}\n```\n\n## PlantUML\n```plantuml\n@startuml\nparticipant Alice\nparticipant "The **Famous** Bob" as Bob\n\nAlice -> Bob : hello --there--\n... Some ~~long delay~~ ...\nBob -> Alice : ok\nnote left\n This is **bold**\n This is //italics//\n This is ""monospaced""\n This is --stroked--\n This is __underlined__\n This is ~~waved~~\nend note\n\nAlice -> Bob : A //well formatted// message\nnote right of Alice\n This is displayed\n __left of__ Alice.\nend note\nnote left of Bob\n This is displayed\n **left of Alice Bob**.\nend note\nnote over Alice, Bob\n This is hosted by \nend note\n@enduml\n```\n\n## ToDo List\n\n- [ ] ToDos\n - [X] Buy some salad\n - [ ] Brush teeth\n - [x] Drink some water\n - [ ] **Click my box** and see the source code, if you\'re allowed to edit!\n\n', + '---\ntitle: Features\ndescription: Many features, such wow!\nrobots: noindex\ntags:\n - hedgedoc\n - demo\n - react\nopengraph:\n title: Features\n---\n# Embedding demo\n[TOC]\n\n## Vega-Lite\n\n```vega-lite\n\n{\n "$schema": "https://vega.github.io/schema/vega-lite/v5.json",\n "description": "Reproducing http://robslink.com/SAS/democd91/pyramid_pie.htm",\n "data": {\n "values": [\n {"category": "Sky", "value": 75, "order": 3},\n {"category": "Shady side of a pyramid", "value": 10, "order": 1},\n {"category": "Sunny side of a pyramid", "value": 15, "order": 2}\n ]\n },\n "mark": {"type": "arc", "outerRadius": 80},\n "encoding": {\n "theta": {\n "field": "value", "type": "quantitative",\n "scale": {"range": [2.35619449, 8.639379797]},\n "stack": true\n },\n "color": {\n "field": "category", "type": "nominal",\n "scale": {\n "domain": ["Sky", "Shady side of a pyramid", "Sunny side of a pyramid"],\n "range": ["#416D9D", "#674028", "#DEAC58"]\n },\n "legend": {\n "orient": "none",\n "title": null,\n "columns": 1,\n "legendX": 200,\n "legendY": 80\n }\n },\n "order": {\n "field": "order"\n }\n },\n "view": {"stroke": null}\n}\n\n\n```\n\n## GraphViz\n\n```graphviz\ngraph {\n a -- b\n a -- b\n b -- a [color=blue]\n}\n```\n\n```graphviz\ndigraph structs {\n node [shape=record];\n struct1 [label=" left| mid\ dle| right"];\n struct2 [label=" one| two"];\n struct3 [label="hello\nworld |{ b |{c| d|e}| f}| g | h"];\n struct1:f1 -> struct2:f0;\n struct1:f2 -> struct3:here;\n}\n```\n\n```graphviz\ndigraph G {\n main -> parse -> execute;\n main -> init;\n main -> cleanup;\n execute -> make_string;\n execute -> printf\n init -> make_string;\n main -> printf;\n execute -> compare;\n}\n```\n\n```graphviz\ndigraph D {\n node [fontname="Arial"];\n node_A [shape=record label="shape=record|{above|middle|below}|right"];\n node_B [shape=plaintext label="shape=plaintext|{curly|braces and|bars without}|effect"];\n}\n```\n\n```graphviz\ndigraph D {\n A -> {B, C, D} -> {F}\n}\n```\n\n## High Res Image\n\n![Wheat Field with Cypresses](/public/img/highres.jpg)\n\n## Sequence Diagram (deprecated)\n\n```sequence\nTitle: Here is a title\nnote over A: asdd\nA->B: Normal line\nB-->C: Dashed line\nC->>D: Open arrow\nD-->>A: Dashed open arrow\nparticipant IOOO\n```\n\n## Mermaid\n\n```mermaid\ngantt\n title A Gantt Diagram\n\n section Section\n A task: a1, 2014-01-01, 30d\n Another task: after a1, 20d\n\n section Another\n Task in sec: 2014-01-12, 12d\n Another task: 24d\n```\n\n## Flowchart\n\n```flow\nst=>start: Start\ne=>end: End\nop=>operation: My Operation\nop2=>operation: lalala\ncond=>condition: Yes or No?\n\nst->op->op2->cond\ncond(yes)->e\ncond(no)->op2\n```\n\n## ABC\n\n```abc\nX:1\nT:Speed the Plough\nM:4/4\nC:Trad.\nK:G\n|:GABc dedB|dedB dedB|c2ec B2dB|c2A2 A2BA|\nGABc dedB|dedB dedB|c2ec B2dB|A2F2 G4:|\n|:g2gf gdBd|g2f2 e2d2|c2ec B2dB|c2A2 A2df|\ng2gf g2Bd|g2f2 e2d2|c2ec B2dB|A2F2 G4:|\n```\n\n## CSV\n\n```csv delimiter=; header\nUsername; Identifier;First name;Last name\n"booker12; rbooker";9012;Rachel;Booker\ngrey07;2070;Laura;Grey\njohnson81;4081;Craig;Johnson\njenkins46;9346;Mary;Jenkins\nsmith79;5079;Jamie;Smith\n```\n\n## some plain text\n\nLorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.\n\n## KaTeX\nYou can render *LaTeX* mathematical expressions using **KaTeX**, as on [math.stackexchange.com](https://math.stackexchange.com/):\n\nThe *Gamma function* satisfying $\\Gamma(n) = (n-1)!\\quad\\forall n\\in\\mathbb N$ is via the Euler integral\n\n$$\nx = {-b \\pm \\sqrt{b^2-4ac} \\over 2a}.\n$$\n\n$$\n\\Gamma(z) = \\int_0^\\infty t^{z-1}e^{-t}dt\\,.\n$$\n\n> More information about **LaTeX** mathematical expressions [here](https://meta.math.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference).\n\n## Blockquote\n> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n> [color=red] [name=John Doe] [time=2020-06-21 22:50]\n\n## Slideshare\n{%slideshare mazlan1/internet-of-things-the-tip-of-an-iceberg %}\n\n## Gist\nhttps://gist.github.com/schacon/1\n\n## YouTube\nhttps://www.youtube.com/watch?v=YE7VzlLtp-4\n\n## Vimeo\nhttps://vimeo.com/23237102\n\n## Asciinema\nhttps://asciinema.org/a/117928\n\n## PDF\n{%pdf https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf %}\n\n## Code highlighting\n```js=\nvar s = "JavaScript syntax highlighting";\nalert(s);\nfunction $initHighlight(block, cls) {\n try {\n if (cls.search(/\\bno\\-highlight\\b/) != -1)\n return process(block, true, 0x0F) +\n \' class=""\';\n } catch (e) {\n /* handle exception */\n }\n for (var i = 0 / 2; i < classes.length; i++) {\n if (checkCondition(classes[i]) === undefined)\n return /\\d+[\\s/]/g;\n }\n}\n```\n\n## PlantUML\n```plantuml\n@startuml\nparticipant Alice\nparticipant "The **Famous** Bob" as Bob\n\nAlice -> Bob : hello --there--\n... Some ~~long delay~~ ...\nBob -> Alice : ok\nnote left\n This is **bold**\n This is //italics//\n This is ""monospaced""\n This is --stroked--\n This is __underlined__\n This is ~~waved~~\nend note\n\nAlice -> Bob : A //well formatted// message\nnote right of Alice\n This is displayed\n __left of__ Alice.\nend note\nnote left of Bob\n This is displayed\n **left of Alice Bob**.\nend note\nnote over Alice, Bob\n This is hosted by \nend note\n@enduml\n```\n\n## ToDo List\n\n- [ ] ToDos\n - [X] Buy some salad\n - [ ] Brush teeth\n - [x] Drink some water\n - [ ] **Click my box** and see the source code, if you\'re allowed to edit!\n\n', metadata: { id: 'exampleId', version: 2, diff --git a/frontend/src/pages/api/private/users/erik.ts b/frontend/src/pages/api/private/users/erik.ts index a39c40bab..f42614d44 100644 --- a/frontend/src/pages/api/private/users/erik.ts +++ b/frontend/src/pages/api/private/users/erik.ts @@ -11,7 +11,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => { respondToMatchingRequest(HttpMethod.GET, req, res, { username: 'erik', displayName: 'Erik', - photo: 'public/img/avatar.png' + photo: '/public/img/avatar.png' }) } diff --git a/frontend/src/pages/api/private/users/molly.ts b/frontend/src/pages/api/private/users/molly.ts index 18c0f8b05..36741d757 100644 --- a/frontend/src/pages/api/private/users/molly.ts +++ b/frontend/src/pages/api/private/users/molly.ts @@ -11,7 +11,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => { respondToMatchingRequest(HttpMethod.GET, req, res, { username: 'molly', displayName: 'Molly', - photo: 'public/img/avatar.png' + photo: '/public/img/avatar.png' }) } diff --git a/frontend/src/pages/api/private/users/tilman.ts b/frontend/src/pages/api/private/users/tilman.ts index 31d581c12..528617ad0 100644 --- a/frontend/src/pages/api/private/users/tilman.ts +++ b/frontend/src/pages/api/private/users/tilman.ts @@ -11,7 +11,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => { respondToMatchingRequest(HttpMethod.GET, req, res, { username: 'tilman', displayName: 'Tilman', - photo: 'public/img/avatar.png' + photo: '/public/img/avatar.png' }) } diff --git a/frontend/src/utils/base-url-from-env-extractor.ts b/frontend/src/utils/base-url-from-env-extractor.ts index 2167c7d95..90c511202 100644 --- a/frontend/src/utils/base-url-from-env-extractor.ts +++ b/frontend/src/utils/base-url-from-env-extractor.ts @@ -6,7 +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 { NoSubdirectoryAllowedError, parseUrl } from '@hedgedoc/commons' import { Optional } from '@mrdrogdrog/optional' /** @@ -20,8 +20,8 @@ export class BaseUrlFromEnvExtractor { try { return parseUrl(envVarValue) } catch (error) { - if (error instanceof MissingTrailingSlashError) { - this.logger.error(`The path in ${envVarName} must end with an '/'`) + if (error instanceof NoSubdirectoryAllowedError) { + this.logger.error(error.message) return Optional.empty() } else { throw error