Refactor handling of environment variables (#2303)
* Refactor environment variables Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
|
@ -1 +1,3 @@
|
||||||
NEXT_PUBLIC_USE_MOCK_API=true
|
NEXT_PUBLIC_USE_MOCK_API=true
|
||||||
|
HD_EDITOR_BASE_URL="http://localhost:3001/"
|
||||||
|
HD_RENDERER_BASE_URL="http://127.0.0.1:3001/"
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
NEXT_PUBLIC_USE_MOCK_API=false
|
NEXT_PUBLIC_USE_MOCK_API=false
|
||||||
|
NEXT_PUBLIC_TEST_MODE=false
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
NEXT_PUBLIC_USE_MOCK_API=true
|
NEXT_PUBLIC_USE_MOCK_API=true
|
||||||
|
HD_EDITOR_BASE_URL="http://127.0.0.1:3001/"
|
||||||
|
HD_RENDERER_BASE_URL="http://127.0.0.1:3001/"
|
||||||
|
|
6
.github/workflows/deploy-pr.yml
vendored
|
@ -35,13 +35,13 @@ jobs:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
|
|
||||||
- name: Patch files
|
- name: Patch files
|
||||||
run: bash netlify/patch-files.sh
|
run: bash netlify/patch-files.sh "${{ github.event.number }}"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn install --immutable
|
run: yarn install --immutable
|
||||||
|
|
||||||
- name: Build netlify variant
|
- name: Build mock variant
|
||||||
run: yarn build:netlify
|
run: yarn build:mock
|
||||||
|
|
||||||
- name: Remove Next.js cache to avoid it being deployed
|
- name: Remove Next.js cache to avoid it being deployed
|
||||||
run: rm -r .next/cache
|
run: rm -r .next/cache
|
||||||
|
|
2
.github/workflows/e2e.yml
vendored
|
@ -108,6 +108,8 @@ jobs:
|
||||||
|
|
||||||
- name: Run server
|
- name: Run server
|
||||||
run: yarn start:ci &
|
run: yarn start:ci &
|
||||||
|
env:
|
||||||
|
NODE_ENV: test
|
||||||
|
|
||||||
- name: Wait for server
|
- name: Wait for server
|
||||||
run: "sleep 3 && curl --max-time 120 http://127.0.0.1:3001/"
|
run: "sleep 3 && curl --max-time 120 http://127.0.0.1:3001/"
|
||||||
|
|
|
@ -3,7 +3,7 @@ Upstream-Name: react-client
|
||||||
Upstream-Contact: The HedgeDoc developers <license@hedgedoc.org>
|
Upstream-Contact: The HedgeDoc developers <license@hedgedoc.org>
|
||||||
Source: https://github.com/hedgedoc/react-client
|
Source: https://github.com/hedgedoc/react-client
|
||||||
|
|
||||||
Files: public/mock-public/*
|
Files: public/public/*
|
||||||
Copyright: 2021 The HedgeDoc developers (see AUTHORS file)
|
Copyright: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
License: CC0-1.0
|
License: CC0-1.0
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ Files: locales/*
|
||||||
Copyright: 2021 The HedgeDoc developers (see AUTHORS file)
|
Copyright: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
License: CC-BY-SA-4.0
|
License: CC-BY-SA-4.0
|
||||||
|
|
||||||
Files: public/mock-public/img/highres.jpg
|
Files: public/public/img/highres.jpg
|
||||||
Copyright: Vincent van Gogh
|
Copyright: Vincent van Gogh
|
||||||
License: CC0-1.0
|
License: CC0-1.0
|
||||||
|
|
||||||
|
|
13
Dockerfile
|
@ -4,14 +4,17 @@
|
||||||
|
|
||||||
# BUILD
|
# BUILD
|
||||||
FROM node:18-alpine AS builder
|
FROM node:18-alpine AS builder
|
||||||
ARG BUILD_VERSION=CLIENT_VERSION_MISSING
|
ENV NODE_ENV=production
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ARG BUILD_VERSION=CLIENT_VERSION_MISSING
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . ./
|
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 && \
|
sed -i "s/CLIENT_VERSION_MISSING/${BUILD_VERSION}/" src/version.json && \
|
||||||
yarn build:for-real-backend
|
yarn build
|
||||||
|
|
||||||
# RUNNER
|
# RUNNER
|
||||||
FROM node:18-alpine
|
FROM node:18-alpine
|
||||||
|
@ -27,6 +30,6 @@ COPY --from=builder --chown=node:node /app/.next/standalone ./
|
||||||
|
|
||||||
USER node
|
USER node
|
||||||
|
|
||||||
ENV PORT 3000
|
ENV PORT 3001
|
||||||
EXPOSE 3000/tcp
|
EXPOSE 3001/tcp
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
|
|
|
@ -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!
|
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 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
|
## UI Test
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ describe('Delete note', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('correctly deletes a note', () => {
|
it('correctly deletes a note', () => {
|
||||||
cy.intercept('DELETE', `/api/mock-backend/private/notes/${testNoteId}`, {
|
cy.intercept('DELETE', `api/private/notes/${testNoteId}`, {
|
||||||
statusCode: 204
|
statusCode: 204
|
||||||
})
|
})
|
||||||
cy.getByCypressId('sidebar.deleteNote.button').click()
|
cy.getByCypressId('sidebar.deleteNote.button').click()
|
||||||
|
|
|
@ -17,7 +17,7 @@ describe('File upload', () => {
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/mock-backend/private/media'
|
url: 'api/private/media'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
statusCode: 201,
|
statusCode: 201,
|
||||||
|
@ -74,7 +74,7 @@ describe('File upload', () => {
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/mock-backend/private/media'
|
url: 'api/private/media'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
statusCode: 400
|
statusCode: 400
|
||||||
|
|
|
@ -24,7 +24,7 @@ describe('History', () => {
|
||||||
describe('is as given when not empty', () => {
|
describe('is as given when not empty', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.clearLocalStorage('history')
|
cy.clearLocalStorage('history')
|
||||||
cy.intercept('GET', '/api/mock-backend/private/me/history', {
|
cy.intercept('GET', 'api/private/me/history', {
|
||||||
body: [
|
body: [
|
||||||
{
|
{
|
||||||
identifier: 'cypress',
|
identifier: 'cypress',
|
||||||
|
@ -51,7 +51,7 @@ describe('History', () => {
|
||||||
describe('is untitled when not empty', () => {
|
describe('is untitled when not empty', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.clearLocalStorage('history')
|
cy.clearLocalStorage('history')
|
||||||
cy.intercept('GET', '/api/mock-backend/private/me/history', {
|
cy.intercept('GET', 'api/private/me/history', {
|
||||||
body: [
|
body: [
|
||||||
{
|
{
|
||||||
identifier: 'cypress-no-title',
|
identifier: 'cypress-no-title',
|
||||||
|
@ -84,7 +84,7 @@ describe('History', () => {
|
||||||
|
|
||||||
describe('working', () => {
|
describe('working', () => {
|
||||||
beforeEach(() => {
|
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)
|
req.reply(200, req.body)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -106,7 +106,7 @@ describe('History', () => {
|
||||||
|
|
||||||
describe('failing', () => {
|
describe('failing', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept('PUT', '/api/mock-backend/private/me/history/features', {
|
cy.intercept('PUT', 'api/private/me/history/features', {
|
||||||
statusCode: 401
|
statusCode: 401
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -128,7 +128,7 @@ describe('History', () => {
|
||||||
describe('Import', () => {
|
describe('Import', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.clearLocalStorage('history')
|
cy.clearLocalStorage('history')
|
||||||
cy.intercept('GET', '/api/mock-backend/private/me/history', {
|
cy.intercept('GET', 'api/private/me/history', {
|
||||||
body: []
|
body: []
|
||||||
})
|
})
|
||||||
cy.visitHistory()
|
cy.visitHistory()
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
describe('Intro page', () => {
|
describe('Intro page', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept('/mock-public/intro.md', 'test content')
|
cy.intercept('public/intro.md', 'test content')
|
||||||
cy.visitHome()
|
cy.visitHome()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ describe('Intro page', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("won't show anything if no content was found", () => {
|
it("won't show anything if no content was found", () => {
|
||||||
cy.intercept('/mock-public/intro.md', {
|
cy.intercept('public/intro.md', {
|
||||||
statusCode: 404
|
statusCode: 404
|
||||||
})
|
})
|
||||||
cy.visitHome()
|
cy.visitHome()
|
||||||
|
|
|
@ -11,13 +11,13 @@ const motdMockHtml = 'This is the <strong>mock</strong> Motd call'
|
||||||
|
|
||||||
describe('Motd', () => {
|
describe('Motd', () => {
|
||||||
const mockExistingMotd = (useEtag?: boolean, content = motdMockContent) => {
|
const mockExistingMotd = (useEtag?: boolean, content = motdMockContent) => {
|
||||||
cy.intercept('GET', '/mock-public/motd.md', {
|
cy.intercept('GET', 'public/motd.md', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
headers: { [useEtag ? 'etag' : 'Last-Modified']: MOCK_LAST_MODIFIED },
|
headers: { [useEtag ? 'etag' : 'Last-Modified']: MOCK_LAST_MODIFIED },
|
||||||
body: content
|
body: content
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.intercept('HEAD', '/mock-public/motd.md', {
|
cy.intercept('HEAD', 'public/motd.md', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
headers: { [useEtag ? 'etag' : 'Last-Modified']: MOCK_LAST_MODIFIED }
|
headers: { [useEtag ? 'etag' : 'Last-Modified']: MOCK_LAST_MODIFIED }
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,7 +8,7 @@ describe('profile page', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
{
|
{
|
||||||
url: '/api/mock-backend/private/tokens',
|
url: 'api/private/tokens',
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -25,7 +25,7 @@ describe('profile page', () => {
|
||||||
)
|
)
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
{
|
{
|
||||||
url: '/api/mock-backend/private/tokens',
|
url: 'api/private/tokens',
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -42,7 +42,7 @@ describe('profile page', () => {
|
||||||
)
|
)
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
{
|
{
|
||||||
url: '/api/mock-backend/private/tokens/cypress',
|
url: 'api/private/tokens/cypress',
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -14,7 +14,7 @@ declare namespace Cypress {
|
||||||
|
|
||||||
export const branding = {
|
export const branding = {
|
||||||
name: 'DEMO Corp',
|
name: 'DEMO Corp',
|
||||||
logo: '/mock-public/img/demo.png'
|
logo: 'public/img/demo.png'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authProviders = [
|
export const authProviders = [
|
||||||
|
@ -77,15 +77,11 @@ export const config = {
|
||||||
commit: 'MOCK'
|
commit: 'MOCK'
|
||||||
},
|
},
|
||||||
plantumlServer: 'http://mock-plantuml.local',
|
plantumlServer: 'http://mock-plantuml.local',
|
||||||
maxDocumentLength: 200,
|
maxDocumentLength: 200
|
||||||
iframeCommunication: {
|
|
||||||
editorOrigin: 'http://127.0.0.1:3001/',
|
|
||||||
rendererOrigin: 'http://127.0.0.1:3001/'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Cypress.Commands.add('loadConfig', (additionalConfig?: Partial<typeof config>) => {
|
Cypress.Commands.add('loadConfig', (additionalConfig?: Partial<typeof config>) => {
|
||||||
return cy.intercept('/api/mock-backend/private/config', {
|
return cy.intercept('api/private/config', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
body: {
|
body: {
|
||||||
...config,
|
...config,
|
||||||
|
@ -97,11 +93,11 @@ Cypress.Commands.add('loadConfig', (additionalConfig?: Partial<typeof config>) =
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.loadConfig()
|
cy.loadConfig()
|
||||||
|
|
||||||
cy.intercept('GET', '/mock-public/motd.md', {
|
cy.intercept('GET', 'public/motd.md', {
|
||||||
body: '404 Not Found!',
|
body: '404 Not Found!',
|
||||||
statusCode: 404
|
statusCode: 404
|
||||||
})
|
})
|
||||||
cy.intercept('HEAD', '/mock-public/motd.md', {
|
cy.intercept('HEAD', 'public/motd.md', {
|
||||||
statusCode: 404
|
statusCode: 404
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
export const testNoteId = 'test'
|
export const testNoteId = 'test'
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept(`/api/mock-backend/private/notes/${testNoteId}`, {
|
cy.intercept(`api/private/notes/${testNoteId}`, {
|
||||||
content: '',
|
content: '',
|
||||||
metadata: {
|
metadata: {
|
||||||
id: testNoteId,
|
id: testNoteId,
|
||||||
|
|
|
@ -4,3 +4,6 @@ command = "echo Pseudo build command because the build is made by the CI"
|
||||||
|
|
||||||
[[plugins]]
|
[[plugins]]
|
||||||
package = "@netlify/plugin-nextjs"
|
package = "@netlify/plugin-nextjs"
|
||||||
|
|
||||||
|
[dev]
|
||||||
|
targetPort = 3001
|
||||||
|
|
|
@ -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.
|
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)
|
[![Deployed using netlify](https://www.netlify.com/img/global/badges/netlify-color-accent.svg)](https://www.netlify.com)
|
||||||
|
|
|
@ -8,9 +8,11 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo 'Patch intro.md to include netlify banner.'
|
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.'
|
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'
|
echo 'Patch version.json to include git hash'
|
||||||
jq ".version = \"0.0.0+${GITHUB_SHA:0:8}\"" src/version.json > src/_version.json
|
jq ".version = \"0.0.0+${GITHUB_SHA:0:8}\"" src/version.json > src/_version.json
|
||||||
mv 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
|
||||||
|
|
|
@ -3,47 +3,27 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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.
|
console.log('Node environment is', process.env.NODE_ENV)
|
||||||
// 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)
|
|
||||||
|
|
||||||
if (isMockMode) {
|
if (isMockMode) {
|
||||||
console.log('Uses mock API')
|
console.log('Use 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!'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTestMode) {
|
if (isTestMode) {
|
||||||
console.warn(`This build runs in test mode. This means:
|
console.warn(`This build runs in test mode. This means:
|
||||||
- no sandboxed iframe
|
- 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:
|
console.warn(`This build runs in mock mode. This means:
|
||||||
- No real data. All API responses are mocked
|
- No real data. All API responses are mocked
|
||||||
- No persistent data
|
- 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} */
|
/** @type {import('next').NextConfig} */
|
||||||
const rawNextConfig = {
|
const rawNextConfig = {
|
||||||
webpack: (config) => {
|
webpack: (config) => {
|
||||||
|
@ -111,9 +85,8 @@ const rawNextConfig = {
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
output: 'standalone',
|
output: 'standalone'
|
||||||
}
|
}
|
||||||
|
|
||||||
const completeNextConfig = withBundleAnalyzer(rawNextConfig)
|
const completeNextConfig = withBundleAnalyzer(rawNextConfig)
|
||||||
|
|
||||||
module.exports = completeNextConfig
|
module.exports = completeNextConfig
|
||||||
|
|
16
package.json
|
@ -3,28 +3,24 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "cross-env NODE_ENV=production next build",
|
||||||
"build:netlify": "cross-env NEXT_PUBLIC_IGNORE_IFRAME_ORIGIN_CONFIG=true yarn build:mock",
|
|
||||||
"build:mock": "cross-env NEXT_PUBLIC_USE_MOCK_API=true 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:test": "cross-env NODE_ENV=test 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",
|
|
||||||
"analyze": "cross-env ANALYZE=true next build",
|
"analyze": "cross-env ANALYZE=true next build",
|
||||||
"dev": "cross-env PORT=3001 next dev",
|
"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",
|
"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": "prettier -c \"src/**/*.{ts,tsx,js}\" \"cypress/**/*.{ts,tsx}\"",
|
||||||
"format:fix": "prettier -w \"src/**/*.{ts,tsx,js}\" \"cypress/**/*.{ts,tsx}\"",
|
"format:fix": "prettier -w \"src/**/*.{ts,tsx,js}\" \"cypress/**/*.{ts,tsx}\"",
|
||||||
"lint": "eslint --max-warnings=0 --ext .ts,.tsx src",
|
"lint": "eslint --max-warnings=0 --ext .ts,.tsx src",
|
||||||
"lint:fix": "eslint --fix --ext .ts,.tsx src",
|
"lint:fix": "eslint --fix --ext .ts,.tsx src",
|
||||||
"start": "cross-env PORT=3001 next start",
|
"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:ci": "cross-env NODE_ENV=test PORT=3001 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",
|
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
"cy:run:chrome": "cypress run --browser chrome",
|
"cy:run:chrome": "cypress run --browser chrome",
|
||||||
"cy:run:firefox": "cypress run --browser firefox",
|
"cy:run:firefox": "cypress run --browser firefox",
|
||||||
"test": "jest --watch",
|
"test": "cross-env NODE_ENV=test jest --watch",
|
||||||
"test:ci": "jest --ci"
|
"test:ci": "cross-env NODE_ENV=test jest --ci"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 3.9 MiB After Width: | Height: | Size: 3.9 MiB |
|
@ -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.
|
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)
|
1
public/public/readme.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This directory should only be used in mock mode.
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 250 KiB |
|
@ -4,7 +4,6 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { apiUrl } from '../../../utils/api-url'
|
|
||||||
import deepmerge from 'deepmerge'
|
import deepmerge from 'deepmerge'
|
||||||
import { defaultConfig, defaultHeaders } from '../default-config'
|
import { defaultConfig, defaultHeaders } from '../default-config'
|
||||||
import { ApiResponse } from '../api-response'
|
import { ApiResponse } from '../api-response'
|
||||||
|
@ -28,7 +27,7 @@ export abstract class ApiRequestBuilder<ResponseType> {
|
||||||
* @param endpoint The target endpoint without a leading slash.
|
* @param endpoint The target endpoint without a leading slash.
|
||||||
*/
|
*/
|
||||||
constructor(endpoint: string) {
|
constructor(endpoint: string) {
|
||||||
this.targetUrl = apiUrl + endpoint
|
this.targetUrl = `api/private/${endpoint}`
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async sendRequestAndVerifyResponse(
|
protected async sendRequestAndVerifyResponse(
|
||||||
|
|
|
@ -19,14 +19,14 @@ describe('DeleteApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
describe('sendRequest without body', () => {
|
describe('sendRequest without body', () => {
|
||||||
it('without headers', async () => {
|
it('without headers', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 204, { method: 'DELETE' })
|
expectFetch('api/private/test', 204, { method: 'DELETE' })
|
||||||
await new DeleteApiRequestBuilder<string, undefined>('test').sendRequest()
|
await new DeleteApiRequestBuilder<string, undefined>('test').sendRequest()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('with single header', async () => {
|
it('with single header', async () => {
|
||||||
const expectedHeaders = new Headers()
|
const expectedHeaders = new Headers()
|
||||||
expectedHeaders.append('test', 'true')
|
expectedHeaders.append('test', 'true')
|
||||||
expectFetch('/api/mock-backend/private/test', 204, {
|
expectFetch('api/private/test', 204, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: expectedHeaders
|
headers: expectedHeaders
|
||||||
})
|
})
|
||||||
|
@ -36,7 +36,7 @@ describe('DeleteApiRequestBuilder', () => {
|
||||||
it('with overriding single header', async () => {
|
it('with overriding single header', async () => {
|
||||||
const expectedHeaders = new Headers()
|
const expectedHeaders = new Headers()
|
||||||
expectedHeaders.append('test', 'false')
|
expectedHeaders.append('test', 'false')
|
||||||
expectFetch('/api/mock-backend/private/test', 204, {
|
expectFetch('api/private/test', 204, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: expectedHeaders
|
headers: expectedHeaders
|
||||||
})
|
})
|
||||||
|
@ -50,7 +50,7 @@ describe('DeleteApiRequestBuilder', () => {
|
||||||
const expectedHeaders = new Headers()
|
const expectedHeaders = new Headers()
|
||||||
expectedHeaders.append('test', 'true')
|
expectedHeaders.append('test', 'true')
|
||||||
expectedHeaders.append('test2', 'false')
|
expectedHeaders.append('test2', 'false')
|
||||||
expectFetch('/api/mock-backend/private/test', 204, {
|
expectFetch('api/private/test', 204, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: expectedHeaders
|
headers: expectedHeaders
|
||||||
})
|
})
|
||||||
|
@ -65,7 +65,7 @@ describe('DeleteApiRequestBuilder', () => {
|
||||||
const expectedHeaders = new Headers()
|
const expectedHeaders = new Headers()
|
||||||
expectedHeaders.append('Content-Type', 'application/json')
|
expectedHeaders.append('Content-Type', 'application/json')
|
||||||
|
|
||||||
expectFetch('/api/mock-backend/private/test', 204, {
|
expectFetch('api/private/test', 204, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: expectedHeaders,
|
headers: expectedHeaders,
|
||||||
body: '{"test":true,"foo":"bar"}'
|
body: '{"test":true,"foo":"bar"}'
|
||||||
|
@ -79,7 +79,7 @@ describe('DeleteApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sendRequest with other body', async () => {
|
it('sendRequest with other body', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 204, {
|
expectFetch('api/private/test', 204, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
body: 'HedgeDoc'
|
body: 'HedgeDoc'
|
||||||
})
|
})
|
||||||
|
@ -87,13 +87,13 @@ describe('DeleteApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sendRequest with expected status code', async () => {
|
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<string, undefined>('test').withExpectedStatusCode(200).sendRequest()
|
await new DeleteApiRequestBuilder<string, undefined>('test').withExpectedStatusCode(200).sendRequest()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('sendRequest with custom options', () => {
|
describe('sendRequest with custom options', () => {
|
||||||
it('with one option', async () => {
|
it('with one option', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 204, {
|
expectFetch('api/private/test', 204, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
cache: 'force-cache'
|
cache: 'force-cache'
|
||||||
})
|
})
|
||||||
|
@ -105,7 +105,7 @@ describe('DeleteApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('overriding single option', async () => {
|
it('overriding single option', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 204, {
|
expectFetch('api/private/test', 204, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
cache: 'no-store'
|
cache: 'no-store'
|
||||||
})
|
})
|
||||||
|
@ -120,7 +120,7 @@ describe('DeleteApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('with multiple options', async () => {
|
it('with multiple options', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 204, {
|
expectFetch('api/private/test', 204, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
cache: 'force-cache',
|
cache: 'force-cache',
|
||||||
integrity: 'test'
|
integrity: 'test'
|
||||||
|
@ -136,7 +136,7 @@ describe('DeleteApiRequestBuilder', () => {
|
||||||
|
|
||||||
describe('sendRequest with custom error map', () => {
|
describe('sendRequest with custom error map', () => {
|
||||||
it('for valid status code', async () => {
|
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<string, undefined>('test')
|
await new DeleteApiRequestBuilder<string, undefined>('test')
|
||||||
.withStatusCodeErrorMapping({
|
.withStatusCodeErrorMapping({
|
||||||
400: 'noooooo',
|
400: 'noooooo',
|
||||||
|
@ -146,7 +146,7 @@ describe('DeleteApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('for invalid status code 1', async () => {
|
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<string, undefined>('test')
|
const request = new DeleteApiRequestBuilder<string, undefined>('test')
|
||||||
.withStatusCodeErrorMapping({
|
.withStatusCodeErrorMapping({
|
||||||
400: 'noooooo',
|
400: 'noooooo',
|
||||||
|
@ -157,7 +157,7 @@ describe('DeleteApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('for invalid status code 2', async () => {
|
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<string, undefined>('test')
|
const request = new DeleteApiRequestBuilder<string, undefined>('test')
|
||||||
.withStatusCodeErrorMapping({
|
.withStatusCodeErrorMapping({
|
||||||
400: 'noooooo',
|
400: 'noooooo',
|
||||||
|
|
|
@ -20,14 +20,14 @@ describe('GetApiRequestBuilder', () => {
|
||||||
|
|
||||||
describe('sendRequest', () => {
|
describe('sendRequest', () => {
|
||||||
it('without headers', async () => {
|
it('without headers', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 200, { method: 'GET' })
|
expectFetch('api/private/test', 200, { method: 'GET' })
|
||||||
await new GetApiRequestBuilder<string>('test').sendRequest()
|
await new GetApiRequestBuilder<string>('test').sendRequest()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('with single header', async () => {
|
it('with single header', async () => {
|
||||||
const expectedHeaders = new Headers()
|
const expectedHeaders = new Headers()
|
||||||
expectedHeaders.append('test', 'true')
|
expectedHeaders.append('test', 'true')
|
||||||
expectFetch('/api/mock-backend/private/test', 200, {
|
expectFetch('api/private/test', 200, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: expectedHeaders
|
headers: expectedHeaders
|
||||||
})
|
})
|
||||||
|
@ -37,7 +37,7 @@ describe('GetApiRequestBuilder', () => {
|
||||||
it('with overriding single header', async () => {
|
it('with overriding single header', async () => {
|
||||||
const expectedHeaders = new Headers()
|
const expectedHeaders = new Headers()
|
||||||
expectedHeaders.append('test', 'false')
|
expectedHeaders.append('test', 'false')
|
||||||
expectFetch('/api/mock-backend/private/test', 200, {
|
expectFetch('api/private/test', 200, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: expectedHeaders
|
headers: expectedHeaders
|
||||||
})
|
})
|
||||||
|
@ -51,7 +51,7 @@ describe('GetApiRequestBuilder', () => {
|
||||||
const expectedHeaders = new Headers()
|
const expectedHeaders = new Headers()
|
||||||
expectedHeaders.append('test', 'true')
|
expectedHeaders.append('test', 'true')
|
||||||
expectedHeaders.append('test2', 'false')
|
expectedHeaders.append('test2', 'false')
|
||||||
expectFetch('/api/mock-backend/private/test', 200, {
|
expectFetch('api/private/test', 200, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: expectedHeaders
|
headers: expectedHeaders
|
||||||
})
|
})
|
||||||
|
@ -63,13 +63,13 @@ describe('GetApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sendRequest with expected status code', async () => {
|
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<string>('test').withExpectedStatusCode(200).sendRequest()
|
await new GetApiRequestBuilder<string>('test').withExpectedStatusCode(200).sendRequest()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('sendRequest with custom options', () => {
|
describe('sendRequest with custom options', () => {
|
||||||
it('with one option', async () => {
|
it('with one option', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 200, {
|
expectFetch('api/private/test', 200, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
cache: 'force-cache'
|
cache: 'force-cache'
|
||||||
})
|
})
|
||||||
|
@ -81,7 +81,7 @@ describe('GetApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('overriding single option', async () => {
|
it('overriding single option', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 200, {
|
expectFetch('api/private/test', 200, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
cache: 'no-store'
|
cache: 'no-store'
|
||||||
})
|
})
|
||||||
|
@ -96,7 +96,7 @@ describe('GetApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('with multiple options', async () => {
|
it('with multiple options', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 200, {
|
expectFetch('api/private/test', 200, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
cache: 'force-cache',
|
cache: 'force-cache',
|
||||||
integrity: 'test'
|
integrity: 'test'
|
||||||
|
@ -112,7 +112,7 @@ describe('GetApiRequestBuilder', () => {
|
||||||
|
|
||||||
describe('sendRequest with custom error map', () => {
|
describe('sendRequest with custom error map', () => {
|
||||||
it('for valid status code', async () => {
|
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<string>('test')
|
await new GetApiRequestBuilder<string>('test')
|
||||||
.withStatusCodeErrorMapping({
|
.withStatusCodeErrorMapping({
|
||||||
400: 'noooooo',
|
400: 'noooooo',
|
||||||
|
@ -122,7 +122,7 @@ describe('GetApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('for invalid status code 1', async () => {
|
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<string>('test')
|
const request = new GetApiRequestBuilder<string>('test')
|
||||||
.withStatusCodeErrorMapping({
|
.withStatusCodeErrorMapping({
|
||||||
400: 'noooooo',
|
400: 'noooooo',
|
||||||
|
@ -133,7 +133,7 @@ describe('GetApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('for invalid status code 2', async () => {
|
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<string>('test')
|
const request = new GetApiRequestBuilder<string>('test')
|
||||||
.withStatusCodeErrorMapping({
|
.withStatusCodeErrorMapping({
|
||||||
400: 'noooooo',
|
400: 'noooooo',
|
||||||
|
|
|
@ -20,14 +20,14 @@ describe('PostApiRequestBuilder', () => {
|
||||||
|
|
||||||
describe('sendRequest without body', () => {
|
describe('sendRequest without body', () => {
|
||||||
it('without headers', async () => {
|
it('without headers', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 201, { method: 'POST' })
|
expectFetch('api/private/test', 201, { method: 'POST' })
|
||||||
await new PostApiRequestBuilder<string, undefined>('test').sendRequest()
|
await new PostApiRequestBuilder<string, undefined>('test').sendRequest()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('with single header', async () => {
|
it('with single header', async () => {
|
||||||
const expectedHeaders = new Headers()
|
const expectedHeaders = new Headers()
|
||||||
expectedHeaders.append('test', 'true')
|
expectedHeaders.append('test', 'true')
|
||||||
expectFetch('/api/mock-backend/private/test', 201, {
|
expectFetch('api/private/test', 201, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: expectedHeaders
|
headers: expectedHeaders
|
||||||
})
|
})
|
||||||
|
@ -37,7 +37,7 @@ describe('PostApiRequestBuilder', () => {
|
||||||
it('with overriding single header', async () => {
|
it('with overriding single header', async () => {
|
||||||
const expectedHeaders = new Headers()
|
const expectedHeaders = new Headers()
|
||||||
expectedHeaders.append('test', 'false')
|
expectedHeaders.append('test', 'false')
|
||||||
expectFetch('/api/mock-backend/private/test', 201, {
|
expectFetch('api/private/test', 201, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: expectedHeaders
|
headers: expectedHeaders
|
||||||
})
|
})
|
||||||
|
@ -51,7 +51,7 @@ describe('PostApiRequestBuilder', () => {
|
||||||
const expectedHeaders = new Headers()
|
const expectedHeaders = new Headers()
|
||||||
expectedHeaders.append('test', 'true')
|
expectedHeaders.append('test', 'true')
|
||||||
expectedHeaders.append('test2', 'false')
|
expectedHeaders.append('test2', 'false')
|
||||||
expectFetch('/api/mock-backend/private/test', 201, {
|
expectFetch('api/private/test', 201, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: expectedHeaders
|
headers: expectedHeaders
|
||||||
})
|
})
|
||||||
|
@ -66,7 +66,7 @@ describe('PostApiRequestBuilder', () => {
|
||||||
const expectedHeaders = new Headers()
|
const expectedHeaders = new Headers()
|
||||||
expectedHeaders.append('Content-Type', 'application/json')
|
expectedHeaders.append('Content-Type', 'application/json')
|
||||||
|
|
||||||
expectFetch('/api/mock-backend/private/test', 201, {
|
expectFetch('api/private/test', 201, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: expectedHeaders,
|
headers: expectedHeaders,
|
||||||
body: '{"test":true,"foo":"bar"}'
|
body: '{"test":true,"foo":"bar"}'
|
||||||
|
@ -80,7 +80,7 @@ describe('PostApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sendRequest with other body', async () => {
|
it('sendRequest with other body', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 201, {
|
expectFetch('api/private/test', 201, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: 'HedgeDoc'
|
body: 'HedgeDoc'
|
||||||
})
|
})
|
||||||
|
@ -88,13 +88,13 @@ describe('PostApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sendRequest with expected status code', async () => {
|
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<string, undefined>('test').withExpectedStatusCode(200).sendRequest()
|
await new PostApiRequestBuilder<string, undefined>('test').withExpectedStatusCode(200).sendRequest()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('sendRequest with custom options', () => {
|
describe('sendRequest with custom options', () => {
|
||||||
it('with one option', async () => {
|
it('with one option', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 201, {
|
expectFetch('api/private/test', 201, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
cache: 'force-cache'
|
cache: 'force-cache'
|
||||||
})
|
})
|
||||||
|
@ -106,7 +106,7 @@ describe('PostApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('overriding single option', async () => {
|
it('overriding single option', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 201, {
|
expectFetch('api/private/test', 201, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
cache: 'no-store'
|
cache: 'no-store'
|
||||||
})
|
})
|
||||||
|
@ -121,7 +121,7 @@ describe('PostApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('with multiple options', async () => {
|
it('with multiple options', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 201, {
|
expectFetch('api/private/test', 201, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
cache: 'force-cache',
|
cache: 'force-cache',
|
||||||
integrity: 'test'
|
integrity: 'test'
|
||||||
|
@ -137,7 +137,7 @@ describe('PostApiRequestBuilder', () => {
|
||||||
|
|
||||||
describe('sendRequest with custom error map', () => {
|
describe('sendRequest with custom error map', () => {
|
||||||
it('for valid status code', async () => {
|
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<string, undefined>('test')
|
await new PostApiRequestBuilder<string, undefined>('test')
|
||||||
.withStatusCodeErrorMapping({
|
.withStatusCodeErrorMapping({
|
||||||
400: 'noooooo',
|
400: 'noooooo',
|
||||||
|
@ -147,7 +147,7 @@ describe('PostApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('for invalid status code 1', async () => {
|
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<string, undefined>('test')
|
const request = new PostApiRequestBuilder<string, undefined>('test')
|
||||||
.withStatusCodeErrorMapping({
|
.withStatusCodeErrorMapping({
|
||||||
400: 'noooooo',
|
400: 'noooooo',
|
||||||
|
@ -158,7 +158,7 @@ describe('PostApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('for invalid status code 2', async () => {
|
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<string, undefined>('test')
|
const request = new PostApiRequestBuilder<string, undefined>('test')
|
||||||
.withStatusCodeErrorMapping({
|
.withStatusCodeErrorMapping({
|
||||||
400: 'noooooo',
|
400: 'noooooo',
|
||||||
|
|
|
@ -20,14 +20,14 @@ describe('PutApiRequestBuilder', () => {
|
||||||
|
|
||||||
describe('sendRequest without body', () => {
|
describe('sendRequest without body', () => {
|
||||||
it('without headers', async () => {
|
it('without headers', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 200, { method: 'PUT' })
|
expectFetch('api/private/test', 200, { method: 'PUT' })
|
||||||
await new PutApiRequestBuilder<string, undefined>('test').sendRequest()
|
await new PutApiRequestBuilder<string, undefined>('test').sendRequest()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('with single header', async () => {
|
it('with single header', async () => {
|
||||||
const expectedHeaders = new Headers()
|
const expectedHeaders = new Headers()
|
||||||
expectedHeaders.append('test', 'true')
|
expectedHeaders.append('test', 'true')
|
||||||
expectFetch('/api/mock-backend/private/test', 200, {
|
expectFetch('api/private/test', 200, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: expectedHeaders
|
headers: expectedHeaders
|
||||||
})
|
})
|
||||||
|
@ -37,7 +37,7 @@ describe('PutApiRequestBuilder', () => {
|
||||||
it('with overriding single header', async () => {
|
it('with overriding single header', async () => {
|
||||||
const expectedHeaders = new Headers()
|
const expectedHeaders = new Headers()
|
||||||
expectedHeaders.append('test', 'false')
|
expectedHeaders.append('test', 'false')
|
||||||
expectFetch('/api/mock-backend/private/test', 200, {
|
expectFetch('api/private/test', 200, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: expectedHeaders
|
headers: expectedHeaders
|
||||||
})
|
})
|
||||||
|
@ -51,7 +51,7 @@ describe('PutApiRequestBuilder', () => {
|
||||||
const expectedHeaders = new Headers()
|
const expectedHeaders = new Headers()
|
||||||
expectedHeaders.append('test', 'true')
|
expectedHeaders.append('test', 'true')
|
||||||
expectedHeaders.append('test2', 'false')
|
expectedHeaders.append('test2', 'false')
|
||||||
expectFetch('/api/mock-backend/private/test', 200, {
|
expectFetch('api/private/test', 200, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: expectedHeaders
|
headers: expectedHeaders
|
||||||
})
|
})
|
||||||
|
@ -66,7 +66,7 @@ describe('PutApiRequestBuilder', () => {
|
||||||
const expectedHeaders = new Headers()
|
const expectedHeaders = new Headers()
|
||||||
expectedHeaders.append('Content-Type', 'application/json')
|
expectedHeaders.append('Content-Type', 'application/json')
|
||||||
|
|
||||||
expectFetch('/api/mock-backend/private/test', 200, {
|
expectFetch('api/private/test', 200, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: expectedHeaders,
|
headers: expectedHeaders,
|
||||||
body: '{"test":true,"foo":"bar"}'
|
body: '{"test":true,"foo":"bar"}'
|
||||||
|
@ -80,7 +80,7 @@ describe('PutApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sendRequest with other body', async () => {
|
it('sendRequest with other body', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 200, {
|
expectFetch('api/private/test', 200, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: 'HedgeDoc'
|
body: 'HedgeDoc'
|
||||||
})
|
})
|
||||||
|
@ -88,13 +88,13 @@ describe('PutApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sendRequest with expected status code', async () => {
|
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<string, undefined>('test').withExpectedStatusCode(200).sendRequest()
|
await new PutApiRequestBuilder<string, undefined>('test').withExpectedStatusCode(200).sendRequest()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('sendRequest with custom options', () => {
|
describe('sendRequest with custom options', () => {
|
||||||
it('with one option', async () => {
|
it('with one option', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 200, {
|
expectFetch('api/private/test', 200, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
cache: 'force-cache'
|
cache: 'force-cache'
|
||||||
})
|
})
|
||||||
|
@ -106,7 +106,7 @@ describe('PutApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('overriding single option', async () => {
|
it('overriding single option', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 200, {
|
expectFetch('api/private/test', 200, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
cache: 'no-store'
|
cache: 'no-store'
|
||||||
})
|
})
|
||||||
|
@ -121,7 +121,7 @@ describe('PutApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('with multiple options', async () => {
|
it('with multiple options', async () => {
|
||||||
expectFetch('/api/mock-backend/private/test', 200, {
|
expectFetch('api/private/test', 200, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
cache: 'force-cache',
|
cache: 'force-cache',
|
||||||
integrity: 'test'
|
integrity: 'test'
|
||||||
|
@ -137,7 +137,7 @@ describe('PutApiRequestBuilder', () => {
|
||||||
|
|
||||||
describe('sendRequest with custom error map', () => {
|
describe('sendRequest with custom error map', () => {
|
||||||
it('for valid status code', async () => {
|
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<string, undefined>('test')
|
await new PutApiRequestBuilder<string, undefined>('test')
|
||||||
.withStatusCodeErrorMapping({
|
.withStatusCodeErrorMapping({
|
||||||
400: 'noooooo',
|
400: 'noooooo',
|
||||||
|
@ -147,7 +147,7 @@ describe('PutApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('for invalid status code 1', async () => {
|
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<string, undefined>('test')
|
const request = new PutApiRequestBuilder<string, undefined>('test')
|
||||||
.withStatusCodeErrorMapping({
|
.withStatusCodeErrorMapping({
|
||||||
400: 'noooooo',
|
400: 'noooooo',
|
||||||
|
@ -158,7 +158,7 @@ describe('PutApiRequestBuilder', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('for invalid status code 2', async () => {
|
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<string, undefined>('test')
|
const request = new PutApiRequestBuilder<string, undefined>('test')
|
||||||
.withStatusCodeErrorMapping({
|
.withStatusCodeErrorMapping({
|
||||||
400: 'noooooo',
|
400: 'noooooo',
|
||||||
|
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -14,7 +14,6 @@ export interface Config {
|
||||||
version: BackendVersion
|
version: BackendVersion
|
||||||
plantumlServer?: string
|
plantumlServer?: string
|
||||||
maxDocumentLength: number
|
maxDocumentLength: number
|
||||||
iframeCommunication: iframeCommunicationConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AuthProviderType {
|
export enum AuthProviderType {
|
||||||
|
@ -67,11 +66,6 @@ export interface AuthProviderWithoutCustomName {
|
||||||
|
|
||||||
export type AuthProvider = AuthProviderWithCustomName | AuthProviderWithoutCustomName
|
export type AuthProvider = AuthProviderWithCustomName | AuthProviderWithoutCustomName
|
||||||
|
|
||||||
export interface iframeCommunicationConfig {
|
|
||||||
editorOrigin: string
|
|
||||||
rendererOrigin: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BrandingConfig {
|
export interface BrandingConfig {
|
||||||
name?: string
|
name?: string
|
||||||
logo?: string
|
logo?: string
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
import { setMotd } from '../../../redux/motd/methods'
|
import { setMotd } from '../../../redux/motd/methods'
|
||||||
import { Logger } from '../../../utils/logger'
|
import { Logger } from '../../../utils/logger'
|
||||||
import { customizeAssetsUrl } from '../../../utils/customize-assets-url'
|
|
||||||
import { defaultConfig } from '../../../api/common/default-config'
|
import { defaultConfig } from '../../../api/common/default-config'
|
||||||
|
|
||||||
export const MOTD_LOCAL_STORAGE_KEY = 'motd.lastModified'
|
export const MOTD_LOCAL_STORAGE_KEY = 'motd.lastModified'
|
||||||
|
@ -21,7 +20,7 @@ const log = new Logger('Motd')
|
||||||
*/
|
*/
|
||||||
export const fetchMotd = async (): Promise<void> => {
|
export const fetchMotd = async (): Promise<void> => {
|
||||||
const cachedLastModified = window.localStorage.getItem(MOTD_LOCAL_STORAGE_KEY)
|
const cachedLastModified = window.localStorage.getItem(MOTD_LOCAL_STORAGE_KEY)
|
||||||
const motdUrl = `${customizeAssetsUrl}motd.md`
|
const motdUrl = `public/motd.md`
|
||||||
|
|
||||||
if (cachedLastModified) {
|
if (cachedLastModified) {
|
||||||
const response = await fetch(motdUrl, {
|
const response = await fetch(motdUrl, {
|
||||||
|
|
38
src/components/common/base-url/base-url-context-provider.tsx
Normal file
|
@ -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<BaseUrls | undefined>(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<PropsWithChildren<BaseUrlContextProviderProps>> = ({
|
||||||
|
baseUrls,
|
||||||
|
children
|
||||||
|
}) => {
|
||||||
|
const [baseUrlState] = useState<undefined | BaseUrls>(() => baseUrls)
|
||||||
|
|
||||||
|
return baseUrlState === undefined ? (
|
||||||
|
<div className={'text-white'}>HedgeDoc is not configured correctly! Please check the server log.</div>
|
||||||
|
) : (
|
||||||
|
<baseUrlContext.Provider value={baseUrlState}>{children}</baseUrlContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
|
@ -6,7 +6,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useAsync } from 'react-use'
|
import { useAsync } from 'react-use'
|
||||||
import { getUser } from '../../../api/users'
|
import { getUser } from '../../../api/users'
|
||||||
import { customizeAssetsUrl } from '../../../utils/customize-assets-url'
|
|
||||||
import type { UserAvatarProps } from './user-avatar'
|
import type { UserAvatarProps } from './user-avatar'
|
||||||
import { UserAvatar } from './user-avatar'
|
import { UserAvatar } from './user-avatar'
|
||||||
import type { UserInfo } from '../../../api/users/types'
|
import type { UserInfo } from '../../../api/users/types'
|
||||||
|
@ -34,7 +33,7 @@ export const UserAvatarForUsername: React.FC<UserAvatarForUsernameProps> = ({ us
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
displayName: t('common.guestUser'),
|
displayName: t('common.guestUser'),
|
||||||
photo: `${customizeAssetsUrl}img/avatar.png`,
|
photo: `public/img/avatar.png`,
|
||||||
username: ''
|
username: ''
|
||||||
}
|
}
|
||||||
}, [username, t])
|
}, [username, t])
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { CommonModal } from '../../../common/modals/common-modal'
|
||||||
import { ShowIf } from '../../../common/show-if/show-if'
|
import { ShowIf } from '../../../common/show-if/show-if'
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
import { NoteType } from '../../../../redux/note-details/types/note-details'
|
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.
|
* Renders a modal which provides shareable URLs of this note.
|
||||||
|
@ -25,7 +25,7 @@ export const ShareModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) =>
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const noteFrontmatter = useApplicationState((state) => state.noteDetails.frontmatter)
|
const noteFrontmatter = useApplicationState((state) => state.noteDetails.frontmatter)
|
||||||
const editorMode = useApplicationState((state) => state.editorConfig.editorMode)
|
const editorMode = useApplicationState((state) => state.editorConfig.editorMode)
|
||||||
const baseUrl = useFrontendBaseUrl()
|
const baseUrl = useBaseUrl()
|
||||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { backendUrl } from '../../../../../utils/backend-url'
|
|
||||||
import { isMockMode } from '../../../../../utils/test-modes'
|
import { isMockMode } from '../../../../../utils/test-modes'
|
||||||
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
||||||
|
import { useBaseUrl } from '../../../../../hooks/common/use-base-url'
|
||||||
|
|
||||||
const LOCAL_FALLBACK_URL = 'ws://localhost:8080/realtime/'
|
const LOCAL_FALLBACK_URL = 'ws://localhost:8080/realtime/'
|
||||||
|
|
||||||
|
@ -16,13 +16,14 @@ const LOCAL_FALLBACK_URL = 'ws://localhost:8080/realtime/'
|
||||||
*/
|
*/
|
||||||
export const useWebsocketUrl = (): URL => {
|
export const useWebsocketUrl = (): URL => {
|
||||||
const noteId = useApplicationState((state) => state.noteDetails.id)
|
const noteId = useApplicationState((state) => state.noteDetails.id)
|
||||||
|
const baseUrl = useBaseUrl()
|
||||||
|
|
||||||
const baseUrl = useMemo(() => {
|
const websocketUrl = useMemo(() => {
|
||||||
if (isMockMode) {
|
if (isMockMode) {
|
||||||
return process.env.NEXT_PUBLIC_REALTIME_URL ?? LOCAL_FALLBACK_URL
|
return LOCAL_FALLBACK_URL
|
||||||
}
|
}
|
||||||
try {
|
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.protocol = backendBaseUrlParsed.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
backendBaseUrlParsed.pathname += 'realtime'
|
backendBaseUrlParsed.pathname += 'realtime'
|
||||||
return backendBaseUrlParsed.toString()
|
return backendBaseUrlParsed.toString()
|
||||||
|
@ -30,11 +31,11 @@ export const useWebsocketUrl = (): URL => {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
return LOCAL_FALLBACK_URL
|
return LOCAL_FALLBACK_URL
|
||||||
}
|
}
|
||||||
}, [])
|
}, [baseUrl])
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const url = new URL(baseUrl)
|
const url = new URL(websocketUrl)
|
||||||
url.search = `?noteId=${noteId}`
|
url.search = `?noteId=${noteId}`
|
||||||
return url
|
return url
|
||||||
}, [baseUrl, noteId])
|
}, [noteId, websocketUrl])
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ const customEmojis: CustomEmoji[] = ForkAwesomeIcons.map((name) => ({
|
||||||
category: 'ForkAwesome'
|
category: 'ForkAwesome'
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const EMOJI_DATA_PATH = '/_next/static/js/emoji-data.json'
|
const EMOJI_DATA_PATH = '_next/static/js/emoji-data.json'
|
||||||
|
|
||||||
const emojiPickerConfig = {
|
const emojiPickerConfig = {
|
||||||
customEmoji: customEmojis,
|
customEmoji: customEmojis,
|
||||||
|
|
|
@ -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
|
* 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 React, { createContext, useContext, useEffect, useMemo } from 'react'
|
||||||
import { RendererToEditorCommunicator } from '../../render-page/window-post-message-communicator/renderer-to-editor-communicator'
|
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 { 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<RendererToEditorCommunicator | undefined>(undefined)
|
const RendererToEditorCommunicatorContext = createContext<RendererToEditorCommunicator | undefined>(undefined)
|
||||||
|
|
||||||
|
@ -27,12 +27,13 @@ export const useRendererToEditorCommunicator: () => RendererToEditorCommunicator
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RendererToEditorCommunicatorContextProvider: React.FC<PropsWithChildren<unknown>> = ({ children }) => {
|
export const RendererToEditorCommunicatorContextProvider: React.FC<PropsWithChildren<unknown>> = ({ children }) => {
|
||||||
const editorOrigin = useOriginFromConfig(ORIGIN_TYPE.EDITOR)
|
const editorOrigin = useBaseUrl(ORIGIN.EDITOR)
|
||||||
const communicator = useMemo<RendererToEditorCommunicator>(() => new RendererToEditorCommunicator(), [])
|
const communicator = useMemo<RendererToEditorCommunicator>(() => new RendererToEditorCommunicator(), [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentCommunicator = communicator
|
const currentCommunicator = communicator
|
||||||
currentCommunicator.setMessageTarget(window.parent, editorOrigin)
|
|
||||||
|
currentCommunicator.setMessageTarget(window.parent, new URL(editorOrigin).origin)
|
||||||
currentCommunicator.registerEventListener()
|
currentCommunicator.registerEventListener()
|
||||||
currentCommunicator.enableCommunication()
|
currentCommunicator.enableCommunication()
|
||||||
currentCommunicator.sendMessageToOtherSide({
|
currentCommunicator.sendMessageToOtherSide({
|
||||||
|
|
|
@ -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])
|
|
||||||
}
|
|
|
@ -7,6 +7,7 @@
|
||||||
import type { RefObject } from 'react'
|
import type { RefObject } from 'react'
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
import { Logger } from '../../../../utils/logger'
|
import { Logger } from '../../../../utils/logger'
|
||||||
|
import { ORIGIN, useBaseUrl } from '../../../../hooks/common/use-base-url'
|
||||||
|
|
||||||
const log = new Logger('IframeLoader')
|
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.
|
* 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 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.
|
* @param onNavigateAway An optional callback that is executed when the iframe leaves the enforced URL.
|
||||||
*/
|
*/
|
||||||
export const useForceRenderPageUrlOnIframeLoadCallback = (
|
export const useForceRenderPageUrlOnIframeLoadCallback = (
|
||||||
iFrameReference: RefObject<HTMLIFrameElement>,
|
iFrameReference: RefObject<HTMLIFrameElement>,
|
||||||
rendererOrigin: string,
|
|
||||||
onNavigateAway?: () => void
|
onNavigateAway?: () => void
|
||||||
): (() => 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<boolean>(false)
|
const redirectionInProgress = useRef<boolean>(false)
|
||||||
|
|
||||||
const loadCallback = useCallback(() => {
|
const loadCallback = useCallback(() => {
|
||||||
|
|
|
@ -26,8 +26,8 @@ import { useSendScrollState } from './hooks/use-send-scroll-state'
|
||||||
import { Logger } from '../../../utils/logger'
|
import { Logger } from '../../../utils/logger'
|
||||||
import { useEffectOnRenderTypeChange } from './hooks/use-effect-on-render-type-change'
|
import { useEffectOnRenderTypeChange } from './hooks/use-effect-on-render-type-change'
|
||||||
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
|
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
|
||||||
import { ORIGIN_TYPE, useOriginFromConfig } from '../render-context/use-origin-from-config'
|
|
||||||
import { getGlobalState } from '../../../redux'
|
import { getGlobalState } from '../../../redux'
|
||||||
|
import { ORIGIN, useBaseUrl } from '../../../hooks/common/use-base-url'
|
||||||
|
|
||||||
export interface RenderIframeProps extends RendererProps {
|
export interface RenderIframeProps extends RendererProps {
|
||||||
rendererType: RendererType
|
rendererType: RendererType
|
||||||
|
@ -64,14 +64,14 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
||||||
forcedDarkMode
|
forcedDarkMode
|
||||||
}) => {
|
}) => {
|
||||||
const frameReference = useRef<HTMLIFrameElement>(null)
|
const frameReference = useRef<HTMLIFrameElement>(null)
|
||||||
const rendererOrigin = useOriginFromConfig(ORIGIN_TYPE.RENDERER)
|
const rendererBaseUrl = useBaseUrl(ORIGIN.RENDERER)
|
||||||
const iframeCommunicator = useEditorToRendererCommunicator()
|
const iframeCommunicator = useEditorToRendererCommunicator()
|
||||||
const resetRendererReady = useCallback(() => {
|
const resetRendererReady = useCallback(() => {
|
||||||
log.debug('Reset render status')
|
log.debug('Reset render status')
|
||||||
setRendererStatus(false)
|
setRendererStatus(false)
|
||||||
}, [])
|
}, [])
|
||||||
const rendererReady = useIsRendererReady()
|
const rendererReady = useIsRendererReady()
|
||||||
const onIframeLoad = useForceRenderPageUrlOnIframeLoadCallback(frameReference, rendererOrigin, resetRendererReady)
|
const onIframeLoad = useForceRenderPageUrlOnIframeLoadCallback(frameReference, resetRendererReady)
|
||||||
const [frameHeight, setFrameHeight] = useState<number>(0)
|
const [frameHeight, setFrameHeight] = useState<number>(0)
|
||||||
|
|
||||||
useEffect(() => () => setRendererStatus(false), [iframeCommunicator])
|
useEffect(() => () => setRendererStatus(false), [iframeCommunicator])
|
||||||
|
@ -124,8 +124,9 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
||||||
log.error('Load triggered without content window')
|
log.error('Load triggered without content window')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.debug(`Set iframecommunicator window with origin ${rendererOrigin ?? 'undefined'}`)
|
const origin = new URL(rendererBaseUrl).origin
|
||||||
iframeCommunicator.setMessageTarget(otherWindow, rendererOrigin)
|
log.debug(`Set iframecommunicator window with origin ${origin ?? 'undefined'}`)
|
||||||
|
iframeCommunicator.setMessageTarget(otherWindow, origin)
|
||||||
iframeCommunicator.enableCommunication()
|
iframeCommunicator.enableCommunication()
|
||||||
iframeCommunicator.sendMessageToOtherSide({
|
iframeCommunicator.sendMessageToOtherSide({
|
||||||
type: CommunicationMessageType.SET_BASE_CONFIGURATION,
|
type: CommunicationMessageType.SET_BASE_CONFIGURATION,
|
||||||
|
@ -135,7 +136,7 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
setRendererStatus(true)
|
setRendererStatus(true)
|
||||||
}, [iframeCommunicator, rendererOrigin, rendererType])
|
}, [iframeCommunicator, rendererBaseUrl, rendererType])
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffectOnRenderTypeChange(rendererType, onIframeLoad)
|
useEffectOnRenderTypeChange(rendererType, onIframeLoad)
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { customizeAssetsUrl } from '../../utils/customize-assets-url'
|
|
||||||
import { defaultConfig } from '../../api/common/default-config'
|
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
|
* @throws {Error} if the content can't be fetched
|
||||||
*/
|
*/
|
||||||
export const fetchFrontPageContent = async (): Promise<string> => {
|
export const fetchFrontPageContent = async (): Promise<string> => {
|
||||||
const response = await fetch(customizeAssetsUrl + 'intro.md', {
|
const response = await fetch('public/intro.md', {
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { Trans } from 'react-i18next'
|
||||||
import { VersionInfoModal } from './version-info-modal'
|
import { VersionInfoModal } from './version-info-modal'
|
||||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||||
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
|
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
|
||||||
|
import { Button } from 'react-bootstrap'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a link for the version info and the {@link VersionInfoModal}.
|
* Renders a link for the version info and the {@link VersionInfoModal}.
|
||||||
|
@ -18,9 +19,14 @@ export const VersionInfoLink: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<a {...cypressId('show-version-modal')} href={'#'} className={'text-light'} onClick={showModal}>
|
<Button
|
||||||
|
size={'sm'}
|
||||||
|
variant={'link'}
|
||||||
|
{...cypressId('show-version-modal')}
|
||||||
|
className={'text-light p-0'}
|
||||||
|
onClick={showModal}>
|
||||||
<Trans i18nKey={'landing.versionInfo.versionInfo'} />
|
<Trans i18nKey={'landing.versionInfo.versionInfo'} />
|
||||||
</a>
|
</Button>
|
||||||
<VersionInfoModal onHide={closeModal} show={modalVisibility} />
|
<VersionInfoModal onHide={closeModal} show={modalVisibility} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
|
|
|
@ -34,7 +34,7 @@ export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props })
|
||||||
const metadata = getOneClickProviderMetadata(oneClickProviders[0])
|
const metadata = getOneClickProviderMetadata(oneClickProviders[0])
|
||||||
return metadata.url
|
return metadata.url
|
||||||
}
|
}
|
||||||
return '/login'
|
return 'login'
|
||||||
}, [authProviders])
|
}, [authProviders])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -8,17 +8,19 @@ import React from 'react'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import { useAppTitle } from '../../hooks/common/use-app-title'
|
import { useAppTitle } from '../../hooks/common/use-app-title'
|
||||||
import { FavIcon } from './fav-icon'
|
import { FavIcon } from './fav-icon'
|
||||||
|
import { useBaseUrl } from '../../hooks/common/use-base-url'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets basic browser meta tags.
|
* Sets basic browser meta tags.
|
||||||
*/
|
*/
|
||||||
export const BaseHead: React.FC = () => {
|
export const BaseHead: React.FC = () => {
|
||||||
const appTitle = useAppTitle()
|
const appTitle = useAppTitle()
|
||||||
|
const baseUrl = useBaseUrl()
|
||||||
return (
|
return (
|
||||||
<Head>
|
<Head>
|
||||||
<title>{appTitle}</title>
|
<title>{appTitle}</title>
|
||||||
<FavIcon />
|
<FavIcon />
|
||||||
|
<base href={baseUrl} />
|
||||||
<meta content='width=device-width, initial-scale=1' name='viewport' />
|
<meta content='width=device-width, initial-scale=1' name='viewport' />
|
||||||
</Head>
|
</Head>
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,17 +12,17 @@ import React, { Fragment } from 'react'
|
||||||
export const FavIcon: React.FC = () => {
|
export const FavIcon: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<link href='/icons/apple-touch-icon.png' rel='apple-touch-icon' sizes='180x180' />
|
<link href='icons/apple-touch-icon.png' rel='apple-touch-icon' sizes='180x180' />
|
||||||
<link href='/icons/favicon-32x32.png' rel='icon' sizes='32x32' type='image/png' />
|
<link href='icons/favicon-32x32.png' rel='icon' sizes='32x32' type='image/png' />
|
||||||
<link href='/icons/favicon-16x16.png' rel='icon' sizes='16x16' type='image/png' />
|
<link href='icons/favicon-16x16.png' rel='icon' sizes='16x16' type='image/png' />
|
||||||
<link href='/icons/site.webmanifest' rel='manifest' />
|
<link href='icons/site.webmanifest' rel='manifest' />
|
||||||
<link href='/icons/favicon.ico' rel='shortcut icon' />
|
<link href='icons/favicon.ico' rel='shortcut icon' />
|
||||||
<link color='#b51f08' href='/icons/safari-pinned-tab.svg' rel='mask-icon' />
|
<link color='#b51f08' href='icons/safari-pinned-tab.svg' rel='mask-icon' />
|
||||||
<meta name='apple-mobile-web-app-title' content='HedgeDoc' />
|
<meta name='apple-mobile-web-app-title' content='HedgeDoc' />
|
||||||
<meta name='application-name' content='HedgeDoc' />
|
<meta name='application-name' content='HedgeDoc' />
|
||||||
<meta name='msapplication-TileColor' content='#b51f08' />
|
<meta name='msapplication-TileColor' content='#b51f08' />
|
||||||
<meta name='theme-color' content='#b51f08' />
|
<meta name='theme-color' content='#b51f08' />
|
||||||
<meta content='/icons/browserconfig.xml' name='msapplication-config' />
|
<meta content='icons/browserconfig.xml' name='msapplication-config' />
|
||||||
<meta content='HedgeDoc - Collaborative markdown notes' name='description' />
|
<meta content='HedgeDoc - Collaborative markdown notes' name='description' />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,7 +7,6 @@ import type { AuthProvider } from '../../../../api/config/types'
|
||||||
import { AuthProviderType } from '../../../../api/config/types'
|
import { AuthProviderType } from '../../../../api/config/types'
|
||||||
import type { IconName } from '../../../common/fork-awesome/types'
|
import type { IconName } from '../../../common/fork-awesome/types'
|
||||||
import styles from '../via-one-click.module.scss'
|
import styles from '../via-one-click.module.scss'
|
||||||
import { backendUrl } from '../../../../utils/backend-url'
|
|
||||||
import { Logger } from '../../../../utils/logger'
|
import { Logger } from '../../../../utils/logger'
|
||||||
|
|
||||||
export interface OneClickMetadata {
|
export interface OneClickMetadata {
|
||||||
|
@ -18,7 +17,7 @@ export interface OneClickMetadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getBackendAuthUrl = (providerIdentifer: string): string => {
|
const getBackendAuthUrl = (providerIdentifer: string): string => {
|
||||||
return `${backendUrl}auth/${providerIdentifer}`
|
return `auth/${providerIdentifer}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const logger = new Logger('GetOneClickProviderMetadata')
|
const logger = new Logger('GetOneClickProviderMetadata')
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { Button, Card } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||||
import { AccountDeletionModal } from './account-deletion-modal'
|
import { AccountDeletionModal } from './account-deletion-modal'
|
||||||
import { apiUrl } from '../../../utils/api-url'
|
|
||||||
import { useBooleanState } from '../../../hooks/common/use-boolean-state'
|
import { useBooleanState } from '../../../hooks/common/use-boolean-state'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,7 +25,7 @@ export const ProfileAccountManagement: React.FC = () => {
|
||||||
<Card.Title>
|
<Card.Title>
|
||||||
<Trans i18nKey='profile.accountManagement' />
|
<Trans i18nKey='profile.accountManagement' />
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
<Button variant='secondary' block href={apiUrl + 'me/export'} className='mb-2'>
|
<Button variant='secondary' block href={'me/export'} className='mb-2'>
|
||||||
<ForkAwesomeIcon icon='cloud-download' fixedWidth={true} className='mx-2' />
|
<ForkAwesomeIcon icon='cloud-download' fixedWidth={true} className='mx-2' />
|
||||||
<Trans i18nKey='profile.exportUserData' />
|
<Trans i18nKey='profile.exportUserData' />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -128,7 +128,9 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!baseConfiguration) {
|
if (!baseConfiguration) {
|
||||||
return null
|
return (
|
||||||
|
<span>This is the render endpoint. If you can read this text then please check your HedgeDoc configuration.</span>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (baseConfiguration.rendererType) {
|
switch (baseConfiguration.rendererType) {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import { isMockMode } from '../utils/test-modes'
|
||||||
|
|
||||||
export enum HttpMethod {
|
export enum HttpMethod {
|
||||||
GET = 'GET',
|
GET = 'GET',
|
||||||
|
@ -31,6 +32,10 @@ export const respondToMatchingRequest = <T>(
|
||||||
response: T,
|
response: T,
|
||||||
statusCode = 200
|
statusCode = 200
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
if (!isMockMode) {
|
||||||
|
res.status(404).send('Mock API is disabled')
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (method !== req.method) {
|
if (method !== req.method) {
|
||||||
res.status(405).send('Method not allowed')
|
res.status(405).send('Method not allowed')
|
||||||
return false
|
return false
|
||||||
|
|
33
src/hooks/common/use-base-url.tsx
Normal file
|
@ -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])
|
||||||
|
}
|
|
@ -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])
|
|
||||||
}
|
|
|
@ -3,33 +3,58 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 { ErrorBoundary } from '../components/error-boundary/error-boundary'
|
||||||
import { ApplicationLoader } from '../components/application-loader/application-loader'
|
import { ApplicationLoader } from '../components/application-loader/application-loader'
|
||||||
import '../../global-styles/dark.scss'
|
import '../../global-styles/dark.scss'
|
||||||
import '../../global-styles/index.scss'
|
import '../../global-styles/index.scss'
|
||||||
import type { NextPage } from 'next'
|
|
||||||
import { BaseHead } from '../components/layout/base-head'
|
import { BaseHead } from '../components/layout/base-head'
|
||||||
import { StoreProvider } from '../redux/store-provider'
|
import { StoreProvider } from '../redux/store-provider'
|
||||||
import { UiNotificationBoundary } from '../components/notifications/ui-notification-boundary'
|
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.
|
* The actual hedgedoc next js app.
|
||||||
* Provides necessary wrapper components to every page.
|
* Provides necessary wrapper components to every page.
|
||||||
*/
|
*/
|
||||||
const HedgeDocApp: NextPage<AppProps> = ({ Component, pageProps }: AppProps) => {
|
function HedgeDocApp({ Component, pageProps }: AppProps<AppPageProps>) {
|
||||||
return (
|
return (
|
||||||
<StoreProvider>
|
<BaseUrlContextProvider baseUrls={pageProps.baseUrls}>
|
||||||
<BaseHead />
|
<StoreProvider>
|
||||||
<ApplicationLoader>
|
<ExpectedOriginBoundary>
|
||||||
<ErrorBoundary>
|
<BaseHead />
|
||||||
<UiNotificationBoundary>
|
<ApplicationLoader>
|
||||||
<Component {...pageProps} />
|
<ErrorBoundary>
|
||||||
</UiNotificationBoundary>
|
<UiNotificationBoundary>
|
||||||
</ErrorBoundary>
|
<Component {...pageProps} />
|
||||||
</ApplicationLoader>
|
</UiNotificationBoundary>
|
||||||
</StoreProvider>
|
</ErrorBoundary>
|
||||||
|
</ApplicationLoader>
|
||||||
|
</ExpectedOriginBoundary>
|
||||||
|
</StoreProvider>
|
||||||
|
</BaseUrlContextProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const baseUrlFromEnvExtractor: BaseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
|
||||||
|
|
||||||
|
HedgeDocApp.getInitialProps = (): AppInitialProps<AppPageProps> => {
|
||||||
|
const baseUrls = baseUrlFromEnvExtractor.extractBaseUrls().orElse(undefined)
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageProps: {
|
||||||
|
baseUrls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
export default HedgeDocApp
|
export default HedgeDocApp
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import type { Config } from '../../../../api/config/types'
|
import type { Config } from '../../../api/config/types'
|
||||||
import { AuthProviderType } from '../../../../api/config/types'
|
import { AuthProviderType } from '../../../api/config/types'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../handler-utils/respond-to-matching-request'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
respondToMatchingRequest<Config>(HttpMethod.GET, req, res, {
|
respondToMatchingRequest<Config>(HttpMethod.GET, req, res, {
|
||||||
|
@ -54,7 +54,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
],
|
],
|
||||||
branding: {
|
branding: {
|
||||||
name: 'DEMO Corp',
|
name: 'DEMO Corp',
|
||||||
logo: '/mock-public/img/demo.png'
|
logo: 'public/img/demo.png'
|
||||||
},
|
},
|
||||||
useImageProxy: false,
|
useImageProxy: false,
|
||||||
specialUrls: {
|
specialUrls: {
|
||||||
|
@ -69,11 +69,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
commit: 'mock'
|
commit: 'mock'
|
||||||
},
|
},
|
||||||
plantumlServer: 'https://www.plantuml.com/plantuml',
|
plantumlServer: 'https://www.plantuml.com/plantuml',
|
||||||
maxDocumentLength: 1000000,
|
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/'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
|
||||||
import type { GroupInfo } from '../../../../../api/group/types'
|
import type { GroupInfo } from '../../../../api/group/types'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
respondToMatchingRequest<GroupInfo>(HttpMethod.GET, req, res, {
|
respondToMatchingRequest<GroupInfo>(HttpMethod.GET, req, res, {
|
|
@ -4,8 +4,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
|
||||||
import type { GroupInfo } from '../../../../../api/group/types'
|
import type { GroupInfo } from '../../../../api/group/types'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
respondToMatchingRequest<GroupInfo>(HttpMethod.GET, req, res, {
|
respondToMatchingRequest<GroupInfo>(HttpMethod.GET, req, res, {
|
|
@ -4,8 +4,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
|
||||||
import type { GroupInfo } from '../../../../../api/group/types'
|
import type { GroupInfo } from '../../../../api/group/types'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
respondToMatchingRequest<GroupInfo>(HttpMethod.GET, req, res, {
|
respondToMatchingRequest<GroupInfo>(HttpMethod.GET, req, res, {
|
|
@ -5,8 +5,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
|
||||||
import type { HistoryEntry } from '../../../../../api/history/types'
|
import type { HistoryEntry } from '../../../../api/history/types'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
respondToMatchingRequest<HistoryEntry[]>(HttpMethod.GET, req, res, [
|
respondToMatchingRequest<HistoryEntry[]>(HttpMethod.GET, req, res, [
|
|
@ -5,13 +5,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
|
||||||
import type { LoginUserInfo } from '../../../../../api/me/types'
|
import type { LoginUserInfo } from '../../../../api/me/types'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
||||||
respondToMatchingRequest<LoginUserInfo>(HttpMethod.GET, req, res, {
|
respondToMatchingRequest<LoginUserInfo>(HttpMethod.GET, req, res, {
|
||||||
username: 'mock',
|
username: 'mock',
|
||||||
photo: '/mock-public/img/avatar.png',
|
photo: 'public/img/avatar.png',
|
||||||
displayName: 'Mock User',
|
displayName: 'Mock User',
|
||||||
authProvider: 'local',
|
authProvider: 'local',
|
||||||
email: 'mock@hedgedoc.test'
|
email: 'mock@hedgedoc.test'
|
|
@ -5,8 +5,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
|
||||||
import type { MediaUpload } from '../../../../../api/media/types'
|
import type { MediaUpload } from '../../../../api/media/types'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
|
@ -5,9 +5,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import type { MediaUpload } from '../../../../api/media/types'
|
import type { MediaUpload } from '../../../api/media/types'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../handler-utils/respond-to-matching-request'
|
||||||
import { isMockMode, isTestMode } from '../../../../utils/test-modes'
|
import { isMockMode, isTestMode } from '../../../utils/test-modes'
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
|
||||||
if (isMockMode && !isTestMode) {
|
if (isMockMode && !isTestMode) {
|
||||||
|
@ -21,7 +21,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse): Promise<void>
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
{
|
{
|
||||||
url: '/mock-public/img/avatar.png',
|
url: 'public/img/avatar.png',
|
||||||
noteId: null,
|
noteId: null,
|
||||||
username: 'test',
|
username: 'test',
|
||||||
createdAt: '2022-02-27T21:54:23.856Z'
|
createdAt: '2022-02-27T21:54:23.856Z'
|
54
src/pages/api/private/notes/features/index.ts
Normal file
|
@ -5,8 +5,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../../../../handler-utils/respond-to-matching-request'
|
||||||
import type { RevisionDetails } from '../../../../../../../api/revisions/types'
|
import type { RevisionDetails } from '../../../../../../api/revisions/types'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
||||||
respondToMatchingRequest<RevisionDetails>(HttpMethod.GET, req, res, {
|
respondToMatchingRequest<RevisionDetails>(HttpMethod.GET, req, res, {
|
|
@ -4,8 +4,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../../../../handler-utils/respond-to-matching-request'
|
||||||
import type { RevisionDetails } from '../../../../../../../api/revisions/types'
|
import type { RevisionDetails } from '../../../../../../api/revisions/types'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
||||||
respondToMatchingRequest<RevisionDetails>(HttpMethod.GET, req, res, {
|
respondToMatchingRequest<RevisionDetails>(HttpMethod.GET, req, res, {
|
|
@ -5,8 +5,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../../../../handler-utils/respond-to-matching-request'
|
||||||
import type { RevisionMetadata } from '../../../../../../../api/revisions/types'
|
import type { RevisionMetadata } from '../../../../../../api/revisions/types'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
||||||
respondToMatchingRequest<RevisionMetadata[]>(HttpMethod.GET, req, res, [
|
respondToMatchingRequest<RevisionMetadata[]>(HttpMethod.GET, req, res, [
|
|
@ -5,8 +5,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
|
||||||
import type { Note } from '../../../../../api/notes/types'
|
import type { Note } from '../../../../api/notes/types'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
||||||
respondToMatchingRequest<Note>(
|
respondToMatchingRequest<Note>(
|
|
@ -5,8 +5,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request'
|
||||||
import type { Note } from '../../../../../../api/notes/types'
|
import type { Note } from '../../../../../api/notes/types'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
||||||
respondToMatchingRequest<Note>(HttpMethod.GET, req, res, {
|
respondToMatchingRequest<Note>(HttpMethod.GET, req, res, {
|
|
@ -4,8 +4,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import type { AccessToken } from '../../../../api/tokens/types'
|
import type { AccessToken } from '../../../api/tokens/types'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../handler-utils/respond-to-matching-request'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
respondToMatchingRequest<AccessToken[]>(HttpMethod.GET, req, res, [
|
respondToMatchingRequest<AccessToken[]>(HttpMethod.GET, req, res, [
|
|
@ -4,14 +4,14 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
|
||||||
import type { UserInfo } from '../../../../../api/users/types'
|
import type { UserInfo } from '../../../../api/users/types'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
||||||
respondToMatchingRequest<UserInfo>(HttpMethod.GET, req, res, {
|
respondToMatchingRequest<UserInfo>(HttpMethod.GET, req, res, {
|
||||||
username: 'erik',
|
username: 'erik',
|
||||||
displayName: 'Erik',
|
displayName: 'Erik',
|
||||||
photo: '/mock-public/img/avatar.png'
|
photo: 'public/img/avatar.png'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
|
||||||
import type { UserInfo } from '../../../../../api/users/types'
|
import type { UserInfo } from '../../../../api/users/types'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
||||||
respondToMatchingRequest<UserInfo>(HttpMethod.GET, req, res, {
|
respondToMatchingRequest<UserInfo>(HttpMethod.GET, req, res, {
|
||||||
username: 'molly',
|
username: 'molly',
|
||||||
displayName: 'Molly',
|
displayName: 'Molly',
|
||||||
photo: '/mock-public/img/avatar.png'
|
photo: 'public/img/avatar.png'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,14 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { HttpMethod, respondToMatchingRequest } from '../../../../../handler-utils/respond-to-matching-request'
|
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
|
||||||
import type { UserInfo } from '../../../../../api/users/types'
|
import type { UserInfo } from '../../../../api/users/types'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
||||||
respondToMatchingRequest<UserInfo>(HttpMethod.GET, req, res, {
|
respondToMatchingRequest<UserInfo>(HttpMethod.GET, req, res, {
|
||||||
username: 'tilman',
|
username: 'tilman',
|
||||||
displayName: 'Tilman',
|
displayName: 'Tilman',
|
||||||
photo: '/mock-public/img/avatar.png'
|
photo: 'public/img/avatar.png'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -29,11 +29,7 @@ export const initialState: Config = {
|
||||||
patch: 0
|
patch: 0
|
||||||
},
|
},
|
||||||
plantumlServer: undefined,
|
plantumlServer: undefined,
|
||||||
maxDocumentLength: 0,
|
maxDocumentLength: 0
|
||||||
iframeCommunication: {
|
|
||||||
editorOrigin: '',
|
|
||||||
rendererOrigin: ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConfigReducer: Reducer<Config, ConfigActions> = (state: Config = initialState, action: ConfigActions) => {
|
export const ConfigReducer: Reducer<Config, ConfigActions> = (state: Config = initialState, action: ConfigActions) => {
|
||||||
|
|
|
@ -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/`
|
|
|
@ -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()
|
|
68
src/utils/base-url-from-env-extractor.test.ts
Normal file
|
@ -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/'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
89
src/utils/base-url-from-env-extractor.ts
Normal file
|
@ -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<BaseUrls> | undefined
|
||||||
|
private logger = new Logger('Base URL Configuration')
|
||||||
|
|
||||||
|
private extractUrlFromEnvVar(envVarName: string, envVarValue: string | undefined): Optional<URL> {
|
||||||
|
return Optional.ofNullable(envVarValue)
|
||||||
|
.filter((value) => {
|
||||||
|
const endsWithSlash = value.endsWith('/')
|
||||||
|
if (!endsWithSlash) {
|
||||||
|
this.logger.error(`${envVarName} must end with an '/'`)
|
||||||
|
}
|
||||||
|
return endsWithSlash
|
||||||
|
})
|
||||||
|
.map((value) => {
|
||||||
|
try {
|
||||||
|
return new URL(value)
|
||||||
|
} catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractEditorBaseUrlFromEnv(): Optional<URL> {
|
||||||
|
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<URL> {
|
||||||
|
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<BaseUrls> {
|
||||||
|
if (!this.isEnvironmentExtractDone()) {
|
||||||
|
this.renewBaseUrls()
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(this.baseUrls).flatMap((value) => value)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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/`
|
|
|
@ -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'
|
|
||||||
}
|
|
44
src/utils/test-modes.js
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -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'
|
|
27
src/utils/uri-origin-boundary.tsx
Normal file
|
@ -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<PropsWithChildren> = ({ children }) => {
|
||||||
|
const baseUrl = useBaseUrl()
|
||||||
|
const expectedOrigin = useMemo(() => new URL(baseUrl).origin, [baseUrl])
|
||||||
|
|
||||||
|
if (isClientSideRendering() && window.location.origin !== expectedOrigin) {
|
||||||
|
return <span>{`You can't open this page using this URL. For this endpoint "${expectedOrigin}" is expected.`}</span>
|
||||||
|
} else {
|
||||||
|
return <Fragment>{children}</Fragment>
|
||||||
|
}
|
||||||
|
}
|