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 <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-08-12 20:07:38 +02:00
parent 7401791ec8
commit dccd58f0c1
32 changed files with 111 additions and 116 deletions

View file

@ -27,7 +27,7 @@ export abstract class ApiRequestBuilder<ResponseType> {
* @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<ApiResponse<ResponseType>> {

View file

@ -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<string, undefined>('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<string>('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)

View file

@ -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<string>('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<string>('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)

View file

@ -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<string, undefined>('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<string, string>('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)

View file

@ -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<string, undefined>('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<string, undefined>('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)

View file

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

View file

@ -23,7 +23,7 @@ export interface MotdApiResponse {
*/
export const fetchMotd = async (): Promise<MotdApiResponse | undefined> => {
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, {

View file

@ -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<string> => {
const response = await fetch('public/intro.md', {
const response = await fetch('/public/intro.md', {
...defaultConfig,
method: 'GET'
})

View file

@ -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 (
<Head>
<title>{appTitle}</title>
<FavIcon />
<base href={baseUrl} />
<meta content='width=device-width, initial-scale=1' name='viewport' />
</Head>
)

View file

@ -11,17 +11,17 @@ import React, { Fragment } from 'react'
export const FavIcon: React.FC = () => {
return (
<Fragment>
<link href='icons/apple-touch-icon.png' rel='apple-touch-icon' sizes='180x180' />
<link href='icons/favicon-32x32.png' rel='icon' sizes='32x32' type='image/png' />
<link href='icons/favicon-16x16.png' rel='icon' sizes='16x16' type='image/png' />
<link href='icons/site.webmanifest' rel='manifest' />
<link href='icons/favicon.ico' rel='shortcut icon' />
<link color='#b51f08' href='icons/safari-pinned-tab.svg' rel='mask-icon' />
<link href='/icons/apple-touch-icon.png' rel='apple-touch-icon' sizes='180x180' />
<link href='/icons/favicon-32x32.png' rel='icon' sizes='32x32' type='image/png' />
<link href='/icons/favicon-16x16.png' rel='icon' sizes='16x16' type='image/png' />
<link href='/icons/site.webmanifest' rel='manifest' />
<link href='/icons/favicon.ico' rel='shortcut icon' />
<link color='#b51f08' href='/icons/safari-pinned-tab.svg' rel='mask-icon' />
<meta name='apple-mobile-web-app-title' content='HedgeDoc' />
<meta name='application-name' content='HedgeDoc' />
<meta name='msapplication-TileColor' content='#b51f08' />
<meta name='theme-color' content='#b51f08' />
<meta content='icons/browserconfig.xml' name='msapplication-config' />
<meta content='/icons/browserconfig.xml' name='msapplication-config' />
<meta content='HedgeDoc - Collaborative markdown notes' name='description' />
</Fragment>
)

View file

@ -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: {

View file

@ -10,7 +10,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
respondToMatchingRequest<LoginUserInfo>(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'

View file

@ -20,7 +20,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse): Promise<void>
req,
res,
{
url: 'public/img/avatar.png',
url: '/public/img/avatar.png',
noteId: null,
username: 'test',
createdAt: '2022-02-27T21:54:23.856Z'

File diff suppressed because one or more lines are too long

View file

@ -11,7 +11,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => {
respondToMatchingRequest<UserInfo>(HttpMethod.GET, req, res, {
username: 'erik',
displayName: 'Erik',
photo: 'public/img/avatar.png'
photo: '/public/img/avatar.png'
})
}

View file

@ -11,7 +11,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => {
respondToMatchingRequest<UserInfo>(HttpMethod.GET, req, res, {
username: 'molly',
displayName: 'Molly',
photo: 'public/img/avatar.png'
photo: '/public/img/avatar.png'
})
}

View file

@ -11,7 +11,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => {
respondToMatchingRequest<UserInfo>(HttpMethod.GET, req, res, {
username: 'tilman',
displayName: 'Tilman',
photo: 'public/img/avatar.png'
photo: '/public/img/avatar.png'
})
}

View file

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