feat: fetch frontend config in server side rendering

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-04-04 17:27:20 +02:00
parent 312d1adf6f
commit 24f1b2a361
41 changed files with 270 additions and 220 deletions

View file

@ -1,18 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { getConfig } from '../../../api/config'
import { setConfig } from '../../../redux/config/methods'
/**
* Get the {@link Config frontend config} and save it in the global application state.
*/
export const fetchFrontendConfig = async (): Promise<void> => {
const config = await getConfig()
if (!config) {
return Promise.reject(new Error('Config empty!'))
}
setConfig(config)
}

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -7,7 +7,6 @@ import { refreshHistoryState } from '../../../redux/history/methods'
import { Logger } from '../../../utils/logger'
import { isDevMode, isTestMode } from '../../../utils/test-modes'
import { fetchAndSetUser } from '../../login-page/auth/utils'
import { fetchFrontendConfig } from './fetch-frontend-config'
import { loadDarkMode } from './load-dark-mode'
import { setUpI18n } from './setupI18n'
@ -55,10 +54,6 @@ export const createSetUpTaskList = (): InitTask[] => {
name: 'Load Translations',
task: setUpI18n
},
{
name: 'Load config',
task: fetchFrontendConfig
},
{
name: 'Fetch user information',
task: fetchUserInformation

View file

@ -1,10 +1,10 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { createContext, useState } from 'react'
import type { PropsWithChildren } from 'react'
import React, { createContext, useState } from 'react'
export interface BaseUrls {
renderer: string
@ -28,9 +28,8 @@ export const BaseUrlContextProvider: React.FC<PropsWithChildren<BaseUrlContextPr
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>
<span className={'text-white bg-dark'}>HedgeDoc is not configured correctly! Please check the server log.</span>
) : (
<baseUrlContext.Provider value={baseUrlState}>{children}</baseUrlContext.Provider>
)

View file

@ -1,9 +1,9 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { useFrontendConfig } from '../frontend-config-context/use-frontend-config'
import { ShowIf } from '../show-if/show-if'
import styles from './branding.module.scss'
import React, { useMemo } from 'react'
@ -21,7 +21,7 @@ export interface BrandingProps {
* @param delimiter If the delimiter between the HedgeDoc logo and the branding should be shown.
*/
export const Branding: React.FC<BrandingProps> = ({ inline = false, delimiter = true }) => {
const branding = useApplicationState((state) => state.config.branding)
const branding = useFrontendConfig().branding
const showBranding = !!branding.name || !!branding.logo
const brandingDom = useMemo(() => {

View file

@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { FrontendConfig } from '../../../api/config/types'
import { createContext } from 'react'
export const frontendConfigContext = createContext<FrontendConfig | undefined>(undefined)

View file

@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { getConfig } from '../../../api/config'
import type { FrontendConfig } from '../../../api/config/types'
import { useBaseUrl } from '../../../hooks/common/use-base-url'
import { Logger } from '../../../utils/logger'
import { frontendConfigContext } from './context'
import type { PropsWithChildren } from 'react'
import React, { useEffect, useState } from 'react'
const logger = new Logger('FrontendConfigContextProvider')
interface FrontendConfigContextProviderProps extends PropsWithChildren {
config?: FrontendConfig
}
/**
* Provides the given frontend configuration in a context or renders an error message otherwise.
*
* @param config the frontend config to provoide
* @param children the react elements to show if the config is valid
*/
export const FrontendConfigContextProvider: React.FC<FrontendConfigContextProviderProps> = ({ config, children }) => {
const [configState, setConfigState] = useState<undefined | FrontendConfig>(() => config)
const baseUrl = useBaseUrl()
useEffect(() => {
if (config === undefined && configState === undefined) {
logger.debug('Fetching Config client side')
getConfig(baseUrl)
.then((config) => setConfigState(config))
.catch((error) => logger.error(error))
}
}, [baseUrl, config, configState])
return configState === undefined ? (
<span className={'text-white bg-dark'}>No frontend config received! Please check the server log.</span>
) : (
<frontendConfigContext.Provider value={configState}>{children}</frontendConfigContext.Provider>
)
}

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { FrontendConfig } from '../../../api/config/types'
import { frontendConfigContext } from './context'
import { Optional } from '@mrdrogdrog/optional'
import { useContext } from 'react'
/**
* Retrieves the current frontend config from the next react context.
*/
export const useFrontendConfig = (): FrontendConfig => {
return Optional.ofNullable(useContext(frontendConfigContext)).orElseThrow(
() => new Error('No frontend config context found. Did you forget to use the provider component?')
)
}

View file

@ -22,7 +22,7 @@ jest.mock('../../../hooks/common/use-base-url')
describe('motd modal', () => {
beforeAll(async () => {
jest.spyOn(UseBaseUrlModule, 'useBaseUrl').mockImplementation(() => 'https://example.org')
jest.spyOn(UseBaseUrlModule, 'useBaseUrl').mockImplementation(() => new URL('https://example.org'))
await mockI18n()
})

View file

@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../../../hooks/common/use-application-state'
import { useBaseUrl } from '../../../../hooks/common/use-base-url'
import { CopyableField } from '../../../common/copyable/copyable-field/copyable-field'
import React, { useMemo } from 'react'
export enum LinkType {
EDITOR = 'n',
SLIDESHOW = 'p',
DOCUMENT = 's'
}
export interface LinkFieldProps {
type: LinkType
}
/**
* Renders a specific URL to the current note.
* @param type defines the URL type. (editor, read only document, slideshow, etc.)
*/
export const NoteUrlField: React.FC<LinkFieldProps> = ({ type }) => {
const baseUrl = useBaseUrl()
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
const url = useMemo(() => {
const url = new URL(baseUrl)
url.pathname += `${type}/${noteIdentifier}`
return url.toString()
}, [baseUrl, noteIdentifier, type])
return <CopyableField content={url} shareOriginUrl={url} />
}

View file

@ -4,11 +4,10 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../../../hooks/common/use-application-state'
import { useBaseUrl } from '../../../../hooks/common/use-base-url'
import { CopyableField } from '../../../common/copyable/copyable-field/copyable-field'
import type { ModalVisibilityProps } from '../../../common/modals/common-modal'
import { CommonModal } from '../../../common/modals/common-modal'
import { ShowIf } from '../../../common/show-if/show-if'
import { LinkType, NoteUrlField } from './note-url-field'
import { NoteType } from '@hedgedoc/commons'
import React from 'react'
import { Modal } from 'react-bootstrap'
@ -23,21 +22,19 @@ import { Trans, useTranslation } from 'react-i18next'
export const ShareModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
useTranslation()
const noteFrontmatter = useApplicationState((state) => state.noteDetails.frontmatter)
const baseUrl = useBaseUrl()
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
return (
<CommonModal show={show} onHide={onHide} showCloseButton={true} titleI18nKey={'editor.modal.shareLink.title'}>
<Modal.Body>
<Trans i18nKey={'editor.modal.shareLink.editorDescription'} />
<CopyableField content={`${baseUrl}n/${noteIdentifier}`} shareOriginUrl={`${baseUrl}n/${noteIdentifier}`} />
<NoteUrlField type={LinkType.EDITOR} />
<ShowIf condition={noteFrontmatter.type === NoteType.SLIDE}>
<Trans i18nKey={'editor.modal.shareLink.slidesDescription'} />
<CopyableField content={`${baseUrl}p/${noteIdentifier}`} shareOriginUrl={`${baseUrl}p/${noteIdentifier}`} />
<NoteUrlField type={LinkType.SLIDESHOW} />
</ShowIf>
<ShowIf condition={noteFrontmatter.type === NoteType.DOCUMENT}>
<Trans i18nKey={'editor.modal.shareLink.viewOnlyDescription'} />
<CopyableField content={`${baseUrl}s/${noteIdentifier}`} shareOriginUrl={`${baseUrl}s/${noteIdentifier}`} />
<NoteUrlField type={LinkType.DOCUMENT} />
</ShowIf>
</Modal.Body>
</CommonModal>

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -22,7 +22,7 @@ export const useWebsocketUrl = (): URL | undefined => {
return LOCAL_FALLBACK_URL
}
try {
const backendBaseUrlParsed = new URL(baseUrl, window.location.toString())
const backendBaseUrlParsed = new URL(baseUrl)
backendBaseUrlParsed.protocol = backendBaseUrlParsed.protocol === 'https:' ? 'wss:' : 'ws:'
backendBaseUrlParsed.pathname += 'realtime'
return backendBaseUrlParsed.toString()

View file

@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../../../hooks/common/use-application-state'
import { cypressId } from '../../../../utils/cypress-attribute'
import { useFrontendConfig } from '../../../common/frontend-config-context/use-frontend-config'
import type { ModalVisibilityProps } from '../../../common/modals/common-modal'
import { CommonModal } from '../../../common/modals/common-modal'
import React from 'react'
@ -19,7 +19,7 @@ import { Trans, useTranslation } from 'react-i18next'
*/
export const MaxLengthWarningModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
useTranslation()
const maxDocumentLength = useApplicationState((state) => state.config.maxDocumentLength)
const maxDocumentLength = useFrontendConfig().maxDocumentLength
return (
<CommonModal

View file

@ -1,11 +1,11 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../../../hooks/common/use-application-state'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
import { useNoteMarkdownContent } from '../../../../hooks/common/use-note-markdown-content'
import { useFrontendConfig } from '../../../common/frontend-config-context/use-frontend-config'
import { MaxLengthWarningModal } from './max-length-warning-modal'
import React, { useEffect, useRef } from 'react'
@ -15,7 +15,7 @@ import React, { useEffect, useRef } from 'react'
export const MaxLengthWarning: React.FC = () => {
const [modalVisibility, showModal, closeModal] = useBooleanState()
const maxLengthWarningAlreadyShown = useRef(false)
const maxDocumentLength = useApplicationState((state) => state.config.maxDocumentLength)
const maxDocumentLength = useFrontendConfig().maxDocumentLength
const markdownContent = useNoteMarkdownContent()
useEffect(() => {

View file

@ -1,10 +1,11 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../../../hooks/common/use-application-state'
import { cypressId } from '../../../../utils/cypress-attribute'
import { useFrontendConfig } from '../../../common/frontend-config-context/use-frontend-config'
import React, { useMemo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
@ -14,7 +15,7 @@ import { Trans, useTranslation } from 'react-i18next'
export const RemainingCharactersInfo: React.FC = () => {
const { t } = useTranslation()
const maxDocumentLength = useApplicationState((state) => state.config.maxDocumentLength)
const maxDocumentLength = useFrontendConfig().maxDocumentLength
const contentLength = useApplicationState((state) => state.noteDetails.markdownContent.plain.length)
const remainingCharacters = useMemo(() => maxDocumentLength - contentLength, [contentLength, maxDocumentLength])

View file

@ -1,15 +1,15 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../../hooks/common/use-application-state'
import links from '../../../links.json'
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
import { ExternalLink } from '../../common/links/external-link'
import { TranslatedExternalLink } from '../../common/links/translated-external-link'
import { TranslatedInternalLink } from '../../common/links/translated-internal-link'
import { VersionInfoLink } from './version-info/version-info-link'
import React, { Fragment } from 'react'
import React, { Fragment, useMemo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
/**
@ -18,8 +18,11 @@ import { Trans, useTranslation } from 'react-i18next'
export const PoweredByLinks: React.FC = () => {
useTranslation()
const specialUrls: [string, string][] = useApplicationState((state) =>
Object.entries(state.config.specialUrls).map(([i18nkey, url]) => [i18nkey, String(url)])
const rawSpecialUrls = useFrontendConfig().specialUrls
const specialUrls = useMemo(
() => Object.entries(rawSpecialUrls).map(([i18nkey, url]) => [i18nkey, String(url)]),
[rawSpecialUrls]
)
return (

View file

@ -4,10 +4,10 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { BackendVersion } from '../../../../api/config/types'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
import links from '../../../../links.json'
import { cypressId } from '../../../../utils/cypress-attribute'
import { CopyableField } from '../../../common/copyable/copyable-field/copyable-field'
import { useFrontendConfig } from '../../../common/frontend-config-context/use-frontend-config'
import { TranslatedExternalLink } from '../../../common/links/translated-external-link'
import type { CommonModalProps } from '../../../common/modals/common-modal'
import { CommonModal } from '../../../common/modals/common-modal'
@ -22,7 +22,7 @@ import { Modal } from 'react-bootstrap'
* @param show If the modal should be shown.
*/
export const VersionInfoModal: React.FC<CommonModalProps> = ({ onHide, show }) => {
const serverVersion: BackendVersion = useApplicationState((state) => state.config.version)
const serverVersion: BackendVersion = useFrontendConfig().version
const backendVersion = useMemo(() => {
const version = `${serverVersion.major}.${serverVersion.minor}.${serverVersion.patch}`

View file

@ -1,10 +1,10 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { cypressId } from '../../../utils/cypress-attribute'
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
import { ShowIf } from '../../common/show-if/show-if'
import { filterOneClickProviders } from '../../login-page/auth/utils'
import { getOneClickProviderMetadata } from '../../login-page/auth/utils/get-one-click-provider-metadata'
@ -25,7 +25,7 @@ export type SignInButtonProps = Omit<ButtonProps, 'href'>
*/
export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props }) => {
const { t } = useTranslation()
const authProviders = useApplicationState((state) => state.config.authProviders)
const authProviders = useFrontendConfig().authProviders
const loginLink = useMemo(() => {
const oneClickProviders = authProviders.filter(filterOneClickProviders)

View file

@ -1,12 +1,12 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { doLocalLogin } from '../../../api/auth/local'
import { ErrorToI18nKeyMapper } from '../../../api/common/error-to-i18n-key-mapper'
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
import { ShowIf } from '../../common/show-if/show-if'
import { PasswordField } from './fields/password-field'
import { UsernameField } from './fields/username-field'
@ -25,7 +25,7 @@ export const ViaLocal: React.FC = () => {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string>()
const allowRegister = useApplicationState((state) => state.config.allowRegister)
const allowRegister = useFrontendConfig().allowRegister
const onLoginSubmit = useCallback(
(event: FormEvent) => {

View file

@ -1,11 +1,11 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { getProxiedUrl } from '../../../../api/media'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
import { Logger } from '../../../../utils/logger'
import { useFrontendConfig } from '../../../common/frontend-config-context/use-frontend-config'
import React, { useEffect, useState } from 'react'
const log = new Logger('ProxyImageFrame')
@ -20,7 +20,7 @@ const log = new Logger('ProxyImageFrame')
*/
export const ProxyImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = ({ src, title, alt, ...props }) => {
const [imageUrl, setImageUrl] = useState('')
const imageProxyEnabled = useApplicationState((state) => state.config.useImageProxy)
const imageProxyEnabled = useFrontendConfig().useImageProxy
useEffect(() => {
if (!imageProxyEnabled || !src) {

View file

@ -3,18 +3,18 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { MarkdownRendererExtensionOptions } from '../../../../extensions/base/app-extension'
import { AppExtension } from '../../../../extensions/base/app-extension'
import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
import { basicCompletion } from '../../../editor-page/editor-pane/autocompletions/basic-completion'
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { TableOfContentsMarkdownExtension } from './table-of-contents-markdown-extension'
import type { CompletionSource } from '@codemirror/autocomplete'
import type EventEmitter2 from 'eventemitter2'
import { t } from 'i18next'
export class TableOfContentsAppExtension extends AppExtension {
buildMarkdownRendererExtensions(eventEmitter?: EventEmitter2): MarkdownRendererExtension[] {
return [new TableOfContentsMarkdownExtension(eventEmitter)]
buildMarkdownRendererExtensions(options: MarkdownRendererExtensionOptions): MarkdownRendererExtension[] {
return [new TableOfContentsMarkdownExtension(options.eventEmitter)]
}
buildCheatsheetExtensions(): CheatsheetExtension[] {

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { optionalAppExtensions } from '../../../extensions/extra-integrations/optional-app-extensions'
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
import { DebuggerMarkdownExtension } from '../extensions/debugger-markdown-extension'
import { ProxyImageMarkdownExtension } from '../extensions/image/proxy-image-markdown-extension'
@ -25,9 +26,15 @@ export const useMarkdownExtensions = (
additionalExtensions: MarkdownRendererExtension[]
): MarkdownRendererExtension[] => {
const extensionEventEmitter = useExtensionEventEmitter()
const frontendConfig = useFrontendConfig()
return useMemo(() => {
return [
...optionalAppExtensions.flatMap((extension) => extension.buildMarkdownRendererExtensions(extensionEventEmitter)),
...optionalAppExtensions.flatMap((extension) =>
extension.buildMarkdownRendererExtensions({
frontendConfig: frontendConfig,
eventEmitter: extensionEventEmitter
})
),
...additionalExtensions,
new UploadIndicatingImageFrameMarkdownExtension(),
new LinkAdjustmentMarkdownExtension(baseUrl),
@ -35,5 +42,5 @@ export const useMarkdownExtensions = (
new DebuggerMarkdownExtension(),
new ProxyImageMarkdownExtension()
]
}, [additionalExtensions, baseUrl, extensionEventEmitter])
}, [additionalExtensions, baseUrl, extensionEventEmitter, frontendConfig])
}

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../hooks/common/use-application-state'
import { useFrontendConfig } from '../common/frontend-config-context/use-frontend-config'
import { TranslatedExternalLink } from '../common/links/translated-external-link'
import { ShowIf } from '../common/show-if/show-if'
import React from 'react'
@ -14,7 +14,7 @@ import { Trans, useTranslation } from 'react-i18next'
*/
export const RegisterInfos: React.FC = () => {
useTranslation()
const specialUrls = useApplicationState((state) => state.config.specialUrls)
const specialUrls = useFrontendConfig().specialUrls
return (
<ShowIf condition={!!specialUrls.termsOfUse || !!specialUrls.privacy}>