diff --git a/.env.development b/.env.development index 4ffbf09f3..5e63b4028 100644 --- a/.env.development +++ b/.env.development @@ -1 +1,3 @@ NEXT_PUBLIC_USE_MOCK_API=true +HD_EDITOR_BASE_URL="http://localhost:3001/" +HD_RENDERER_BASE_URL="http://127.0.0.1:3001/" diff --git a/.env.production b/.env.production index 93e82ddd3..790a80ba4 100644 --- a/.env.production +++ b/.env.production @@ -1 +1,2 @@ NEXT_PUBLIC_USE_MOCK_API=false +NEXT_PUBLIC_TEST_MODE=false diff --git a/.env.test b/.env.test index 4ffbf09f3..3a8a4f65d 100644 --- a/.env.test +++ b/.env.test @@ -1 +1,3 @@ NEXT_PUBLIC_USE_MOCK_API=true +HD_EDITOR_BASE_URL="http://127.0.0.1:3001/" +HD_RENDERER_BASE_URL="http://127.0.0.1:3001/" diff --git a/.github/workflows/deploy-pr.yml b/.github/workflows/deploy-pr.yml index a20450b6b..de18a7540 100644 --- a/.github/workflows/deploy-pr.yml +++ b/.github/workflows/deploy-pr.yml @@ -35,13 +35,13 @@ jobs: node-version: 18 - name: Patch files - run: bash netlify/patch-files.sh + run: bash netlify/patch-files.sh "${{ github.event.number }}" - name: Install dependencies run: yarn install --immutable - - name: Build netlify variant - run: yarn build:netlify + - name: Build mock variant + run: yarn build:mock - name: Remove Next.js cache to avoid it being deployed run: rm -r .next/cache diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 0a23b49a5..b89cd37f4 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -108,6 +108,8 @@ jobs: - name: Run server run: yarn start:ci & + env: + NODE_ENV: test - name: Wait for server run: "sleep 3 && curl --max-time 120 http://127.0.0.1:3001/" diff --git a/.reuse/dep5 b/.reuse/dep5 index 89fc2f5a4..d21e416df 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -3,7 +3,7 @@ Upstream-Name: react-client Upstream-Contact: The HedgeDoc developers Source: https://github.com/hedgedoc/react-client -Files: public/mock-public/* +Files: public/public/* Copyright: 2021 The HedgeDoc developers (see AUTHORS file) License: CC0-1.0 @@ -15,7 +15,7 @@ Files: locales/* Copyright: 2021 The HedgeDoc developers (see AUTHORS file) License: CC-BY-SA-4.0 -Files: public/mock-public/img/highres.jpg +Files: public/public/img/highres.jpg Copyright: Vincent van Gogh License: CC0-1.0 diff --git a/Dockerfile b/Dockerfile index 1b5b02672..b2f10b7b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,14 +4,17 @@ # BUILD FROM node:18-alpine AS builder -ARG BUILD_VERSION=CLIENT_VERSION_MISSING +ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 +ARG BUILD_VERSION=CLIENT_VERSION_MISSING WORKDIR /app COPY . ./ -RUN yarn install --immutable && \ +RUN rm -rf public/public && \ + rm -rf src/pages/api && \ + yarn install --immutable && \ sed -i "s/CLIENT_VERSION_MISSING/${BUILD_VERSION}/" src/version.json && \ - yarn build:for-real-backend + yarn build # RUNNER FROM node:18-alpine @@ -27,6 +30,6 @@ COPY --from=builder --chown=node:node /app/.next/standalone ./ USER node -ENV PORT 3000 -EXPOSE 3000/tcp +ENV PORT 3001 +EXPOSE 3001/tcp CMD ["node", "server.js"] diff --git a/README.md b/README.md index 784f812a7..ce842586f 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,8 @@ This will build the app in production mode and save it into the `.next` folder. best performance, minimized and the filenames include a hash value of the content. Don't edit them by hand! 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) path (e.g. `http://127.0.0.1:3001/`) so the app knows under which URL it is available in the browser. +You can also provide `HD_RENDERER_BASE_URL` if the renderer should use another domain than the editor. This is recommended for security reasons but not mandatory. ## UI Test diff --git a/cypress/e2e/deleteNote.spec.ts b/cypress/e2e/deleteNote.spec.ts index 9240db8e7..06eedbbe4 100644 --- a/cypress/e2e/deleteNote.spec.ts +++ b/cypress/e2e/deleteNote.spec.ts @@ -12,7 +12,7 @@ describe('Delete note', () => { }) it('correctly deletes a note', () => { - cy.intercept('DELETE', `/api/mock-backend/private/notes/${testNoteId}`, { + cy.intercept('DELETE', `api/private/notes/${testNoteId}`, { statusCode: 204 }) cy.getByCypressId('sidebar.deleteNote.button').click() diff --git a/cypress/e2e/fileUpload.spec.ts b/cypress/e2e/fileUpload.spec.ts index a3306d7ec..ffe6738fd 100644 --- a/cypress/e2e/fileUpload.spec.ts +++ b/cypress/e2e/fileUpload.spec.ts @@ -17,7 +17,7 @@ describe('File upload', () => { cy.intercept( { method: 'POST', - url: '/api/mock-backend/private/media' + url: 'api/private/media' }, { statusCode: 201, @@ -74,7 +74,7 @@ describe('File upload', () => { cy.intercept( { method: 'POST', - url: '/api/mock-backend/private/media' + url: 'api/private/media' }, { statusCode: 400 diff --git a/cypress/e2e/history.spec.ts b/cypress/e2e/history.spec.ts index aafcdf522..89ab41351 100644 --- a/cypress/e2e/history.spec.ts +++ b/cypress/e2e/history.spec.ts @@ -24,7 +24,7 @@ describe('History', () => { describe('is as given when not empty', () => { beforeEach(() => { cy.clearLocalStorage('history') - cy.intercept('GET', '/api/mock-backend/private/me/history', { + cy.intercept('GET', 'api/private/me/history', { body: [ { identifier: 'cypress', @@ -51,7 +51,7 @@ describe('History', () => { describe('is untitled when not empty', () => { beforeEach(() => { cy.clearLocalStorage('history') - cy.intercept('GET', '/api/mock-backend/private/me/history', { + cy.intercept('GET', 'api/private/me/history', { body: [ { identifier: 'cypress-no-title', @@ -84,7 +84,7 @@ describe('History', () => { describe('working', () => { beforeEach(() => { - cy.intercept('PUT', '/api/mock-backend/private/me/history/features', (req) => { + cy.intercept('PUT', 'api/private/me/history/features', (req) => { req.reply(200, req.body) }) }) @@ -106,7 +106,7 @@ describe('History', () => { describe('failing', () => { beforeEach(() => { - cy.intercept('PUT', '/api/mock-backend/private/me/history/features', { + cy.intercept('PUT', 'api/private/me/history/features', { statusCode: 401 }) }) @@ -128,7 +128,7 @@ describe('History', () => { describe('Import', () => { beforeEach(() => { cy.clearLocalStorage('history') - cy.intercept('GET', '/api/mock-backend/private/me/history', { + cy.intercept('GET', 'api/private/me/history', { body: [] }) cy.visitHistory() diff --git a/cypress/e2e/intro.spec.ts b/cypress/e2e/intro.spec.ts index 015a31c94..f9e9bae0b 100644 --- a/cypress/e2e/intro.spec.ts +++ b/cypress/e2e/intro.spec.ts @@ -7,7 +7,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ describe('Intro page', () => { beforeEach(() => { - cy.intercept('/mock-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('/mock-public/intro.md', { + cy.intercept('public/intro.md', { statusCode: 404 }) cy.visitHome() diff --git a/cypress/e2e/motd.spec.ts b/cypress/e2e/motd.spec.ts index 507429595..085491c27 100644 --- a/cypress/e2e/motd.spec.ts +++ b/cypress/e2e/motd.spec.ts @@ -11,13 +11,13 @@ const motdMockHtml = 'This is the mock Motd call' describe('Motd', () => { const mockExistingMotd = (useEtag?: boolean, content = motdMockContent) => { - cy.intercept('GET', '/mock-public/motd.md', { + cy.intercept('GET', 'public/motd.md', { statusCode: 200, headers: { [useEtag ? 'etag' : 'Last-Modified']: MOCK_LAST_MODIFIED }, body: content }) - cy.intercept('HEAD', '/mock-public/motd.md', { + cy.intercept('HEAD', 'public/motd.md', { statusCode: 200, headers: { [useEtag ? 'etag' : 'Last-Modified']: MOCK_LAST_MODIFIED } }) diff --git a/cypress/e2e/profile.spec.ts b/cypress/e2e/profile.spec.ts index ed869e15c..644da1d3f 100644 --- a/cypress/e2e/profile.spec.ts +++ b/cypress/e2e/profile.spec.ts @@ -8,7 +8,7 @@ describe('profile page', () => { beforeEach(() => { cy.intercept( { - url: '/api/mock-backend/private/tokens', + url: 'api/private/tokens', method: 'GET' }, { @@ -25,7 +25,7 @@ describe('profile page', () => { ) cy.intercept( { - url: '/api/mock-backend/private/tokens', + url: 'api/private/tokens', method: 'POST' }, { @@ -42,7 +42,7 @@ describe('profile page', () => { ) cy.intercept( { - url: '/api/mock-backend/private/tokens/cypress', + url: 'api/private/tokens/cypress', method: 'DELETE' }, { diff --git a/cypress/support/config.ts b/cypress/support/config.ts index 6e9c7e7d2..eb9d6e947 100644 --- a/cypress/support/config.ts +++ b/cypress/support/config.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -14,7 +14,7 @@ declare namespace Cypress { export const branding = { name: 'DEMO Corp', - logo: '/mock-public/img/demo.png' + logo: 'public/img/demo.png' } export const authProviders = [ @@ -77,15 +77,11 @@ export const config = { commit: 'MOCK' }, plantumlServer: 'http://mock-plantuml.local', - maxDocumentLength: 200, - iframeCommunication: { - editorOrigin: 'http://127.0.0.1:3001/', - rendererOrigin: 'http://127.0.0.1:3001/' - } + maxDocumentLength: 200 } Cypress.Commands.add('loadConfig', (additionalConfig?: Partial) => { - return cy.intercept('/api/mock-backend/private/config', { + return cy.intercept('api/private/config', { statusCode: 200, body: { ...config, @@ -97,11 +93,11 @@ Cypress.Commands.add('loadConfig', (additionalConfig?: Partial) = beforeEach(() => { cy.loadConfig() - cy.intercept('GET', '/mock-public/motd.md', { + cy.intercept('GET', 'public/motd.md', { body: '404 Not Found!', statusCode: 404 }) - cy.intercept('HEAD', '/mock-public/motd.md', { + cy.intercept('HEAD', 'public/motd.md', { statusCode: 404 }) }) diff --git a/cypress/support/visit-test-editor.ts b/cypress/support/visit-test-editor.ts index 4f12ed555..5835e70f1 100644 --- a/cypress/support/visit-test-editor.ts +++ b/cypress/support/visit-test-editor.ts @@ -1,12 +1,12 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ export const testNoteId = 'test' beforeEach(() => { - cy.intercept(`/api/mock-backend/private/notes/${testNoteId}`, { + cy.intercept(`api/private/notes/${testNoteId}`, { content: '', metadata: { id: testNoteId, diff --git a/netlify.toml b/netlify.toml index f4661b875..ba619a434 100644 --- a/netlify.toml +++ b/netlify.toml @@ -4,3 +4,6 @@ command = "echo Pseudo build command because the build is made by the CI" [[plugins]] package = "@netlify/plugin-nextjs" + +[dev] +targetPort = 3001 diff --git a/netlify/intro.md b/netlify/intro.md index 353b2b1cb..546a7b80e 100644 --- a/netlify/intro.md +++ b/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](/mock-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/netlify/patch-files.sh b/netlify/patch-files.sh index 283f6aefd..53764c11a 100644 --- a/netlify/patch-files.sh +++ b/netlify/patch-files.sh @@ -8,9 +8,11 @@ set -e echo 'Patch intro.md to include netlify banner.' -cp netlify/intro.md public/mock-public/intro.md +cp netlify/intro.md public/public/intro.md echo 'Patch motd.md to include privacy policy.' -cp netlify/motd.md public/mock-public/motd.md +cp netlify/motd.md public/public/motd.md echo 'Patch version.json to include git hash' jq ".version = \"0.0.0+${GITHUB_SHA:0:8}\"" src/version.json > src/_version.json mv src/_version.json src/version.json +echo "Patch base URL" +echo HD_EDITOR_BASE_URL="https://${1}--hedgedoc-ui-test.netlify.app/" >> .env.production diff --git a/next.config.js b/next.config.js index da0e74815..deb602698 100644 --- a/next.config.js +++ b/next.config.js @@ -3,47 +3,27 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +const { isMockMode, isTestMode } = require('./src/utils/test-modes') +const path = require('path') +const CopyWebpackPlugin = require('copy-webpack-plugin') +const withBundleAnalyzer = require('@next/bundle-analyzer')({ + enabled: process.env.ANALYZE === 'true' +}) -//TODO: [mrdrogdrog] The following function and two constants already exist in typescript code. -// However, this file must be a js file and therefore can't access the ts function. -// I have no idea how to resolve this redundancy without extracting it into a node module. -const isPositiveAnswer = (value) => { - const lowerValue = value.toLowerCase() - return lowerValue === 'yes' || lowerValue === '1' || lowerValue === 'true' -} - -const isTestMode = !!process.env.NEXT_PUBLIC_TEST_MODE && isPositiveAnswer(process.env.NEXT_PUBLIC_TEST_MODE) - -const isMockMode = !!process.env.NEXT_PUBLIC_USE_MOCK_API && isPositiveAnswer(process.env.NEXT_PUBLIC_USE_MOCK_API) - -console.log('Node env is', process.env.NODE_ENV) +console.log('Node environment is', process.env.NODE_ENV) if (isMockMode) { - console.log('Uses mock API') -} else if (!!process.env.NEXT_PUBLIC_BACKEND_BASE_URL) { - console.log('Backend base url is', process.env.NEXT_PUBLIC_BACKEND_BASE_URL) -} else { - console.error(`============== -Neither NEXT_PUBLIC_USE_MOCK_API or NEXT_PUBLIC_BACKEND_BASE_URL is set. -If you want to create a production build we suggest that you set a backend url with NEXT_PUBLIC_BACKEND_BASE_URL. -If you want to create a build that uses the mock api then use build:mock instead or set NEXT_PUBLIC_USE_MOCK_API to "true". -==============`) - process.exit(1) -} - -if (!!process.env.NEXT_PUBLIC_IGNORE_IFRAME_ORIGIN_CONFIG) { - console.warn( - 'You have set NEXT_PUBLIC_IGNORE_IFRAME_ORIGIN_CONFIG. This flag is ONLY for testing purposes and will decrease the security of the editor if used in production!' - ) + console.log('Use mock API') } if (isTestMode) { console.warn(`This build runs in test mode. This means: - no sandboxed iframe - - Additional data-attributes for e2e tests added to DOM`) + - Additional data-attributes for e2e tests added to DOM + - Editor and renderer are running on the same origin`) } -if (!!isMockMode) { +if (isMockMode) { console.warn(`This build runs in mock mode. This means: - No real data. All API responses are mocked - No persistent data @@ -51,12 +31,6 @@ if (!!isMockMode) { `) } -const path = require('path') -const CopyWebpackPlugin = require('copy-webpack-plugin') -const withBundleAnalyzer = require('@next/bundle-analyzer')({ - enabled: process.env.ANALYZE === 'true' -}) - /** @type {import('next').NextConfig} */ const rawNextConfig = { webpack: (config) => { @@ -111,9 +85,8 @@ const rawNextConfig = { } ]) }, - output: 'standalone', + output: 'standalone' } - const completeNextConfig = withBundleAnalyzer(rawNextConfig) module.exports = completeNextConfig diff --git a/package.json b/package.json index 8c45a7f2e..97d0e8fc5 100644 --- a/package.json +++ b/package.json @@ -3,28 +3,24 @@ "version": "0.0.1", "license": "AGPL-3.0", "scripts": { - "build": "next build", - "build:netlify": "cross-env NEXT_PUBLIC_IGNORE_IFRAME_ORIGIN_CONFIG=true yarn build:mock", + "build": "cross-env NODE_ENV=production next build", "build:mock": "cross-env NEXT_PUBLIC_USE_MOCK_API=true next build", - "build:test": "cross-env NEXT_PUBLIC_USE_MOCK_API=true NEXT_PUBLIC_TEST_MODE=true next build", - "build:for-real-backend": "cross-env NEXT_PUBLIC_USE_MOCK_API=false NEXT_PUBLIC_BACKEND_BASE_URL=/ next build", + "build:test": "cross-env NODE_ENV=test NEXT_PUBLIC_TEST_MODE=true next build", "analyze": "cross-env ANALYZE=true next build", "dev": "cross-env PORT=3001 next dev", - "dev:test": "cross-env PORT=3001 NEXT_PUBLIC_TEST_MODE=true next dev", + "dev:test": "cross-env PORT=3001 NODE_ENV=test NEXT_PUBLIC_TEST_MODE=true next dev", "dev:for-real-backend": "cross-env PORT=3001 NEXT_PUBLIC_USE_MOCK_API=false NEXT_PUBLIC_BACKEND_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", "lint:fix": "eslint --fix --ext .ts,.tsx src", "start": "cross-env PORT=3001 next start", - "start:for-real-backend": "cross-env PORT=3001 NEXT_PUBLIC_USE_MOCK_API=false NEXT_PUBLIC_BACKEND_BASE_URL=/ next start", - "start:mock": "cross-env PORT=3001 NEXT_PUBLIC_USE_MOCK_API=true next start", - "start:ci": "cross-env NEXT_PUBLIC_USE_MOCK_API=true NEXT_PUBLIC_TEST_MODE=true PORT=3001 next start", + "start:ci": "cross-env NODE_ENV=test PORT=3001 next start", "cy:open": "cypress open", "cy:run:chrome": "cypress run --browser chrome", "cy:run:firefox": "cypress run --browser firefox", - "test": "jest --watch", - "test:ci": "jest --ci" + "test": "cross-env NODE_ENV=test jest --watch", + "test:ci": "cross-env NODE_ENV=test jest --ci" }, "browserslist": { "production": [ diff --git a/public/mock-public/img/avatar.png b/public/public/img/avatar.png similarity index 100% rename from public/mock-public/img/avatar.png rename to public/public/img/avatar.png diff --git a/public/mock-public/img/demo.png b/public/public/img/demo.png similarity index 100% rename from public/mock-public/img/demo.png rename to public/public/img/demo.png diff --git a/public/mock-public/img/highres.jpg b/public/public/img/highres.jpg similarity index 100% rename from public/mock-public/img/highres.jpg rename to public/public/img/highres.jpg diff --git a/public/mock-public/intro.md b/public/public/intro.md similarity index 70% rename from public/mock-public/intro.md rename to public/public/intro.md index 6752d891a..5455fab79 100644 --- a/public/mock-public/intro.md +++ b/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](/mock-public/screenshot.png) +![HedgeDoc Screenshot](public/screenshot.png) diff --git a/public/mock-public/motd.md b/public/public/motd.md similarity index 100% rename from public/mock-public/motd.md rename to public/public/motd.md diff --git a/public/public/readme.md b/public/public/readme.md new file mode 100644 index 000000000..0eef8d8fd --- /dev/null +++ b/public/public/readme.md @@ -0,0 +1 @@ +This directory should only be used in mock mode. diff --git a/public/mock-public/screenshot.png b/public/public/screenshot.png similarity index 100% rename from public/mock-public/screenshot.png rename to public/public/screenshot.png diff --git a/src/api/common/api-request-builder/api-request-builder.ts b/src/api/common/api-request-builder/api-request-builder.ts index f8cffb219..fac7cf269 100644 --- a/src/api/common/api-request-builder/api-request-builder.ts +++ b/src/api/common/api-request-builder/api-request-builder.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { apiUrl } from '../../../utils/api-url' import deepmerge from 'deepmerge' import { defaultConfig, defaultHeaders } from '../default-config' import { ApiResponse } from '../api-response' @@ -28,7 +27,7 @@ export abstract class ApiRequestBuilder { * @param endpoint The target endpoint without a leading slash. */ constructor(endpoint: string) { - this.targetUrl = apiUrl + endpoint + this.targetUrl = `api/private/${endpoint}` } protected async sendRequestAndVerifyResponse( diff --git a/src/api/common/api-request-builder/delete-api-request-builder.test.ts b/src/api/common/api-request-builder/delete-api-request-builder.test.ts index f94d419c1..28561337d 100644 --- a/src/api/common/api-request-builder/delete-api-request-builder.test.ts +++ b/src/api/common/api-request-builder/delete-api-request-builder.test.ts @@ -19,14 +19,14 @@ describe('DeleteApiRequestBuilder', () => { }) describe('sendRequest without body', () => { it('without headers', async () => { - expectFetch('/api/mock-backend/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/mock-backend/private/test', 204, { + expectFetch('api/private/test', 204, { method: 'DELETE', headers: expectedHeaders }) @@ -36,7 +36,7 @@ describe('DeleteApiRequestBuilder', () => { it('with overriding single header', async () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'false') - expectFetch('/api/mock-backend/private/test', 204, { + expectFetch('api/private/test', 204, { method: 'DELETE', headers: expectedHeaders }) @@ -50,7 +50,7 @@ describe('DeleteApiRequestBuilder', () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'true') expectedHeaders.append('test2', 'false') - expectFetch('/api/mock-backend/private/test', 204, { + expectFetch('api/private/test', 204, { method: 'DELETE', headers: expectedHeaders }) @@ -65,7 +65,7 @@ describe('DeleteApiRequestBuilder', () => { const expectedHeaders = new Headers() expectedHeaders.append('Content-Type', 'application/json') - expectFetch('/api/mock-backend/private/test', 204, { + expectFetch('api/private/test', 204, { method: 'DELETE', headers: expectedHeaders, body: '{"test":true,"foo":"bar"}' @@ -79,7 +79,7 @@ describe('DeleteApiRequestBuilder', () => { }) it('sendRequest with other body', async () => { - expectFetch('/api/mock-backend/private/test', 204, { + expectFetch('api/private/test', 204, { method: 'DELETE', body: 'HedgeDoc' }) @@ -87,13 +87,13 @@ describe('DeleteApiRequestBuilder', () => { }) it('sendRequest with expected status code', async () => { - expectFetch('/api/mock-backend/private/test', 200, { method: 'DELETE' }) + expectFetch('api/private/test', 200, { method: 'DELETE' }) await new DeleteApiRequestBuilder('test').withExpectedStatusCode(200).sendRequest() }) describe('sendRequest with custom options', () => { it('with one option', async () => { - expectFetch('/api/mock-backend/private/test', 204, { + expectFetch('api/private/test', 204, { method: 'DELETE', cache: 'force-cache' }) @@ -105,7 +105,7 @@ describe('DeleteApiRequestBuilder', () => { }) it('overriding single option', async () => { - expectFetch('/api/mock-backend/private/test', 204, { + expectFetch('api/private/test', 204, { method: 'DELETE', cache: 'no-store' }) @@ -120,7 +120,7 @@ describe('DeleteApiRequestBuilder', () => { }) it('with multiple options', async () => { - expectFetch('/api/mock-backend/private/test', 204, { + expectFetch('api/private/test', 204, { method: 'DELETE', cache: 'force-cache', integrity: 'test' @@ -136,7 +136,7 @@ describe('DeleteApiRequestBuilder', () => { describe('sendRequest with custom error map', () => { it('for valid status code', async () => { - expectFetch('/api/mock-backend/private/test', 204, { method: 'DELETE' }) + expectFetch('api/private/test', 204, { method: 'DELETE' }) await new DeleteApiRequestBuilder('test') .withStatusCodeErrorMapping({ 400: 'noooooo', @@ -146,7 +146,7 @@ describe('DeleteApiRequestBuilder', () => { }) it('for invalid status code 1', async () => { - expectFetch('/api/mock-backend/private/test', 400, { method: 'DELETE' }) + expectFetch('api/private/test', 400, { method: 'DELETE' }) const request = new DeleteApiRequestBuilder('test') .withStatusCodeErrorMapping({ 400: 'noooooo', @@ -157,7 +157,7 @@ describe('DeleteApiRequestBuilder', () => { }) it('for invalid status code 2', async () => { - expectFetch('/api/mock-backend/private/test', 401, { method: 'DELETE' }) + expectFetch('api/private/test', 401, { method: 'DELETE' }) const request = new DeleteApiRequestBuilder('test') .withStatusCodeErrorMapping({ 400: 'noooooo', diff --git a/src/api/common/api-request-builder/get-api-request-builder.test.ts b/src/api/common/api-request-builder/get-api-request-builder.test.ts index def80d38c..a37196adf 100644 --- a/src/api/common/api-request-builder/get-api-request-builder.test.ts +++ b/src/api/common/api-request-builder/get-api-request-builder.test.ts @@ -20,14 +20,14 @@ describe('GetApiRequestBuilder', () => { describe('sendRequest', () => { it('without headers', async () => { - expectFetch('/api/mock-backend/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/mock-backend/private/test', 200, { + expectFetch('api/private/test', 200, { method: 'GET', headers: expectedHeaders }) @@ -37,7 +37,7 @@ describe('GetApiRequestBuilder', () => { it('with overriding single header', async () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'false') - expectFetch('/api/mock-backend/private/test', 200, { + expectFetch('api/private/test', 200, { method: 'GET', headers: expectedHeaders }) @@ -51,7 +51,7 @@ describe('GetApiRequestBuilder', () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'true') expectedHeaders.append('test2', 'false') - expectFetch('/api/mock-backend/private/test', 200, { + expectFetch('api/private/test', 200, { method: 'GET', headers: expectedHeaders }) @@ -63,13 +63,13 @@ describe('GetApiRequestBuilder', () => { }) it('sendRequest with expected status code', async () => { - expectFetch('/api/mock-backend/private/test', 200, { method: 'GET' }) + expectFetch('api/private/test', 200, { method: 'GET' }) await new GetApiRequestBuilder('test').withExpectedStatusCode(200).sendRequest() }) describe('sendRequest with custom options', () => { it('with one option', async () => { - expectFetch('/api/mock-backend/private/test', 200, { + expectFetch('api/private/test', 200, { method: 'GET', cache: 'force-cache' }) @@ -81,7 +81,7 @@ describe('GetApiRequestBuilder', () => { }) it('overriding single option', async () => { - expectFetch('/api/mock-backend/private/test', 200, { + expectFetch('api/private/test', 200, { method: 'GET', cache: 'no-store' }) @@ -96,7 +96,7 @@ describe('GetApiRequestBuilder', () => { }) it('with multiple options', async () => { - expectFetch('/api/mock-backend/private/test', 200, { + expectFetch('api/private/test', 200, { method: 'GET', cache: 'force-cache', integrity: 'test' @@ -112,7 +112,7 @@ describe('GetApiRequestBuilder', () => { describe('sendRequest with custom error map', () => { it('for valid status code', async () => { - expectFetch('/api/mock-backend/private/test', 200, { method: 'GET' }) + expectFetch('api/private/test', 200, { method: 'GET' }) await new GetApiRequestBuilder('test') .withStatusCodeErrorMapping({ 400: 'noooooo', @@ -122,7 +122,7 @@ describe('GetApiRequestBuilder', () => { }) it('for invalid status code 1', async () => { - expectFetch('/api/mock-backend/private/test', 400, { method: 'GET' }) + expectFetch('api/private/test', 400, { method: 'GET' }) const request = new GetApiRequestBuilder('test') .withStatusCodeErrorMapping({ 400: 'noooooo', @@ -133,7 +133,7 @@ describe('GetApiRequestBuilder', () => { }) it('for invalid status code 2', async () => { - expectFetch('/api/mock-backend/private/test', 401, { method: 'GET' }) + expectFetch('api/private/test', 401, { method: 'GET' }) const request = new GetApiRequestBuilder('test') .withStatusCodeErrorMapping({ 400: 'noooooo', diff --git a/src/api/common/api-request-builder/post-api-request-builder.test.ts b/src/api/common/api-request-builder/post-api-request-builder.test.ts index ab5623a68..1b5a4a637 100644 --- a/src/api/common/api-request-builder/post-api-request-builder.test.ts +++ b/src/api/common/api-request-builder/post-api-request-builder.test.ts @@ -20,14 +20,14 @@ describe('PostApiRequestBuilder', () => { describe('sendRequest without body', () => { it('without headers', async () => { - expectFetch('/api/mock-backend/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/mock-backend/private/test', 201, { + expectFetch('api/private/test', 201, { method: 'POST', headers: expectedHeaders }) @@ -37,7 +37,7 @@ describe('PostApiRequestBuilder', () => { it('with overriding single header', async () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'false') - expectFetch('/api/mock-backend/private/test', 201, { + expectFetch('api/private/test', 201, { method: 'POST', headers: expectedHeaders }) @@ -51,7 +51,7 @@ describe('PostApiRequestBuilder', () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'true') expectedHeaders.append('test2', 'false') - expectFetch('/api/mock-backend/private/test', 201, { + expectFetch('api/private/test', 201, { method: 'POST', headers: expectedHeaders }) @@ -66,7 +66,7 @@ describe('PostApiRequestBuilder', () => { const expectedHeaders = new Headers() expectedHeaders.append('Content-Type', 'application/json') - expectFetch('/api/mock-backend/private/test', 201, { + expectFetch('api/private/test', 201, { method: 'POST', headers: expectedHeaders, body: '{"test":true,"foo":"bar"}' @@ -80,7 +80,7 @@ describe('PostApiRequestBuilder', () => { }) it('sendRequest with other body', async () => { - expectFetch('/api/mock-backend/private/test', 201, { + expectFetch('api/private/test', 201, { method: 'POST', body: 'HedgeDoc' }) @@ -88,13 +88,13 @@ describe('PostApiRequestBuilder', () => { }) it('sendRequest with expected status code', async () => { - expectFetch('/api/mock-backend/private/test', 200, { method: 'POST' }) + expectFetch('api/private/test', 200, { method: 'POST' }) await new PostApiRequestBuilder('test').withExpectedStatusCode(200).sendRequest() }) describe('sendRequest with custom options', () => { it('with one option', async () => { - expectFetch('/api/mock-backend/private/test', 201, { + expectFetch('api/private/test', 201, { method: 'POST', cache: 'force-cache' }) @@ -106,7 +106,7 @@ describe('PostApiRequestBuilder', () => { }) it('overriding single option', async () => { - expectFetch('/api/mock-backend/private/test', 201, { + expectFetch('api/private/test', 201, { method: 'POST', cache: 'no-store' }) @@ -121,7 +121,7 @@ describe('PostApiRequestBuilder', () => { }) it('with multiple options', async () => { - expectFetch('/api/mock-backend/private/test', 201, { + expectFetch('api/private/test', 201, { method: 'POST', cache: 'force-cache', integrity: 'test' @@ -137,7 +137,7 @@ describe('PostApiRequestBuilder', () => { describe('sendRequest with custom error map', () => { it('for valid status code', async () => { - expectFetch('/api/mock-backend/private/test', 201, { method: 'POST' }) + expectFetch('api/private/test', 201, { method: 'POST' }) await new PostApiRequestBuilder('test') .withStatusCodeErrorMapping({ 400: 'noooooo', @@ -147,7 +147,7 @@ describe('PostApiRequestBuilder', () => { }) it('for invalid status code 1', async () => { - expectFetch('/api/mock-backend/private/test', 400, { method: 'POST' }) + expectFetch('api/private/test', 400, { method: 'POST' }) const request = new PostApiRequestBuilder('test') .withStatusCodeErrorMapping({ 400: 'noooooo', @@ -158,7 +158,7 @@ describe('PostApiRequestBuilder', () => { }) it('for invalid status code 2', async () => { - expectFetch('/api/mock-backend/private/test', 401, { method: 'POST' }) + expectFetch('api/private/test', 401, { method: 'POST' }) const request = new PostApiRequestBuilder('test') .withStatusCodeErrorMapping({ 400: 'noooooo', diff --git a/src/api/common/api-request-builder/put-api-request-builder.test.ts b/src/api/common/api-request-builder/put-api-request-builder.test.ts index 81325bdc3..f653612d7 100644 --- a/src/api/common/api-request-builder/put-api-request-builder.test.ts +++ b/src/api/common/api-request-builder/put-api-request-builder.test.ts @@ -20,14 +20,14 @@ describe('PutApiRequestBuilder', () => { describe('sendRequest without body', () => { it('without headers', async () => { - expectFetch('/api/mock-backend/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/mock-backend/private/test', 200, { + expectFetch('api/private/test', 200, { method: 'PUT', headers: expectedHeaders }) @@ -37,7 +37,7 @@ describe('PutApiRequestBuilder', () => { it('with overriding single header', async () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'false') - expectFetch('/api/mock-backend/private/test', 200, { + expectFetch('api/private/test', 200, { method: 'PUT', headers: expectedHeaders }) @@ -51,7 +51,7 @@ describe('PutApiRequestBuilder', () => { const expectedHeaders = new Headers() expectedHeaders.append('test', 'true') expectedHeaders.append('test2', 'false') - expectFetch('/api/mock-backend/private/test', 200, { + expectFetch('api/private/test', 200, { method: 'PUT', headers: expectedHeaders }) @@ -66,7 +66,7 @@ describe('PutApiRequestBuilder', () => { const expectedHeaders = new Headers() expectedHeaders.append('Content-Type', 'application/json') - expectFetch('/api/mock-backend/private/test', 200, { + expectFetch('api/private/test', 200, { method: 'PUT', headers: expectedHeaders, body: '{"test":true,"foo":"bar"}' @@ -80,7 +80,7 @@ describe('PutApiRequestBuilder', () => { }) it('sendRequest with other body', async () => { - expectFetch('/api/mock-backend/private/test', 200, { + expectFetch('api/private/test', 200, { method: 'PUT', body: 'HedgeDoc' }) @@ -88,13 +88,13 @@ describe('PutApiRequestBuilder', () => { }) it('sendRequest with expected status code', async () => { - expectFetch('/api/mock-backend/private/test', 200, { method: 'PUT' }) + expectFetch('api/private/test', 200, { method: 'PUT' }) await new PutApiRequestBuilder('test').withExpectedStatusCode(200).sendRequest() }) describe('sendRequest with custom options', () => { it('with one option', async () => { - expectFetch('/api/mock-backend/private/test', 200, { + expectFetch('api/private/test', 200, { method: 'PUT', cache: 'force-cache' }) @@ -106,7 +106,7 @@ describe('PutApiRequestBuilder', () => { }) it('overriding single option', async () => { - expectFetch('/api/mock-backend/private/test', 200, { + expectFetch('api/private/test', 200, { method: 'PUT', cache: 'no-store' }) @@ -121,7 +121,7 @@ describe('PutApiRequestBuilder', () => { }) it('with multiple options', async () => { - expectFetch('/api/mock-backend/private/test', 200, { + expectFetch('api/private/test', 200, { method: 'PUT', cache: 'force-cache', integrity: 'test' @@ -137,7 +137,7 @@ describe('PutApiRequestBuilder', () => { describe('sendRequest with custom error map', () => { it('for valid status code', async () => { - expectFetch('/api/mock-backend/private/test', 200, { method: 'PUT' }) + expectFetch('api/private/test', 200, { method: 'PUT' }) await new PutApiRequestBuilder('test') .withStatusCodeErrorMapping({ 400: 'noooooo', @@ -147,7 +147,7 @@ describe('PutApiRequestBuilder', () => { }) it('for invalid status code 1', async () => { - expectFetch('/api/mock-backend/private/test', 400, { method: 'PUT' }) + expectFetch('api/private/test', 400, { method: 'PUT' }) const request = new PutApiRequestBuilder('test') .withStatusCodeErrorMapping({ 400: 'noooooo', @@ -158,7 +158,7 @@ describe('PutApiRequestBuilder', () => { }) it('for invalid status code 2', async () => { - expectFetch('/api/mock-backend/private/test', 401, { method: 'PUT' }) + expectFetch('api/private/test', 401, { method: 'PUT' }) const request = new PutApiRequestBuilder('test') .withStatusCodeErrorMapping({ 400: 'noooooo', diff --git a/src/api/config/types.ts b/src/api/config/types.ts index ce49ab9be..a323cf47c 100644 --- a/src/api/config/types.ts +++ b/src/api/config/types.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -14,7 +14,6 @@ export interface Config { version: BackendVersion plantumlServer?: string maxDocumentLength: number - iframeCommunication: iframeCommunicationConfig } export enum AuthProviderType { @@ -67,11 +66,6 @@ export interface AuthProviderWithoutCustomName { export type AuthProvider = AuthProviderWithCustomName | AuthProviderWithoutCustomName -export interface iframeCommunicationConfig { - editorOrigin: string - rendererOrigin: string -} - export interface BrandingConfig { name?: string logo?: string diff --git a/src/components/application-loader/initializers/fetch-motd.ts b/src/components/application-loader/initializers/fetch-motd.ts index 75a49141f..c43594595 100644 --- a/src/components/application-loader/initializers/fetch-motd.ts +++ b/src/components/application-loader/initializers/fetch-motd.ts @@ -6,7 +6,6 @@ import { setMotd } from '../../../redux/motd/methods' import { Logger } from '../../../utils/logger' -import { customizeAssetsUrl } from '../../../utils/customize-assets-url' import { defaultConfig } from '../../../api/common/default-config' export const MOTD_LOCAL_STORAGE_KEY = 'motd.lastModified' @@ -21,7 +20,7 @@ const log = new Logger('Motd') */ export const fetchMotd = async (): Promise => { const cachedLastModified = window.localStorage.getItem(MOTD_LOCAL_STORAGE_KEY) - const motdUrl = `${customizeAssetsUrl}motd.md` + const motdUrl = `public/motd.md` if (cachedLastModified) { const response = await fetch(motdUrl, { diff --git a/src/components/common/base-url/base-url-context-provider.tsx b/src/components/common/base-url/base-url-context-provider.tsx new file mode 100644 index 000000000..cc8741054 --- /dev/null +++ b/src/components/common/base-url/base-url-context-provider.tsx @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React, { createContext, useState } from 'react' +import type { PropsWithChildren } from 'react' + +export interface BaseUrls { + renderer: string + editor: string +} + +interface BaseUrlContextProviderProps { + baseUrls?: BaseUrls +} + +export const baseUrlContext = createContext(undefined) + +/** + * Provides the given base urls as context content and renders an error message if no base urls have been found. + * + * @param baseUrls The base urls that should be set in the context + * @param children The child components that should receive the context value + */ +export const BaseUrlContextProvider: React.FC> = ({ + baseUrls, + children +}) => { + const [baseUrlState] = useState(() => baseUrls) + + return baseUrlState === undefined ? ( +
HedgeDoc is not configured correctly! Please check the server log.
+ ) : ( + {children} + ) +} diff --git a/src/components/common/user-avatar/user-avatar-for-username.tsx b/src/components/common/user-avatar/user-avatar-for-username.tsx index 30f8a4d83..9098f01da 100644 --- a/src/components/common/user-avatar/user-avatar-for-username.tsx +++ b/src/components/common/user-avatar/user-avatar-for-username.tsx @@ -6,7 +6,6 @@ import React from 'react' import { useAsync } from 'react-use' import { getUser } from '../../../api/users' -import { customizeAssetsUrl } from '../../../utils/customize-assets-url' import type { UserAvatarProps } from './user-avatar' import { UserAvatar } from './user-avatar' import type { UserInfo } from '../../../api/users/types' @@ -34,7 +33,7 @@ export const UserAvatarForUsername: React.FC = ({ us } return { displayName: t('common.guestUser'), - photo: `${customizeAssetsUrl}img/avatar.png`, + photo: `public/img/avatar.png`, username: '' } }, [username, t]) diff --git a/src/components/editor-page/document-bar/share/share-modal.tsx b/src/components/editor-page/document-bar/share/share-modal.tsx index 67bce0f35..4a28cdd8c 100644 --- a/src/components/editor-page/document-bar/share/share-modal.tsx +++ b/src/components/editor-page/document-bar/share/share-modal.tsx @@ -13,7 +13,7 @@ import { CommonModal } from '../../../common/modals/common-modal' import { ShowIf } from '../../../common/show-if/show-if' import { useApplicationState } from '../../../../hooks/common/use-application-state' import { NoteType } from '../../../../redux/note-details/types/note-details' -import { useFrontendBaseUrl } from '../../../../hooks/common/use-frontend-base-url' +import { useBaseUrl } from '../../../../hooks/common/use-base-url' /** * Renders a modal which provides shareable URLs of this note. @@ -25,7 +25,7 @@ export const ShareModal: React.FC = ({ show, onHide }) => useTranslation() const noteFrontmatter = useApplicationState((state) => state.noteDetails.frontmatter) const editorMode = useApplicationState((state) => state.editorConfig.editorMode) - const baseUrl = useFrontendBaseUrl() + const baseUrl = useBaseUrl() const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress) return ( diff --git a/src/components/editor-page/editor-pane/hooks/yjs/use-websocket-url.ts b/src/components/editor-page/editor-pane/hooks/yjs/use-websocket-url.ts index eb0976b64..e71f077d8 100644 --- a/src/components/editor-page/editor-pane/hooks/yjs/use-websocket-url.ts +++ b/src/components/editor-page/editor-pane/hooks/yjs/use-websocket-url.ts @@ -5,9 +5,9 @@ */ import { useMemo } from 'react' -import { backendUrl } from '../../../../../utils/backend-url' import { isMockMode } from '../../../../../utils/test-modes' import { useApplicationState } from '../../../../../hooks/common/use-application-state' +import { useBaseUrl } from '../../../../../hooks/common/use-base-url' const LOCAL_FALLBACK_URL = 'ws://localhost:8080/realtime/' @@ -16,13 +16,14 @@ const LOCAL_FALLBACK_URL = 'ws://localhost:8080/realtime/' */ export const useWebsocketUrl = (): URL => { const noteId = useApplicationState((state) => state.noteDetails.id) + const baseUrl = useBaseUrl() - const baseUrl = useMemo(() => { + const websocketUrl = useMemo(() => { if (isMockMode) { - return process.env.NEXT_PUBLIC_REALTIME_URL ?? LOCAL_FALLBACK_URL + return LOCAL_FALLBACK_URL } try { - const backendBaseUrlParsed = new URL(backendUrl, window.location.toString()) + const backendBaseUrlParsed = new URL(baseUrl, window.location.toString()) backendBaseUrlParsed.protocol = backendBaseUrlParsed.protocol === 'https:' ? 'wss:' : 'ws:' backendBaseUrlParsed.pathname += 'realtime' return backendBaseUrlParsed.toString() @@ -30,11 +31,11 @@ export const useWebsocketUrl = (): URL => { console.error(e) return LOCAL_FALLBACK_URL } - }, []) + }, [baseUrl]) return useMemo(() => { - const url = new URL(baseUrl) + const url = new URL(websocketUrl) url.search = `?noteId=${noteId}` return url - }, [baseUrl, noteId]) + }, [noteId, websocketUrl]) } diff --git a/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker.tsx b/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker.tsx index 577411f35..7ee28b10e 100644 --- a/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker.tsx +++ b/src/components/editor-page/editor-pane/tool-bar/emoji-picker/emoji-picker.tsx @@ -21,7 +21,7 @@ const customEmojis: CustomEmoji[] = ForkAwesomeIcons.map((name) => ({ category: 'ForkAwesome' })) -const EMOJI_DATA_PATH = '/_next/static/js/emoji-data.json' +const EMOJI_DATA_PATH = '_next/static/js/emoji-data.json' const emojiPickerConfig = { customEmoji: customEmojis, diff --git a/src/components/editor-page/render-context/renderer-to-editor-communicator-context-provider.tsx b/src/components/editor-page/render-context/renderer-to-editor-communicator-context-provider.tsx index f1c6c9903..749a960d9 100644 --- a/src/components/editor-page/render-context/renderer-to-editor-communicator-context-provider.tsx +++ b/src/components/editor-page/render-context/renderer-to-editor-communicator-context-provider.tsx @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,7 +8,7 @@ import type { PropsWithChildren } from 'react' import React, { createContext, useContext, useEffect, useMemo } from 'react' import { RendererToEditorCommunicator } from '../../render-page/window-post-message-communicator/renderer-to-editor-communicator' import { CommunicationMessageType } from '../../render-page/window-post-message-communicator/rendering-message' -import { ORIGIN_TYPE, useOriginFromConfig } from './use-origin-from-config' +import { ORIGIN, useBaseUrl } from '../../../hooks/common/use-base-url' const RendererToEditorCommunicatorContext = createContext(undefined) @@ -27,12 +27,13 @@ export const useRendererToEditorCommunicator: () => RendererToEditorCommunicator } export const RendererToEditorCommunicatorContextProvider: React.FC> = ({ children }) => { - const editorOrigin = useOriginFromConfig(ORIGIN_TYPE.EDITOR) + const editorOrigin = useBaseUrl(ORIGIN.EDITOR) const communicator = useMemo(() => new RendererToEditorCommunicator(), []) useEffect(() => { const currentCommunicator = communicator - currentCommunicator.setMessageTarget(window.parent, editorOrigin) + + currentCommunicator.setMessageTarget(window.parent, new URL(editorOrigin).origin) currentCommunicator.registerEventListener() currentCommunicator.enableCommunication() currentCommunicator.sendMessageToOtherSide({ diff --git a/src/components/editor-page/render-context/use-origin-from-config.ts b/src/components/editor-page/render-context/use-origin-from-config.ts deleted file mode 100644 index ea4727508..000000000 --- a/src/components/editor-page/render-context/use-origin-from-config.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { useApplicationState } from '../../../hooks/common/use-application-state' -import { useMemo } from 'react' - -export enum ORIGIN_TYPE { - EDITOR, - RENDERER -} - -/** - * Returns the url origin of the editor or the renderer. - */ -export const useOriginFromConfig = (originType: ORIGIN_TYPE): string => { - const originFromConfig = useApplicationState((state) => - originType === ORIGIN_TYPE.EDITOR - ? state.config.iframeCommunication.editorOrigin - : state.config.iframeCommunication.rendererOrigin - ) - - return useMemo(() => { - return process.env.NEXT_PUBLIC_IGNORE_IFRAME_ORIGIN_CONFIG !== undefined - ? window.location.origin + '/' - : originFromConfig ?? '' - }, [originFromConfig]) -} diff --git a/src/components/editor-page/renderer-pane/hooks/use-force-render-page-url-on-iframe-load-callback.ts b/src/components/editor-page/renderer-pane/hooks/use-force-render-page-url-on-iframe-load-callback.ts index 407ecf53f..dbeb4fa88 100644 --- a/src/components/editor-page/renderer-pane/hooks/use-force-render-page-url-on-iframe-load-callback.ts +++ b/src/components/editor-page/renderer-pane/hooks/use-force-render-page-url-on-iframe-load-callback.ts @@ -7,6 +7,7 @@ import type { RefObject } from 'react' import { useCallback, useEffect, useMemo, useRef } from 'react' import { Logger } from '../../../../utils/logger' +import { ORIGIN, useBaseUrl } from '../../../../hooks/common/use-base-url' const log = new Logger('IframeLoader') @@ -14,15 +15,18 @@ const log = new Logger('IframeLoader') * Generates a callback for an iframe load handler, that enforces a given URL if frame navigates away. * * @param iFrameReference A reference to the iframe react dom element. - * @param rendererOrigin The base url that should be enforced. * @param onNavigateAway An optional callback that is executed when the iframe leaves the enforced URL. */ export const useForceRenderPageUrlOnIframeLoadCallback = ( iFrameReference: RefObject, - rendererOrigin: string, onNavigateAway?: () => void ): (() => void) => { - const forcedUrl = useMemo(() => `${rendererOrigin}render`, [rendererOrigin]) + const rendererBaseUrl = useBaseUrl(ORIGIN.RENDERER) + const forcedUrl = useMemo(() => { + const renderUrl = new URL(rendererBaseUrl) + renderUrl.pathname += 'render' + return renderUrl.toString() + }, [rendererBaseUrl]) const redirectionInProgress = useRef(false) const loadCallback = useCallback(() => { diff --git a/src/components/editor-page/renderer-pane/render-iframe.tsx b/src/components/editor-page/renderer-pane/render-iframe.tsx index df4f93208..dcf80adaf 100644 --- a/src/components/editor-page/renderer-pane/render-iframe.tsx +++ b/src/components/editor-page/renderer-pane/render-iframe.tsx @@ -26,8 +26,8 @@ import { useSendScrollState } from './hooks/use-send-scroll-state' import { Logger } from '../../../utils/logger' import { useEffectOnRenderTypeChange } from './hooks/use-effect-on-render-type-change' import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute' -import { ORIGIN_TYPE, useOriginFromConfig } from '../render-context/use-origin-from-config' import { getGlobalState } from '../../../redux' +import { ORIGIN, useBaseUrl } from '../../../hooks/common/use-base-url' export interface RenderIframeProps extends RendererProps { rendererType: RendererType @@ -64,14 +64,14 @@ export const RenderIframe: React.FC = ({ forcedDarkMode }) => { const frameReference = useRef(null) - const rendererOrigin = useOriginFromConfig(ORIGIN_TYPE.RENDERER) + const rendererBaseUrl = useBaseUrl(ORIGIN.RENDERER) const iframeCommunicator = useEditorToRendererCommunicator() const resetRendererReady = useCallback(() => { log.debug('Reset render status') setRendererStatus(false) }, []) const rendererReady = useIsRendererReady() - const onIframeLoad = useForceRenderPageUrlOnIframeLoadCallback(frameReference, rendererOrigin, resetRendererReady) + const onIframeLoad = useForceRenderPageUrlOnIframeLoadCallback(frameReference, resetRendererReady) const [frameHeight, setFrameHeight] = useState(0) useEffect(() => () => setRendererStatus(false), [iframeCommunicator]) @@ -124,8 +124,9 @@ export const RenderIframe: React.FC = ({ log.error('Load triggered without content window') return } - log.debug(`Set iframecommunicator window with origin ${rendererOrigin ?? 'undefined'}`) - iframeCommunicator.setMessageTarget(otherWindow, rendererOrigin) + const origin = new URL(rendererBaseUrl).origin + log.debug(`Set iframecommunicator window with origin ${origin ?? 'undefined'}`) + iframeCommunicator.setMessageTarget(otherWindow, origin) iframeCommunicator.enableCommunication() iframeCommunicator.sendMessageToOtherSide({ type: CommunicationMessageType.SET_BASE_CONFIGURATION, @@ -135,7 +136,7 @@ export const RenderIframe: React.FC = ({ } }) setRendererStatus(true) - }, [iframeCommunicator, rendererOrigin, rendererType]) + }, [iframeCommunicator, rendererBaseUrl, rendererType]) ) useEffectOnRenderTypeChange(rendererType, onIframeLoad) diff --git a/src/components/intro-page/requests.ts b/src/components/intro-page/requests.ts index 182af33b5..d518705ae 100644 --- a/src/components/intro-page/requests.ts +++ b/src/components/intro-page/requests.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { customizeAssetsUrl } from '../../utils/customize-assets-url' import { defaultConfig } from '../../api/common/default-config' /** @@ -14,7 +13,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(customizeAssetsUrl + 'intro.md', { + const response = await fetch('public/intro.md', { ...defaultConfig, method: 'GET' }) diff --git a/src/components/landing-layout/footer/version-info/version-info-link.tsx b/src/components/landing-layout/footer/version-info/version-info-link.tsx index f4a91fdaf..01fb06d13 100644 --- a/src/components/landing-layout/footer/version-info/version-info-link.tsx +++ b/src/components/landing-layout/footer/version-info/version-info-link.tsx @@ -9,6 +9,7 @@ import { Trans } from 'react-i18next' import { VersionInfoModal } from './version-info-modal' import { cypressId } from '../../../../utils/cypress-attribute' import { useBooleanState } from '../../../../hooks/common/use-boolean-state' +import { Button } from 'react-bootstrap' /** * Renders a link for the version info and the {@link VersionInfoModal}. @@ -18,9 +19,14 @@ export const VersionInfoLink: React.FC = () => { return ( - + ) diff --git a/src/components/landing-layout/navigation/sign-in-button.tsx b/src/components/landing-layout/navigation/sign-in-button.tsx index d7d47d8b5..879fa3f57 100644 --- a/src/components/landing-layout/navigation/sign-in-button.tsx +++ b/src/components/landing-layout/navigation/sign-in-button.tsx @@ -34,7 +34,7 @@ export const SignInButton: React.FC = ({ variant, ...props }) const metadata = getOneClickProviderMetadata(oneClickProviders[0]) return metadata.url } - return '/login' + return 'login' }, [authProviders]) return ( diff --git a/src/components/layout/base-head.tsx b/src/components/layout/base-head.tsx index f4fe66f46..b7ce7ed94 100644 --- a/src/components/layout/base-head.tsx +++ b/src/components/layout/base-head.tsx @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,17 +8,19 @@ import React from 'react' import Head from 'next/head' import { useAppTitle } from '../../hooks/common/use-app-title' import { FavIcon } from './fav-icon' +import { useBaseUrl } from '../../hooks/common/use-base-url' /** * Sets basic browser meta tags. */ export const BaseHead: React.FC = () => { const appTitle = useAppTitle() - + const baseUrl = useBaseUrl() return ( {appTitle} + ) diff --git a/src/components/layout/fav-icon.tsx b/src/components/layout/fav-icon.tsx index a6123bf89..ef39709a1 100644 --- a/src/components/layout/fav-icon.tsx +++ b/src/components/layout/fav-icon.tsx @@ -12,17 +12,17 @@ import React, { Fragment } from 'react' export const FavIcon: React.FC = () => { return ( - - - - - - + + + + + + - + ) diff --git a/src/components/login-page/auth/utils/get-one-click-provider-metadata.ts b/src/components/login-page/auth/utils/get-one-click-provider-metadata.ts index edb48294e..266e30aec 100644 --- a/src/components/login-page/auth/utils/get-one-click-provider-metadata.ts +++ b/src/components/login-page/auth/utils/get-one-click-provider-metadata.ts @@ -7,7 +7,6 @@ import type { AuthProvider } from '../../../../api/config/types' import { AuthProviderType } from '../../../../api/config/types' import type { IconName } from '../../../common/fork-awesome/types' import styles from '../via-one-click.module.scss' -import { backendUrl } from '../../../../utils/backend-url' import { Logger } from '../../../../utils/logger' export interface OneClickMetadata { @@ -18,7 +17,7 @@ export interface OneClickMetadata { } const getBackendAuthUrl = (providerIdentifer: string): string => { - return `${backendUrl}auth/${providerIdentifer}` + return `auth/${providerIdentifer}` } const logger = new Logger('GetOneClickProviderMetadata') diff --git a/src/components/profile-page/account-management/profile-account-management.tsx b/src/components/profile-page/account-management/profile-account-management.tsx index ecb23f08e..636efa757 100644 --- a/src/components/profile-page/account-management/profile-account-management.tsx +++ b/src/components/profile-page/account-management/profile-account-management.tsx @@ -9,7 +9,6 @@ import { Button, Card } from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon' import { AccountDeletionModal } from './account-deletion-modal' -import { apiUrl } from '../../../utils/api-url' import { useBooleanState } from '../../../hooks/common/use-boolean-state' /** @@ -26,7 +25,7 @@ export const ProfileAccountManagement: React.FC = () => { - diff --git a/src/components/render-page/iframe-markdown-renderer.tsx b/src/components/render-page/iframe-markdown-renderer.tsx index ab905f447..ad24506d0 100644 --- a/src/components/render-page/iframe-markdown-renderer.tsx +++ b/src/components/render-page/iframe-markdown-renderer.tsx @@ -128,7 +128,9 @@ export const IframeMarkdownRenderer: React.FC = () => { ) if (!baseConfiguration) { - return null + return ( + This is the render endpoint. If you can read this text then please check your HedgeDoc configuration. + ) } switch (baseConfiguration.rendererType) { diff --git a/src/handler-utils/respond-to-matching-request.ts b/src/handler-utils/respond-to-matching-request.ts index a8a654637..295439e28 100644 --- a/src/handler-utils/respond-to-matching-request.ts +++ b/src/handler-utils/respond-to-matching-request.ts @@ -5,6 +5,7 @@ */ import type { NextApiRequest, NextApiResponse } from 'next' +import { isMockMode } from '../utils/test-modes' export enum HttpMethod { GET = 'GET', @@ -31,6 +32,10 @@ export const respondToMatchingRequest = ( response: T, statusCode = 200 ): boolean => { + if (!isMockMode) { + res.status(404).send('Mock API is disabled') + return false + } if (method !== req.method) { res.status(405).send('Method not allowed') return false diff --git a/src/hooks/common/use-base-url.tsx b/src/hooks/common/use-base-url.tsx new file mode 100644 index 000000000..45da10bb0 --- /dev/null +++ b/src/hooks/common/use-base-url.tsx @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { useContext, useMemo } from 'react' +import { useRouter } from 'next/router' +import { baseUrlContext } from '../../components/common/base-url/base-url-context-provider' + +export enum ORIGIN { + EDITOR, + RENDERER, + CURRENT_PAGE +} + +/** + * Provides the base urls for the editor and the renderer. + */ +export const useBaseUrl = (origin = ORIGIN.CURRENT_PAGE): string => { + const baseUrls = useContext(baseUrlContext) + if (!baseUrls) { + throw new Error('No base url context received. Did you forget to use the provider component?') + } + + const router = useRouter() + + return useMemo(() => { + return (router.route === '/render' && origin === ORIGIN.CURRENT_PAGE) || origin === ORIGIN.RENDERER + ? baseUrls.renderer + : baseUrls.editor + }, [origin, baseUrls.renderer, baseUrls.editor, router.route]) +} diff --git a/src/hooks/common/use-frontend-base-url.ts b/src/hooks/common/use-frontend-base-url.ts deleted file mode 100644 index 53e077ae2..000000000 --- a/src/hooks/common/use-frontend-base-url.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { useRouter } from 'next/router' -import { useMemo } from 'react' - -/** - * Retrieves the frontend base url either from an environment variable or from the window location itself. - * - * @return The base url of the frontend. - */ -export const useFrontendBaseUrl = (): string => { - const { asPath } = useRouter() - return useMemo(() => { - return process.env.NEXT_PUBLIC_FRONTEND_ASSETS_URL || window.location.toString().replace(asPath, '') + '/' - }, [asPath]) -} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 9fc8b3296..d602ba637 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -3,33 +3,58 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import type { AppProps } from 'next/app' +import type { AppInitialProps, AppProps } from 'next/app' import { ErrorBoundary } from '../components/error-boundary/error-boundary' import { ApplicationLoader } from '../components/application-loader/application-loader' import '../../global-styles/dark.scss' import '../../global-styles/index.scss' -import type { NextPage } from 'next' import { BaseHead } from '../components/layout/base-head' import { StoreProvider } from '../redux/store-provider' import { UiNotificationBoundary } from '../components/notifications/ui-notification-boundary' +import { ExpectedOriginBoundary } from '../utils/uri-origin-boundary' +import React from 'react' +import { BaseUrlContextProvider } from '../components/common/base-url/base-url-context-provider' +import type { BaseUrls } from '../components/common/base-url/base-url-context-provider' +import { BaseUrlFromEnvExtractor } from '../utils/base-url-from-env-extractor' + +interface AppPageProps { + baseUrls: BaseUrls | undefined +} /** * The actual hedgedoc next js app. * Provides necessary wrapper components to every page. */ -const HedgeDocApp: NextPage = ({ Component, pageProps }: AppProps) => { +function HedgeDocApp({ Component, pageProps }: AppProps) { return ( - - - - - - - - - - + + + + + + + + + + + + + + ) } +const baseUrlFromEnvExtractor: BaseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() + +HedgeDocApp.getInitialProps = (): AppInitialProps => { + const baseUrls = baseUrlFromEnvExtractor.extractBaseUrls().orElse(undefined) + + return { + pageProps: { + baseUrls + } + } +} + +// noinspection JSUnusedGlobalSymbols export default HedgeDocApp diff --git a/src/pages/api/mock-backend/private/notes/features/index.ts b/src/pages/api/mock-backend/private/notes/features/index.ts deleted file mode 100644 index 21cd99849..000000000 --- a/src/pages/api/mock-backend/private/notes/features/index.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { NextApiRequest, NextApiResponse } from 'next' -import { HttpMethod, respondToMatchingRequest } from '../../../../../../handler-utils/respond-to-matching-request' -import type { Note } from '../../../../../../api/notes/types' - -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](/mock-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, - viewCount: 0, - updatedAt: '2021-04-24T09:27:51.000Z', - createdAt: '2021-04-24T09:27:51.000Z', - updateUsername: null, - primaryAddress: 'features', - editedBy: [], - title: 'Features', - tags: ['hedgedoc', 'demo', 'react'], - description: 'Many features, such wow!', - aliases: [ - { - name: 'features', - primaryAlias: true, - noteId: 'exampleId' - } - ], - permissions: { - owner: 'tilman', - sharedToUsers: [ - { - username: 'molly', - canEdit: true - } - ], - sharedToGroups: [ - { - groupName: '_LOGGED_IN', - canEdit: false - } - ] - } - }, - editedByAtPosition: [] - }) -} - -export default handler diff --git a/src/pages/api/mock-backend/private/config.ts b/src/pages/api/private/config.ts similarity index 76% rename from src/pages/api/mock-backend/private/config.ts rename to src/pages/api/private/config.ts index 014fa2d4d..d1ffc487d 100644 --- a/src/pages/api/mock-backend/private/config.ts +++ b/src/pages/api/private/config.ts @@ -4,9 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import type { NextApiRequest, NextApiResponse } from 'next' -import type { Config } from '../../../../api/config/types' -import { AuthProviderType } from '../../../../api/config/types' -import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request' +import type { Config } from '../../../api/config/types' +import { AuthProviderType } from '../../../api/config/types' +import { HttpMethod, respondToMatchingRequest } from '../../../handler-utils/respond-to-matching-request' const handler = (req: NextApiRequest, res: NextApiResponse) => { respondToMatchingRequest(HttpMethod.GET, req, res, { @@ -54,7 +54,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse) => { ], branding: { name: 'DEMO Corp', - logo: '/mock-public/img/demo.png' + logo: 'public/img/demo.png' }, useImageProxy: false, specialUrls: { @@ -69,11 +69,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse) => { commit: 'mock' }, plantumlServer: 'https://www.plantuml.com/plantuml', - maxDocumentLength: 1000000, - iframeCommunication: { - editorOrigin: process.env.NEXT_PUBLIC_EDITOR_ORIGIN ?? 'http://localhost:3001/', - rendererOrigin: process.env.NEXT_PUBLIC_RENDERER_ORIGIN ?? 'http://127.0.0.1:3001/' - } + maxDocumentLength: 1000000 }) } diff --git a/src/pages/api/mock-backend/private/groups/_EVERYONE.ts b/src/pages/api/private/groups/_EVERYONE.ts similarity index 81% rename from src/pages/api/mock-backend/private/groups/_EVERYONE.ts rename to src/pages/api/private/groups/_EVERYONE.ts index 7e9dcbbe4..2d782adc9 100644 --- a/src/pages/api/mock-backend/private/groups/_EVERYONE.ts +++ b/src/pages/api/private/groups/_EVERYONE.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import type { NextApiRequest, NextApiResponse } from 'next' -import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request' -import type { GroupInfo } from '../../../../../api/group/types' +import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request' +import type { GroupInfo } from '../../../../api/group/types' const handler = (req: NextApiRequest, res: NextApiResponse) => { respondToMatchingRequest(HttpMethod.GET, req, res, { diff --git a/src/pages/api/mock-backend/private/groups/_LOGGED_IN.ts b/src/pages/api/private/groups/_LOGGED_IN.ts similarity index 81% rename from src/pages/api/mock-backend/private/groups/_LOGGED_IN.ts rename to src/pages/api/private/groups/_LOGGED_IN.ts index 3b4d30890..dd485375c 100644 --- a/src/pages/api/mock-backend/private/groups/_LOGGED_IN.ts +++ b/src/pages/api/private/groups/_LOGGED_IN.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import type { NextApiRequest, NextApiResponse } from 'next' -import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request' -import type { GroupInfo } from '../../../../../api/group/types' +import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request' +import type { GroupInfo } from '../../../../api/group/types' const handler = (req: NextApiRequest, res: NextApiResponse) => { respondToMatchingRequest(HttpMethod.GET, req, res, { diff --git a/src/pages/api/mock-backend/private/groups/hedgedoc-devs.ts b/src/pages/api/private/groups/hedgedoc-devs.ts similarity index 81% rename from src/pages/api/mock-backend/private/groups/hedgedoc-devs.ts rename to src/pages/api/private/groups/hedgedoc-devs.ts index cde391019..9a3dc9543 100644 --- a/src/pages/api/mock-backend/private/groups/hedgedoc-devs.ts +++ b/src/pages/api/private/groups/hedgedoc-devs.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import type { NextApiRequest, NextApiResponse } from 'next' -import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request' -import type { GroupInfo } from '../../../../../api/group/types' +import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request' +import type { GroupInfo } from '../../../../api/group/types' const handler = (req: NextApiRequest, res: NextApiResponse) => { respondToMatchingRequest(HttpMethod.GET, req, res, { diff --git a/src/pages/api/mock-backend/private/me/history.ts b/src/pages/api/private/me/history.ts similarity index 91% rename from src/pages/api/mock-backend/private/me/history.ts rename to src/pages/api/private/me/history.ts index 51e29fd7b..7e821acc1 100644 --- a/src/pages/api/mock-backend/private/me/history.ts +++ b/src/pages/api/private/me/history.ts @@ -5,8 +5,8 @@ */ import type { NextApiRequest, NextApiResponse } from 'next' -import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request' -import type { HistoryEntry } from '../../../../../api/history/types' +import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request' +import type { HistoryEntry } from '../../../../api/history/types' const handler = (req: NextApiRequest, res: NextApiResponse) => { respondToMatchingRequest(HttpMethod.GET, req, res, [ diff --git a/src/pages/api/mock-backend/private/me/index.ts b/src/pages/api/private/me/index.ts similarity index 77% rename from src/pages/api/mock-backend/private/me/index.ts rename to src/pages/api/private/me/index.ts index e0892e06e..a9dc8b545 100644 --- a/src/pages/api/mock-backend/private/me/index.ts +++ b/src/pages/api/private/me/index.ts @@ -5,13 +5,13 @@ */ import type { NextApiRequest, NextApiResponse } from 'next' -import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request' -import type { LoginUserInfo } from '../../../../../api/me/types' +import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request' +import type { LoginUserInfo } from '../../../../api/me/types' const handler = (req: NextApiRequest, res: NextApiResponse): void => { respondToMatchingRequest(HttpMethod.GET, req, res, { username: 'mock', - photo: '/mock-public/img/avatar.png', + photo: 'public/img/avatar.png', displayName: 'Mock User', authProvider: 'local', email: 'mock@hedgedoc.test' diff --git a/src/pages/api/mock-backend/private/me/media.ts b/src/pages/api/private/me/media.ts similarity index 88% rename from src/pages/api/mock-backend/private/me/media.ts rename to src/pages/api/private/me/media.ts index 8b5c078c6..64de2ae9f 100644 --- a/src/pages/api/mock-backend/private/me/media.ts +++ b/src/pages/api/private/me/media.ts @@ -5,8 +5,8 @@ */ import type { NextApiRequest, NextApiResponse } from 'next' -import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request' -import type { MediaUpload } from '../../../../../api/media/types' +import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request' +import type { MediaUpload } from '../../../../api/media/types' /* * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) diff --git a/src/pages/api/mock-backend/private/media.ts b/src/pages/api/private/media.ts similarity index 68% rename from src/pages/api/mock-backend/private/media.ts rename to src/pages/api/private/media.ts index 1c2910c91..34ab2bd05 100644 --- a/src/pages/api/mock-backend/private/media.ts +++ b/src/pages/api/private/media.ts @@ -5,9 +5,9 @@ */ import type { NextApiRequest, NextApiResponse } from 'next' -import type { MediaUpload } from '../../../../api/media/types' -import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request' -import { isMockMode, isTestMode } from '../../../../utils/test-modes' +import type { MediaUpload } from '../../../api/media/types' +import { HttpMethod, respondToMatchingRequest } from '../../../handler-utils/respond-to-matching-request' +import { isMockMode, isTestMode } from '../../../utils/test-modes' const handler = async (req: NextApiRequest, res: NextApiResponse): Promise => { if (isMockMode && !isTestMode) { @@ -21,7 +21,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse): Promise req, res, { - url: '/mock-public/img/avatar.png', + url: 'public/img/avatar.png', noteId: null, username: 'test', createdAt: '2022-02-27T21:54:23.856Z' diff --git a/src/pages/api/private/notes/features/index.ts b/src/pages/api/private/notes/features/index.ts new file mode 100644 index 000000000..c0f6783f7 --- /dev/null +++ b/src/pages/api/private/notes/features/index.ts @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { NextApiRequest, NextApiResponse } from 'next' +import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request' +import type { Note } from '../../../../../api/notes/types' + +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', + metadata: { + id: 'exampleId', + version: 2, + viewCount: 0, + updatedAt: '2021-04-24T09:27:51.000Z', + createdAt: '2021-04-24T09:27:51.000Z', + updateUsername: null, + primaryAddress: 'features', + editedBy: [], + title: 'Features', + tags: ['hedgedoc', 'demo', 'react'], + description: 'Many features, such wow!', + aliases: [ + { + name: 'features', + primaryAlias: true, + noteId: 'exampleId' + } + ], + permissions: { + owner: 'tilman', + sharedToUsers: [ + { + username: 'molly', + canEdit: true + } + ], + sharedToGroups: [ + { + groupName: '_LOGGED_IN', + canEdit: false + } + ] + } + }, + editedByAtPosition: [] + }) +} + +export default handler diff --git a/src/pages/api/mock-backend/private/notes/features/revisions/0.ts b/src/pages/api/private/notes/features/revisions/0.ts similarity index 97% rename from src/pages/api/mock-backend/private/notes/features/revisions/0.ts rename to src/pages/api/private/notes/features/revisions/0.ts index 8f0eabd1b..ab6c0bf48 100644 --- a/src/pages/api/mock-backend/private/notes/features/revisions/0.ts +++ b/src/pages/api/private/notes/features/revisions/0.ts @@ -5,8 +5,8 @@ */ import type { NextApiRequest, NextApiResponse } from 'next' -import { HttpMethod, respondToMatchingRequest } from '../../../../../../../handler-utils/respond-to-matching-request' -import type { RevisionDetails } from '../../../../../../../api/revisions/types' +import { HttpMethod, respondToMatchingRequest } from '../../../../../../handler-utils/respond-to-matching-request' +import type { RevisionDetails } from '../../../../../../api/revisions/types' const handler = (req: NextApiRequest, res: NextApiResponse): void => { respondToMatchingRequest(HttpMethod.GET, req, res, { diff --git a/src/pages/api/mock-backend/private/notes/features/revisions/1.ts b/src/pages/api/private/notes/features/revisions/1.ts similarity index 97% rename from src/pages/api/mock-backend/private/notes/features/revisions/1.ts rename to src/pages/api/private/notes/features/revisions/1.ts index 08c7cf68a..1d22b9e28 100644 --- a/src/pages/api/mock-backend/private/notes/features/revisions/1.ts +++ b/src/pages/api/private/notes/features/revisions/1.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import type { NextApiRequest, NextApiResponse } from 'next' -import { HttpMethod, respondToMatchingRequest } from '../../../../../../../handler-utils/respond-to-matching-request' -import type { RevisionDetails } from '../../../../../../../api/revisions/types' +import { HttpMethod, respondToMatchingRequest } from '../../../../../../handler-utils/respond-to-matching-request' +import type { RevisionDetails } from '../../../../../../api/revisions/types' const handler = (req: NextApiRequest, res: NextApiResponse): void => { respondToMatchingRequest(HttpMethod.GET, req, res, { diff --git a/src/pages/api/mock-backend/private/notes/features/revisions/index.ts b/src/pages/api/private/notes/features/revisions/index.ts similarity index 84% rename from src/pages/api/mock-backend/private/notes/features/revisions/index.ts rename to src/pages/api/private/notes/features/revisions/index.ts index 0fb084ad5..69f2189ab 100644 --- a/src/pages/api/mock-backend/private/notes/features/revisions/index.ts +++ b/src/pages/api/private/notes/features/revisions/index.ts @@ -5,8 +5,8 @@ */ import type { NextApiRequest, NextApiResponse } from 'next' -import { HttpMethod, respondToMatchingRequest } from '../../../../../../../handler-utils/respond-to-matching-request' -import type { RevisionMetadata } from '../../../../../../../api/revisions/types' +import { HttpMethod, respondToMatchingRequest } from '../../../../../../handler-utils/respond-to-matching-request' +import type { RevisionMetadata } from '../../../../../../api/revisions/types' const handler = (req: NextApiRequest, res: NextApiResponse): void => { respondToMatchingRequest(HttpMethod.GET, req, res, [ diff --git a/src/pages/api/mock-backend/private/notes/index.ts b/src/pages/api/private/notes/index.ts similarity index 92% rename from src/pages/api/mock-backend/private/notes/index.ts rename to src/pages/api/private/notes/index.ts index 835995fbe..0eb8e9090 100644 --- a/src/pages/api/mock-backend/private/notes/index.ts +++ b/src/pages/api/private/notes/index.ts @@ -5,8 +5,8 @@ */ import type { NextApiRequest, NextApiResponse } from 'next' -import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request' -import type { Note } from '../../../../../api/notes/types' +import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request' +import type { Note } from '../../../../api/notes/types' const handler = (req: NextApiRequest, res: NextApiResponse): void => { respondToMatchingRequest( diff --git a/src/pages/api/mock-backend/private/notes/slide-example/index.ts b/src/pages/api/private/notes/slide-example/index.ts similarity index 98% rename from src/pages/api/mock-backend/private/notes/slide-example/index.ts rename to src/pages/api/private/notes/slide-example/index.ts index ccc486df6..0b2514504 100644 --- a/src/pages/api/mock-backend/private/notes/slide-example/index.ts +++ b/src/pages/api/private/notes/slide-example/index.ts @@ -5,8 +5,8 @@ */ import type { NextApiRequest, NextApiResponse } from 'next' -import { HttpMethod, respondToMatchingRequest } from '../../../../../../handler-utils/respond-to-matching-request' -import type { Note } from '../../../../../../api/notes/types' +import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request' +import type { Note } from '../../../../../api/notes/types' const handler = (req: NextApiRequest, res: NextApiResponse): void => { respondToMatchingRequest(HttpMethod.GET, req, res, { diff --git a/src/pages/api/mock-backend/private/tokens.ts b/src/pages/api/private/tokens.ts similarity index 80% rename from src/pages/api/mock-backend/private/tokens.ts rename to src/pages/api/private/tokens.ts index cfbd2ac7c..de91a28e4 100644 --- a/src/pages/api/mock-backend/private/tokens.ts +++ b/src/pages/api/private/tokens.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import type { NextApiRequest, NextApiResponse } from 'next' -import type { AccessToken } from '../../../../api/tokens/types' -import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request' +import type { AccessToken } from '../../../api/tokens/types' +import { HttpMethod, respondToMatchingRequest } from '../../../handler-utils/respond-to-matching-request' const handler = (req: NextApiRequest, res: NextApiResponse) => { respondToMatchingRequest(HttpMethod.GET, req, res, [ diff --git a/src/pages/api/mock-backend/private/users/erik.ts b/src/pages/api/private/users/erik.ts similarity index 75% rename from src/pages/api/mock-backend/private/users/erik.ts rename to src/pages/api/private/users/erik.ts index 7345de6d7..2f71aaf6a 100644 --- a/src/pages/api/mock-backend/private/users/erik.ts +++ b/src/pages/api/private/users/erik.ts @@ -4,14 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import type { NextApiRequest, NextApiResponse } from 'next' -import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request' -import type { UserInfo } from '../../../../../api/users/types' +import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request' +import type { UserInfo } from '../../../../api/users/types' const handler = (req: NextApiRequest, res: NextApiResponse): void => { respondToMatchingRequest(HttpMethod.GET, req, res, { username: 'erik', displayName: 'Erik', - photo: '/mock-public/img/avatar.png' + photo: 'public/img/avatar.png' }) } diff --git a/src/pages/api/mock-backend/private/users/molly.ts b/src/pages/api/private/users/molly.ts similarity index 75% rename from src/pages/api/mock-backend/private/users/molly.ts rename to src/pages/api/private/users/molly.ts index c5538a92f..17cbe39f7 100644 --- a/src/pages/api/mock-backend/private/users/molly.ts +++ b/src/pages/api/private/users/molly.ts @@ -5,14 +5,14 @@ */ import type { NextApiRequest, NextApiResponse } from 'next' -import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request' -import type { UserInfo } from '../../../../../api/users/types' +import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request' +import type { UserInfo } from '../../../../api/users/types' const handler = (req: NextApiRequest, res: NextApiResponse): void => { respondToMatchingRequest(HttpMethod.GET, req, res, { username: 'molly', displayName: 'Molly', - photo: '/mock-public/img/avatar.png' + photo: 'public/img/avatar.png' }) } diff --git a/src/pages/api/mock-backend/private/users/tilman.ts b/src/pages/api/private/users/tilman.ts similarity index 75% rename from src/pages/api/mock-backend/private/users/tilman.ts rename to src/pages/api/private/users/tilman.ts index a85e7af71..57141828c 100644 --- a/src/pages/api/mock-backend/private/users/tilman.ts +++ b/src/pages/api/private/users/tilman.ts @@ -4,14 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import type { NextApiRequest, NextApiResponse } from 'next' -import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request' -import type { UserInfo } from '../../../../../api/users/types' +import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request' +import type { UserInfo } from '../../../../api/users/types' const handler = (req: NextApiRequest, res: NextApiResponse): void => { respondToMatchingRequest(HttpMethod.GET, req, res, { username: 'tilman', displayName: 'Tilman', - photo: '/mock-public/img/avatar.png' + photo: 'public/img/avatar.png' }) } diff --git a/src/redux/config/reducers.ts b/src/redux/config/reducers.ts index cc3a3d78a..f5c0cf238 100644 --- a/src/redux/config/reducers.ts +++ b/src/redux/config/reducers.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -29,11 +29,7 @@ export const initialState: Config = { patch: 0 }, plantumlServer: undefined, - maxDocumentLength: 0, - iframeCommunication: { - editorOrigin: '', - rendererOrigin: '' - } + maxDocumentLength: 0 } export const ConfigReducer: Reducer = (state: Config = initialState, action: ConfigActions) => { diff --git a/src/utils/api-url.ts b/src/utils/api-url.ts deleted file mode 100644 index 2afe7d12d..000000000 --- a/src/utils/api-url.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { isMockMode } from './test-modes' -import { backendUrl } from './backend-url' - -/** - * Generates the url to the api. - */ -export const apiUrl = isMockMode ? `/api/mock-backend/private/` : `${backendUrl}api/private/` diff --git a/src/utils/backend-url.ts b/src/utils/backend-url.ts deleted file mode 100644 index 376f38533..000000000 --- a/src/utils/backend-url.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { isMockMode } from './test-modes' - -/** - * Generates the backend URL from the environment variable `NEXT_PUBLIC_BACKEND_BASE_URL` or the mock default if mock mode is activated. - * - * @throws Error if the environment variable is unset or doesn't end with "/" - * @return the backend url that should be used in the app - */ -const generateBackendUrl = (): string => { - if (!isMockMode) { - const backendUrl = process.env.NEXT_PUBLIC_BACKEND_BASE_URL - if (backendUrl === undefined) { - throw new Error('NEXT_PUBLIC_BACKEND_BASE_URL is unset and mock mode is disabled') - } else if (!backendUrl.endsWith('/')) { - throw new Error("NEXT_PUBLIC_BACKEND_BASE_URL must end with an '/'") - } else { - return backendUrl - } - } else { - return '/' - } -} - -export const backendUrl = generateBackendUrl() diff --git a/src/utils/base-url-from-env-extractor.test.ts b/src/utils/base-url-from-env-extractor.test.ts new file mode 100644 index 000000000..02e97cbab --- /dev/null +++ b/src/utils/base-url-from-env-extractor.test.ts @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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_RENDERER_BASE_URL = 'https://renderer.example.org/' + const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() + 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 no var is set', () => { + process.env.HD_EDITOR_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_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_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' + 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 ending with a slash", () => { + process.env.HD_EDITOR_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() + }) + + 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/' + delete process.env.HD_RENDERER_BASE_URL + const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() + const result = baseUrlFromEnvExtractor.extractBaseUrls() + expect(result.isPresent()).toBeTruthy() + expect(result.get()).toStrictEqual({ + renderer: 'https://editor.example.org/', + editor: 'https://editor.example.org/' + }) + }) +}) diff --git a/src/utils/base-url-from-env-extractor.ts b/src/utils/base-url-from-env-extractor.ts new file mode 100644 index 000000000..7ea430dd0 --- /dev/null +++ b/src/utils/base-url-from-env-extractor.ts @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Optional } from '@mrdrogdrog/optional' +import type { BaseUrls } from '../components/common/base-url/base-url-context-provider' +import { Logger } from './logger' +import { isTestMode } from './test-modes' + +/** + * Extracts the editor and renderer base urls from the environment variables. + */ +export class BaseUrlFromEnvExtractor { + private baseUrls: Optional | undefined + private logger = new Logger('Base URL Configuration') + + private extractUrlFromEnvVar(envVarName: string, envVarValue: string | undefined): Optional { + return Optional.ofNullable(envVarValue) + .filter((value) => { + const endsWithSlash = value.endsWith('/') + if (!endsWithSlash) { + this.logger.error(`${envVarName} must end with an '/'`) + } + return endsWithSlash + }) + .map((value) => { + try { + return new URL(value) + } catch (error) { + return null + } + }) + } + + private extractEditorBaseUrlFromEnv(): Optional { + const envValue = this.extractUrlFromEnvVar('HD_EDITOR_BASE_URL', process.env.HD_EDITOR_BASE_URL) + if (envValue.isEmpty()) { + this.logger.error("HD_EDITOR_BASE_URL isn't a valid URL!") + } + return envValue + } + + private extractRendererBaseUrlFromEnv(editorBaseUrl: URL): Optional { + if (isTestMode) { + this.logger.info('Test mode activated. Using editor base url for renderer.') + return Optional.of(editorBaseUrl) + } + + if (!process.env.HD_RENDERER_BASE_URL) { + this.logger.info('HD_RENDERER_BASE_URL is unset. Using editor base url for renderer.') + return Optional.of(editorBaseUrl) + } + + return this.extractUrlFromEnvVar('HD_RENDERER_BASE_URL', process.env.HD_RENDERER_BASE_URL) + } + + private renewBaseUrls(): void { + this.baseUrls = this.extractEditorBaseUrlFromEnv().flatMap((editorBaseUrl) => + this.extractRendererBaseUrlFromEnv(editorBaseUrl).map((rendererBaseUrl) => { + return { + editor: editorBaseUrl.toString(), + renderer: rendererBaseUrl.toString() + } + }) + ) + this.baseUrls.ifPresent((urls) => { + this.logger.info('Editor base URL', urls.editor.toString()) + this.logger.info('Renderer base URL', urls.renderer.toString()) + }) + } + + private isEnvironmentExtractDone(): boolean { + return this.baseUrls !== undefined + } + + /** + * Extracts the editor and renderer base urls from the environment variables. + * + * @return An {@link Optional} with the base urls. + */ + public extractBaseUrls(): Optional { + if (!this.isEnvironmentExtractDone()) { + this.renewBaseUrls() + } + return Optional.ofNullable(this.baseUrls).flatMap((value) => value) + } +} diff --git a/src/utils/customize-assets-url.ts b/src/utils/customize-assets-url.ts deleted file mode 100644 index 4a8c867f5..000000000 --- a/src/utils/customize-assets-url.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { backendUrl } from './backend-url' -import { isMockMode } from './test-modes' - -/** - * Generates the url to the assets. - */ -export const customizeAssetsUrl = isMockMode - ? `/mock-public/` - : process.env.NEXT_PUBLIC_CUSTOMIZE_ASSETS_URL || `${backendUrl}public/` diff --git a/src/utils/is-positive-answer.ts b/src/utils/is-positive-answer.ts deleted file mode 100644 index 710532d30..000000000 --- a/src/utils/is-positive-answer.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/** - * Checks if the given string is a positive answer (yes, true or 1). - * - * @param value The value to check - */ -export const isPositiveAnswer = (value: string) => { - const lowerValue = value.toLowerCase() - return lowerValue === 'yes' || lowerValue === '1' || lowerValue === 'true' -} diff --git a/src/utils/test-modes.js b/src/utils/test-modes.js new file mode 100644 index 000000000..bc411835d --- /dev/null +++ b/src/utils/test-modes.js @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * This file is intentionally a js and not a ts file because it is used in `next.config.js` + */ + +/** + * Checks if the given string is a positive answer (yes, true or 1). + * + * @param {string} value The value to check + * @return {boolean} {@code true} if the value describes a positive answer string + */ +const isPositiveAnswer = (value) => { + const lowerValue = value.toLowerCase() + return lowerValue === 'yes' || lowerValue === '1' || lowerValue === 'true' +} + +/** + * Defines if the current runtime is built in e2e test mode. + * @type boolean + */ +const isTestMode = !!process.env.NEXT_PUBLIC_TEST_MODE && isPositiveAnswer(process.env.NEXT_PUBLIC_TEST_MODE) + +/** + * Defines if the current runtime should use the mocked backend. + * @type boolean + */ +const isMockMode = !!process.env.NEXT_PUBLIC_USE_MOCK_API && isPositiveAnswer(process.env.NEXT_PUBLIC_USE_MOCK_API) + +/** + * Defines if the current runtime was built in development mode. + * @type boolean + */ +const isDevMode = process.env.NODE_ENV === 'development' + +module.exports = { + isTestMode, + isMockMode, + isDevMode +} diff --git a/src/utils/test-modes.ts b/src/utils/test-modes.ts deleted file mode 100644 index c7bbf2a73..000000000 --- a/src/utils/test-modes.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { isPositiveAnswer } from './is-positive-answer' - -/** - * Checks if the current runtime is built in e2e test mode. - */ -export const isTestMode = !!process.env.NEXT_PUBLIC_TEST_MODE && isPositiveAnswer(process.env.NEXT_PUBLIC_TEST_MODE) - -/** - * Checks if the current runtime should use the mocked backend. - */ -export const isMockMode = - !!process.env.NEXT_PUBLIC_USE_MOCK_API && isPositiveAnswer(process.env.NEXT_PUBLIC_USE_MOCK_API) - -/** - * Checks if the current runtime was built in development mode. - */ -export const isDevMode = process.env.NODE_ENV === 'development' diff --git a/src/utils/uri-origin-boundary.tsx b/src/utils/uri-origin-boundary.tsx new file mode 100644 index 000000000..0a8cbabf4 --- /dev/null +++ b/src/utils/uri-origin-boundary.tsx @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React, { Fragment, useMemo } from 'react' +import type { PropsWithChildren } from 'react' +import { isClientSideRendering } from './is-client-side-rendering' +import { useBaseUrl } from '../hooks/common/use-base-url' + +/** + * Checks if the url of the current browser window matches the expected origin. + * This is necessary to ensure that the render endpoint is only opened from the rendering origin. + * + * @param children The children react element that should be rendered if the origin is correct + */ +export const ExpectedOriginBoundary: React.FC = ({ children }) => { + const baseUrl = useBaseUrl() + const expectedOrigin = useMemo(() => new URL(baseUrl).origin, [baseUrl]) + + if (isClientSideRendering() && window.location.origin !== expectedOrigin) { + return {`You can't open this page using this URL. For this endpoint "${expectedOrigin}" is expected.`} + } else { + return {children} + } +}