mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-01-05 12:50:59 +00:00
feat: merge login, register and intro page
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
76ef2511e7
commit
56643ffd85
39 changed files with 533 additions and 374 deletions
|
@ -147,7 +147,7 @@ describe('History', () => {
|
||||||
body: []
|
body: []
|
||||||
})
|
})
|
||||||
cy.visitHistory()
|
cy.visitHistory()
|
||||||
cy.logout()
|
cy.logOut()
|
||||||
|
|
||||||
cy.fixture('history.json').as('history')
|
cy.fixture('history.json').as('history')
|
||||||
cy.fixture('history-2.json').as('history-2')
|
cy.fixture('history-2.json').as('history-2')
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
describe('Intro page', () => {
|
describe('Intro page', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept('/public/intro.md', 'test content')
|
cy.intercept('/public/intro.md', 'test content')
|
||||||
|
cy.logOut()
|
||||||
cy.visitHome()
|
cy.visitHome()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -25,15 +26,4 @@ describe('Intro page', () => {
|
||||||
cy.getByCypressId('documentIframe').should('not.exist')
|
cy.getByCypressId('documentIframe').should('not.exist')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('sign in button', () => {
|
|
||||||
it('is hidden when logged in', () => {
|
|
||||||
cy.getByCypressId('sign-in-button').should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('is visible when logged out', () => {
|
|
||||||
cy.logout()
|
|
||||||
cy.getByCypressId('sign-in-button').should('exist')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,18 +7,33 @@ import type { AuthProvider } from '../../src/api/config/types'
|
||||||
import { AuthProviderType } from '../../src/api/config/types'
|
import { AuthProviderType } from '../../src/api/config/types'
|
||||||
|
|
||||||
const initLoggedOutTestWithCustomAuthProviders = (cy: Cypress.cy, enabledProviders: AuthProvider[]) => {
|
const initLoggedOutTestWithCustomAuthProviders = (cy: Cypress.cy, enabledProviders: AuthProvider[]) => {
|
||||||
|
cy.logOut()
|
||||||
cy.loadConfig({
|
cy.loadConfig({
|
||||||
authProviders: enabledProviders
|
authProviders: enabledProviders
|
||||||
})
|
})
|
||||||
cy.visitHome()
|
cy.visitHistory()
|
||||||
cy.logout()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('When logged-in, ', () => {
|
describe('When logged-in, ', () => {
|
||||||
it('sign-in button is hidden', () => {
|
it('sign-in button is hidden', () => {
|
||||||
cy.visitHome()
|
cy.visitHistory()
|
||||||
|
cy.getByCypressId('base-app-bar').should('be.visible')
|
||||||
cy.getByCypressId('sign-in-button').should('not.exist')
|
cy.getByCypressId('sign-in-button').should('not.exist')
|
||||||
})
|
})
|
||||||
|
describe('login page route will redirect', () => {
|
||||||
|
it('to /history if no redirect url has been provided', () => {
|
||||||
|
cy.visit('/login')
|
||||||
|
cy.url().should('contain', '/history')
|
||||||
|
})
|
||||||
|
it('to any page if a redirect url has been provided', () => {
|
||||||
|
cy.visit('/login?redirectBackTo=/profile')
|
||||||
|
cy.url().should('contain', '/profile')
|
||||||
|
})
|
||||||
|
it('to /history if a external redirect url has been provided', () => {
|
||||||
|
cy.visit('/login?redirectBackTo=https://example.org')
|
||||||
|
cy.url().should('contain', '/history')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When logged-out ', () => {
|
describe('When logged-out ', () => {
|
||||||
|
|
|
@ -8,7 +8,9 @@ import { HttpMethod } from '../../src/handler-utils/respond-to-matching-request'
|
||||||
|
|
||||||
declare namespace Cypress {
|
declare namespace Cypress {
|
||||||
interface Chainable {
|
interface Chainable {
|
||||||
loadConfig(): Chainable<Window>
|
loadConfig(additionalConfig?: Partial<typeof config>): Chainable<Window>,
|
||||||
|
logIn: Chainable<Window>,
|
||||||
|
logOut: Chainable<Window>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,8 +86,17 @@ Cypress.Commands.add('loadConfig', (additionalConfig?: Partial<typeof config>) =
|
||||||
return cy.request(HttpMethod.POST, '/api/private/config', { ...config, ...additionalConfig })
|
return cy.request(HttpMethod.POST, '/api/private/config', { ...config, ...additionalConfig })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('logIn', () => {
|
||||||
|
return cy.setCookie('mock-session', '1', { path: '/' })
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('logOut', () => {
|
||||||
|
return cy.clearCookie('mock-session')
|
||||||
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.loadConfig()
|
cy.loadConfig()
|
||||||
|
cy.logIn()
|
||||||
|
|
||||||
cy.intercept('GET', '/public/motd.md', {
|
cy.intercept('GET', '/public/motd.md', {
|
||||||
body: '404 Not Found!',
|
body: '404 Not Found!',
|
||||||
|
|
|
@ -22,7 +22,6 @@ import './config'
|
||||||
import './fill'
|
import './fill'
|
||||||
import './get-by-id'
|
import './get-by-id'
|
||||||
import './get-iframe-content'
|
import './get-iframe-content'
|
||||||
import './logout'
|
|
||||||
import './visit'
|
import './visit'
|
||||||
import './visit-test-editor'
|
import './visit-test-editor'
|
||||||
import 'cypress-commands'
|
import 'cypress-commands'
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare namespace Cypress {
|
|
||||||
interface Chainable {
|
|
||||||
/**
|
|
||||||
* Custom command to log the user out.
|
|
||||||
* @example cy.logout()
|
|
||||||
*/
|
|
||||||
logout(): Chainable<Window>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Cypress.Commands.add('logout', () => {
|
|
||||||
cy.getByCypressId('user-dropdown').click()
|
|
||||||
cy.getByCypressId('user-dropdown-sign-out-button').click()
|
|
||||||
})
|
|
|
@ -554,6 +554,9 @@
|
||||||
"signIn": "Sign In",
|
"signIn": "Sign In",
|
||||||
"signOut": "Sign Out",
|
"signOut": "Sign Out",
|
||||||
"logoutFailed": "There was an error logging you out.\nClose your browser window to ensure session data is removed.",
|
"logoutFailed": "There was an error logging you out.\nClose your browser window to ensure session data is removed.",
|
||||||
|
"guest": {
|
||||||
|
"title": "Continue as guest"
|
||||||
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
|
|
|
@ -89,7 +89,7 @@ const rawNextConfig = {
|
||||||
return Promise.resolve([
|
return Promise.resolve([
|
||||||
{
|
{
|
||||||
source: '/',
|
source: '/',
|
||||||
destination: '/intro',
|
destination: '/login',
|
||||||
permanent: true
|
permanent: true
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import React from 'react'
|
|
||||||
import { BaseAppBar } from '../../../../components/layout/app-bar/base-app-bar'
|
|
||||||
|
|
||||||
export default function AppBar() {
|
|
||||||
return <BaseAppBar>Landing</BaseAppBar>
|
|
||||||
}
|
|
|
@ -1,11 +1,29 @@
|
||||||
|
'use client'
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { BaseAppBar } from '../../../../components/layout/app-bar/base-app-bar'
|
import { Nav, Navbar } from 'react-bootstrap'
|
||||||
|
import styles from '../../../../components/layout/app-bar/navbar.module.scss'
|
||||||
|
import { HelpDropdown } from '../../../../components/layout/app-bar/app-bar-elements/help-dropdown/help-dropdown'
|
||||||
|
import { SettingsButton } from '../../../../components/global-dialogs/settings-dialog/settings-button'
|
||||||
|
import { BrandingElement } from '../../../../components/layout/app-bar/app-bar-elements/branding-element'
|
||||||
|
|
||||||
export default function AppBar() {
|
export default function AppBar() {
|
||||||
return <BaseAppBar>Login</BaseAppBar>
|
return (
|
||||||
|
<Navbar expand={true} className={`px-2 py-1 align-items-center ${styles.navbar}`}>
|
||||||
|
<Nav className={`align-items-center justify-content-start gap-2 flex-grow-1 ${styles.side}`}>
|
||||||
|
<BrandingElement />
|
||||||
|
</Nav>
|
||||||
|
<Nav className={`align-items-stretch justify-content-end flex-grow-1 ${styles.side} h-100 py-1`}>
|
||||||
|
<div className={'d-flex gap-2'}>
|
||||||
|
<HelpDropdown />
|
||||||
|
<SettingsButton />
|
||||||
|
</div>
|
||||||
|
</Nav>
|
||||||
|
</Navbar>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { CustomBranding } from '../../../components/common/custom-branding/custom-branding'
|
|
||||||
import { HedgeDocLogoVertical } from '../../../components/common/hedge-doc-logo/hedge-doc-logo-vertical'
|
|
||||||
import { LogoSize } from '../../../components/common/hedge-doc-logo/logo-size'
|
|
||||||
import { EditorToRendererCommunicatorContextProvider } from '../../../components/editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
|
||||||
import { IntroCustomContent } from '../../../components/intro-page/intro-custom-content'
|
|
||||||
import { LandingLayout } from '../../../components/landing-layout/landing-layout'
|
|
||||||
import type { NextPage } from 'next'
|
|
||||||
import React from 'react'
|
|
||||||
import { Trans } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the intro page with the logo and the customizable intro text.
|
|
||||||
*/
|
|
||||||
const IntroPage: NextPage = () => {
|
|
||||||
return (
|
|
||||||
<LandingLayout>
|
|
||||||
<EditorToRendererCommunicatorContextProvider>
|
|
||||||
<div className={'flex-fill mt-3'}>
|
|
||||||
<h1 dir='auto' className={'align-items-center d-flex justify-content-center flex-column'}>
|
|
||||||
<HedgeDocLogoVertical size={LogoSize.BIG} autoTextColor={true} />
|
|
||||||
</h1>
|
|
||||||
<p className='lead'>
|
|
||||||
<Trans i18nKey='app.slogan' />
|
|
||||||
</p>
|
|
||||||
<div className={'mb-5'}>
|
|
||||||
<CustomBranding />
|
|
||||||
</div>
|
|
||||||
<IntroCustomContent />
|
|
||||||
</div>
|
|
||||||
</EditorToRendererCommunicatorContextProvider>
|
|
||||||
</LandingLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IntroPage
|
|
|
@ -5,89 +5,55 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { AuthProviderWithCustomName } from '../../../api/config/types'
|
|
||||||
import { AuthProviderType } from '../../../api/config/types'
|
|
||||||
import { useFrontendConfig } from '../../../components/common/frontend-config-context/use-frontend-config'
|
|
||||||
import { RedirectBack } from '../../../components/common/redirect-back'
|
|
||||||
import { ShowIf } from '../../../components/common/show-if/show-if'
|
|
||||||
import { LandingLayout } from '../../../components/landing-layout/landing-layout'
|
|
||||||
import { filterOneClickProviders } from '../../../components/login-page/auth/utils'
|
|
||||||
import { ViaLdap } from '../../../components/login-page/auth/via-ldap'
|
|
||||||
import { ViaLocal } from '../../../components/login-page/auth/via-local'
|
|
||||||
import { ViaOneClick } from '../../../components/login-page/auth/via-one-click'
|
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
|
||||||
import type { NextPage } from 'next'
|
|
||||||
import React, { useMemo } from 'react'
|
|
||||||
import { Card, Col, Row } from 'react-bootstrap'
|
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
import type { NextPage } from 'next'
|
||||||
* Renders the login page with buttons and fields for the enabled auth providers.
|
import { EditorToRendererCommunicatorContextProvider } from '../../../components/editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
||||||
* Redirects the user to the history page if they are already logged in.
|
import { HedgeDocLogoVertical } from '../../../components/common/hedge-doc-logo/hedge-doc-logo-vertical'
|
||||||
*/
|
import { LogoSize } from '../../../components/common/hedge-doc-logo/logo-size'
|
||||||
|
import { Trans } from 'react-i18next'
|
||||||
|
import { CustomBranding } from '../../../components/common/custom-branding/custom-branding'
|
||||||
|
import { IntroCustomContent } from '../../../components/intro-page/intro-custom-content'
|
||||||
|
import React from 'react'
|
||||||
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
|
import { RedirectToParamOrHistory } from '../../../components/login-page/redirect-to-param-or-history'
|
||||||
|
import { Col, Container, Row } from 'react-bootstrap'
|
||||||
|
import { LocalLoginCard } from '../../../components/login-page/local-login/local-login-card'
|
||||||
|
import { LdapLoginCards } from '../../../components/login-page/ldap/ldap-login-cards'
|
||||||
|
import { OneClickLoginCard } from '../../../components/login-page/one-click/one-click-login-card'
|
||||||
|
import { GuestCard } from '../../../components/login-page/guest/guest-card'
|
||||||
|
|
||||||
const LoginPage: NextPage = () => {
|
const LoginPage: NextPage = () => {
|
||||||
useTranslation()
|
|
||||||
const authProviders = useFrontendConfig().authProviders
|
|
||||||
const userLoggedIn = useApplicationState((state) => !!state.user)
|
const userLoggedIn = useApplicationState((state) => !!state.user)
|
||||||
|
|
||||||
const ldapProviders = useMemo(() => {
|
|
||||||
return authProviders
|
|
||||||
.filter((provider) => provider.type === AuthProviderType.LDAP)
|
|
||||||
.map((provider) => {
|
|
||||||
const ldapProvider = provider as AuthProviderWithCustomName
|
|
||||||
return (
|
|
||||||
<ViaLdap
|
|
||||||
providerName={ldapProvider.providerName}
|
|
||||||
identifier={ldapProvider.identifier}
|
|
||||||
key={ldapProvider.identifier}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}, [authProviders])
|
|
||||||
|
|
||||||
const localLoginEnabled = useMemo(() => {
|
|
||||||
return authProviders.some((provider) => provider.type === AuthProviderType.LOCAL)
|
|
||||||
}, [authProviders])
|
|
||||||
|
|
||||||
const oneClickProviders = useMemo(() => {
|
|
||||||
return authProviders.filter(filterOneClickProviders).map((provider, index) => (
|
|
||||||
<div className={'p-2 d-flex flex-column social-button-container'} key={index}>
|
|
||||||
<ViaOneClick provider={provider} />
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
}, [authProviders])
|
|
||||||
|
|
||||||
if (userLoggedIn) {
|
if (userLoggedIn) {
|
||||||
return <RedirectBack />
|
return <RedirectToParamOrHistory />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LandingLayout>
|
<Container>
|
||||||
<div className='my-3'>
|
<Row>
|
||||||
<Row className='h-100 d-flex justify-content-center'>
|
<Col xs={8}>
|
||||||
<ShowIf condition={ldapProviders.length > 0 || localLoginEnabled}>
|
<EditorToRendererCommunicatorContextProvider>
|
||||||
<Col xs={12} sm={10} lg={4}>
|
<div className={'d-flex flex-column align-items-center mt-3'}>
|
||||||
<ShowIf condition={localLoginEnabled}>
|
<HedgeDocLogoVertical size={LogoSize.BIG} autoTextColor={true} />
|
||||||
<ViaLocal />
|
<h5>
|
||||||
</ShowIf>
|
<Trans i18nKey='app.slogan' />
|
||||||
{ldapProviders}
|
</h5>
|
||||||
</Col>
|
<div className={'mb-5'}>
|
||||||
</ShowIf>
|
<CustomBranding />
|
||||||
<ShowIf condition={oneClickProviders.length > 0}>
|
</div>
|
||||||
<Col xs={12} sm={10} lg={4}>
|
<IntroCustomContent />
|
||||||
<Card className='mb-4'>
|
</div>
|
||||||
<Card.Body>
|
</EditorToRendererCommunicatorContextProvider>
|
||||||
<Card.Title>
|
</Col>
|
||||||
<Trans i18nKey='login.signInVia' values={{ service: '' }} />
|
<Col xs={4} className={'pt-3 d-flex gap-3 flex-column'}>
|
||||||
</Card.Title>
|
<GuestCard />
|
||||||
{oneClickProviders}
|
<LocalLoginCard />
|
||||||
</Card.Body>
|
<LdapLoginCards />
|
||||||
</Card>
|
<OneClickLoginCard />
|
||||||
</Col>
|
</Col>
|
||||||
</ShowIf>
|
</Row>
|
||||||
</Row>
|
</Container>
|
||||||
</div>
|
|
||||||
</LandingLayout>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,120 +0,0 @@
|
||||||
'use client'
|
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { doLocalRegister } from '../../../api/auth/local'
|
|
||||||
import type { ApiError } from '../../../api/common/api-error'
|
|
||||||
import { DisplayNameField } from '../../../components/common/fields/display-name-field'
|
|
||||||
import { NewPasswordField } from '../../../components/common/fields/new-password-field'
|
|
||||||
import { PasswordAgainField } from '../../../components/common/fields/password-again-field'
|
|
||||||
import { UsernameLabelField } from '../../../components/common/fields/username-label-field'
|
|
||||||
import { useFrontendConfig } from '../../../components/common/frontend-config-context/use-frontend-config'
|
|
||||||
import { Redirect } from '../../../components/common/redirect'
|
|
||||||
import { LandingLayout } from '../../../components/landing-layout/landing-layout'
|
|
||||||
import { fetchAndSetUser } from '../../../components/login-page/auth/utils'
|
|
||||||
import { useUiNotifications } from '../../../components/notifications/ui-notification-boundary'
|
|
||||||
import { RegisterError } from '../../../components/register-page/register-error'
|
|
||||||
import { RegisterInfos } from '../../../components/register-page/register-infos'
|
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
|
||||||
import { useLowercaseOnInputChange } from '../../../hooks/common/use-lowercase-on-input-change'
|
|
||||||
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
|
||||||
import type { NextPage } from 'next'
|
|
||||||
import { useRouter } from 'next/navigation'
|
|
||||||
import type { FormEvent } from 'react'
|
|
||||||
import React, { useCallback, useMemo, useState } from 'react'
|
|
||||||
import { Button, Card, Col, Form, Row } from 'react-bootstrap'
|
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the registration page with fields for username, display name, password, password retype and information about terms and conditions.
|
|
||||||
*/
|
|
||||||
const RegisterPage: NextPage = () => {
|
|
||||||
useTranslation()
|
|
||||||
const router = useRouter()
|
|
||||||
const allowRegister = useFrontendConfig().allowRegister
|
|
||||||
const userExists = useApplicationState((state) => !!state.user)
|
|
||||||
|
|
||||||
const [username, setUsername] = useState('')
|
|
||||||
const [displayName, setDisplayName] = useState('')
|
|
||||||
const [password, setPassword] = useState('')
|
|
||||||
const [passwordAgain, setPasswordAgain] = useState('')
|
|
||||||
const [error, setError] = useState<ApiError>()
|
|
||||||
|
|
||||||
const { dispatchUiNotification } = useUiNotifications()
|
|
||||||
|
|
||||||
const doRegisterSubmit = useCallback(
|
|
||||||
(event: FormEvent) => {
|
|
||||||
doLocalRegister(username, displayName, password)
|
|
||||||
.then(() => fetchAndSetUser())
|
|
||||||
.then(() => dispatchUiNotification('login.register.success.title', 'login.register.success.message', {}))
|
|
||||||
.then(() => router.push('/'))
|
|
||||||
.catch((error: ApiError) => setError(error))
|
|
||||||
event.preventDefault()
|
|
||||||
},
|
|
||||||
[username, displayName, password, dispatchUiNotification, router]
|
|
||||||
)
|
|
||||||
|
|
||||||
const ready = useMemo(() => {
|
|
||||||
return username.trim() !== '' && displayName.trim() !== '' && password.trim() !== '' && password === passwordAgain
|
|
||||||
}, [username, password, displayName, passwordAgain])
|
|
||||||
|
|
||||||
const isWeakPassword = useMemo(() => {
|
|
||||||
return error?.backendErrorName === 'PasswordTooWeakError'
|
|
||||||
}, [error])
|
|
||||||
|
|
||||||
const isValidUsername = useMemo(() => Boolean(username.trim()), [username])
|
|
||||||
|
|
||||||
const onUsernameChange = useLowercaseOnInputChange(setUsername)
|
|
||||||
const onDisplayNameChange = useOnInputChange(setDisplayName)
|
|
||||||
const onPasswordChange = useOnInputChange(setPassword)
|
|
||||||
const onPasswordAgainChange = useOnInputChange(setPasswordAgain)
|
|
||||||
|
|
||||||
if (userExists) {
|
|
||||||
return <Redirect to={'/intro'} />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!allowRegister) {
|
|
||||||
return <Redirect to={'/login'} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LandingLayout>
|
|
||||||
<div className='my-3'>
|
|
||||||
<h1 className='mb-4'>
|
|
||||||
<Trans i18nKey='login.register.title' />
|
|
||||||
</h1>
|
|
||||||
<Row className='h-100 d-flex justify-content-center'>
|
|
||||||
<Col lg={6}>
|
|
||||||
<Card className='mb-4 text-start'>
|
|
||||||
<Card.Body>
|
|
||||||
<Form onSubmit={doRegisterSubmit} className={'d-flex flex-column gap-3'}>
|
|
||||||
<UsernameLabelField onChange={onUsernameChange} value={username} isValid={isValidUsername} />
|
|
||||||
<DisplayNameField onChange={onDisplayNameChange} value={displayName} />
|
|
||||||
<NewPasswordField onChange={onPasswordChange} value={password} hasError={isWeakPassword} />
|
|
||||||
<PasswordAgainField
|
|
||||||
password={password}
|
|
||||||
onChange={onPasswordAgainChange}
|
|
||||||
value={passwordAgain}
|
|
||||||
hasError={isWeakPassword}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RegisterInfos />
|
|
||||||
|
|
||||||
<Button variant='primary' type='submit' disabled={!ready}>
|
|
||||||
<Trans i18nKey='login.register.title' />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<RegisterError error={error} />
|
|
||||||
</Form>
|
|
||||||
</Card.Body>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
</LandingLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RegisterPage
|
|
|
@ -6,10 +6,10 @@
|
||||||
import { refreshHistoryState } from '../../../redux/history/methods'
|
import { refreshHistoryState } from '../../../redux/history/methods'
|
||||||
import { Logger } from '../../../utils/logger'
|
import { Logger } from '../../../utils/logger'
|
||||||
import { isDevMode, isTestMode } from '../../../utils/test-modes'
|
import { isDevMode, isTestMode } from '../../../utils/test-modes'
|
||||||
import { fetchAndSetUser } from '../../login-page/auth/utils'
|
|
||||||
import { loadDarkMode } from './load-dark-mode'
|
import { loadDarkMode } from './load-dark-mode'
|
||||||
import { setUpI18n } from './setupI18n'
|
import { setUpI18n } from './setupI18n'
|
||||||
import { loadFromLocalStorage } from '../../../redux/editor/methods'
|
import { loadFromLocalStorage } from '../../../redux/editor/methods'
|
||||||
|
import { fetchAndSetUser } from '../../login-page/utils/fetch-and-set-user'
|
||||||
|
|
||||||
const logger = new Logger('Application Loader')
|
const logger = new Logger('Application Loader')
|
||||||
|
|
||||||
|
|
|
@ -7,13 +7,13 @@ import { useTranslatedText } from '../../../hooks/common/use-translated-text'
|
||||||
import { cypressId } from '../../../utils/cypress-attribute'
|
import { cypressId } from '../../../utils/cypress-attribute'
|
||||||
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
|
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
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'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import type { ButtonProps } from 'react-bootstrap/Button'
|
import type { ButtonProps } from 'react-bootstrap/Button'
|
||||||
import { Trans } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
|
import { filterOneClickProviders } from '../../login-page/utils/filter-one-click-providers'
|
||||||
|
import { getOneClickProviderMetadata } from '../../login-page/one-click/get-one-click-provider-metadata'
|
||||||
|
|
||||||
export type SignInButtonProps = Omit<ButtonProps, 'href'>
|
export type SignInButtonProps = Omit<ButtonProps, 'href'>
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props })
|
||||||
const authProviders = useFrontendConfig().authProviders
|
const authProviders = useFrontendConfig().authProviders
|
||||||
|
|
||||||
const loginLink = useMemo(() => {
|
const loginLink = useMemo(() => {
|
||||||
const oneClickProviders = authProviders.filter(filterOneClickProviders)
|
const oneClickProviders = filterOneClickProviders(authProviders)
|
||||||
if (authProviders.length === 1 && oneClickProviders.length === 1) {
|
if (authProviders.length === 1 && oneClickProviders.length === 1) {
|
||||||
const metadata = getOneClickProviderMetadata(oneClickProviders[0])
|
const metadata = getOneClickProviderMetadata(oneClickProviders[0])
|
||||||
return metadata.url
|
return metadata.url
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const HelpDropdown: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown>
|
<Dropdown drop={'start'}>
|
||||||
<Dropdown.Toggle size={'sm'} className={'h-100'}>
|
<Dropdown.Toggle size={'sm'} className={'h-100'}>
|
||||||
<UiIcon icon={IconQuestion} />
|
<UiIcon icon={IconQuestion} />
|
||||||
</Dropdown.Toggle>
|
</Dropdown.Toggle>
|
||||||
|
|
|
@ -15,13 +15,17 @@ import type { PropsWithChildren } from 'react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Nav, Navbar } from 'react-bootstrap'
|
import { Nav, Navbar } from 'react-bootstrap'
|
||||||
import { HistoryButton } from './app-bar-elements/help-dropdown/history-button'
|
import { HistoryButton } from './app-bar-elements/help-dropdown/history-button'
|
||||||
|
import { cypressId } from '../../../utils/cypress-attribute'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the base app bar with branding, help, settings user elements.
|
* Renders the base app bar with branding, help, settings user elements.
|
||||||
*/
|
*/
|
||||||
export const BaseAppBar: React.FC<PropsWithChildren> = ({ children }) => {
|
export const BaseAppBar: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<Navbar expand={true} className={`px-2 py-1 align-items-center border-bottom ${styles.navbar}`}>
|
<Navbar
|
||||||
|
expand={true}
|
||||||
|
className={`px-2 py-1 align-items-center border-bottom ${styles.navbar}`}
|
||||||
|
{...cypressId('base-app-bar')}>
|
||||||
<Nav className={`align-items-center justify-content-start gap-2 flex-grow-1 ${styles.side}`}>
|
<Nav className={`align-items-center justify-content-start gap-2 flex-grow-1 ${styles.side}`}>
|
||||||
<BrandingElement />
|
<BrandingElement />
|
||||||
</Nav>
|
</Nav>
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import type { ChangeEvent } from 'react'
|
|
||||||
|
|
||||||
export interface AuthFieldProps {
|
|
||||||
onChange: (event: ChangeEvent<HTMLInputElement>) => void
|
|
||||||
invalid: boolean
|
|
||||||
}
|
|
39
frontend/src/components/login-page/guest/guest-card.tsx
Normal file
39
frontend/src/components/login-page/guest/guest-card.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { Card } from 'react-bootstrap'
|
||||||
|
import { NewNoteButton } from '../../common/new-note-button/new-note-button'
|
||||||
|
import { HistoryButton } from '../../layout/app-bar/app-bar-elements/help-dropdown/history-button'
|
||||||
|
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the card with the options for not logged-in users.
|
||||||
|
*/
|
||||||
|
export const GuestCard: React.FC = () => {
|
||||||
|
const allowAnonymous = useFrontendConfig().allowAnonymous
|
||||||
|
|
||||||
|
useTranslation()
|
||||||
|
|
||||||
|
if (!allowAnonymous) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Card.Body>
|
||||||
|
<Card.Title>
|
||||||
|
<Trans i18nKey={'login.guest.title'}></Trans>
|
||||||
|
</Card.Title>
|
||||||
|
<div className={'d-flex flex-row gap-2'}>
|
||||||
|
<NewNoteButton />
|
||||||
|
<HistoryButton />
|
||||||
|
</div>
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -7,12 +7,12 @@ import { doLdapLogin } from '../../../api/auth/ldap'
|
||||||
import { useLowercaseOnInputChange } from '../../../hooks/common/use-lowercase-on-input-change'
|
import { useLowercaseOnInputChange } from '../../../hooks/common/use-lowercase-on-input-change'
|
||||||
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
||||||
import { UsernameField } from '../../common/fields/username-field'
|
import { UsernameField } from '../../common/fields/username-field'
|
||||||
import { PasswordField } from './fields/password-field'
|
|
||||||
import { fetchAndSetUser } from './utils'
|
|
||||||
import type { FormEvent } from 'react'
|
import type { FormEvent } from 'react'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { Alert, Button, Card, Form } from 'react-bootstrap'
|
import { Alert, Button, Card, Form } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { fetchAndSetUser } from '../utils/fetch-and-set-user'
|
||||||
|
import { PasswordField } from '../password-field'
|
||||||
|
|
||||||
export interface ViaLdapProps {
|
export interface ViaLdapProps {
|
||||||
providerName: string
|
providerName: string
|
||||||
|
@ -22,7 +22,7 @@ export interface ViaLdapProps {
|
||||||
/**
|
/**
|
||||||
* Renders the LDAP login box with username and password field.
|
* Renders the LDAP login box with username and password field.
|
||||||
*/
|
*/
|
||||||
export const ViaLdap: React.FC<ViaLdapProps> = ({ providerName, identifier }) => {
|
export const LdapLoginCard: React.FC<ViaLdapProps> = ({ providerName, identifier }) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
|
@ -43,12 +43,12 @@ export const ViaLdap: React.FC<ViaLdapProps> = ({ providerName, identifier }) =>
|
||||||
const onPasswordChange = useOnInputChange(setPassword)
|
const onPasswordChange = useOnInputChange(setPassword)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='bg-dark mb-4'>
|
<Card>
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
<Card.Title>
|
<Card.Title>
|
||||||
<Trans i18nKey='login.signInVia' values={{ service: providerName }} />
|
<Trans i18nKey='login.signInVia' values={{ service: providerName }} />
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
<Form onSubmit={onLoginSubmit}>
|
<Form onSubmit={onLoginSubmit} className={'d-flex gap-3 flex-column'}>
|
||||||
<UsernameField onChange={onUsernameChange} isValid={!!error} value={username} />
|
<UsernameField onChange={onUsernameChange} isValid={!!error} value={username} />
|
||||||
<PasswordField onChange={onPasswordChange} invalid={!!error} />
|
<PasswordField onChange={onPasswordChange} invalid={!!error} />
|
||||||
<Alert className='small' show={!!error} variant='danger'>
|
<Alert className='small' show={!!error} variant='danger'>
|
35
frontend/src/components/login-page/ldap/ldap-login-cards.tsx
Normal file
35
frontend/src/components/login-page/ldap/ldap-login-cards.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Fragment, useMemo } from 'react'
|
||||||
|
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
|
||||||
|
import type { AuthProviderWithCustomName } from '../../../api/config/types'
|
||||||
|
import { AuthProviderType } from '../../../api/config/types'
|
||||||
|
import { LdapLoginCard } from './ldap-login-card'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a ldap login card for every ldap auth provider.
|
||||||
|
*/
|
||||||
|
export const LdapLoginCards: React.FC = () => {
|
||||||
|
const authProviders = useFrontendConfig().authProviders
|
||||||
|
|
||||||
|
const ldapProviders = useMemo(() => {
|
||||||
|
return authProviders
|
||||||
|
.filter((provider) => provider.type === AuthProviderType.LDAP)
|
||||||
|
.map((provider) => {
|
||||||
|
const ldapProvider = provider as AuthProviderWithCustomName
|
||||||
|
return (
|
||||||
|
<LdapLoginCard
|
||||||
|
providerName={ldapProvider.providerName}
|
||||||
|
identifier={ldapProvider.identifier}
|
||||||
|
key={ldapProvider.identifier}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}, [authProviders])
|
||||||
|
|
||||||
|
return ldapProviders.length === 0 ? null : <Fragment>{ldapProviders}</Fragment>
|
||||||
|
}
|
|
@ -8,28 +8,25 @@ import { ErrorToI18nKeyMapper } from '../../../api/common/error-to-i18n-key-mapp
|
||||||
import { useLowercaseOnInputChange } from '../../../hooks/common/use-lowercase-on-input-change'
|
import { useLowercaseOnInputChange } from '../../../hooks/common/use-lowercase-on-input-change'
|
||||||
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
||||||
import { UsernameField } from '../../common/fields/username-field'
|
import { UsernameField } from '../../common/fields/username-field'
|
||||||
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 { fetchAndSetUser } from './utils'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import type { FormEvent } from 'react'
|
import type { FormEvent } from 'react'
|
||||||
import React, { useCallback, useMemo, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import { Alert, Button, Card, Form } from 'react-bootstrap'
|
import { Alert, Button, Card, Form } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { fetchAndSetUser } from '../utils/fetch-and-set-user'
|
||||||
|
import { PasswordField } from '../password-field'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the local login box with username and password field and the optional button for registering a new user.
|
* Renders the local login box with username and password field and the optional button for registering a new user.
|
||||||
*/
|
*/
|
||||||
export const ViaLocal: React.FC = () => {
|
export const LocalLoginCardBody: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
const allowRegister = useFrontendConfig().allowRegister
|
|
||||||
|
|
||||||
const onLoginSubmit = useCallback(
|
const onLoginSubmit = useCallback(
|
||||||
(event: FormEvent) => {
|
(event: FormEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
doLocalLogin(username, password)
|
doLocalLogin(username, password)
|
||||||
.then(() => fetchAndSetUser())
|
.then(() => fetchAndSetUser())
|
||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
|
@ -40,7 +37,6 @@ export const ViaLocal: React.FC = () => {
|
||||||
.orFallbackI18nKey('other')
|
.orFallbackI18nKey('other')
|
||||||
setError(errorI18nKey)
|
setError(errorI18nKey)
|
||||||
})
|
})
|
||||||
event.preventDefault()
|
|
||||||
},
|
},
|
||||||
[username, password]
|
[username, password]
|
||||||
)
|
)
|
||||||
|
@ -51,30 +47,20 @@ export const ViaLocal: React.FC = () => {
|
||||||
const translationOptions = useMemo(() => ({ service: t('login.auth.username') }), [t])
|
const translationOptions = useMemo(() => ({ service: t('login.auth.username') }), [t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='mb-4'>
|
<Card.Body>
|
||||||
<Card.Body>
|
<Card.Title>
|
||||||
<Card.Title>
|
<Trans i18nKey='login.signInVia' values={translationOptions} />
|
||||||
<Trans i18nKey='login.signInVia' values={translationOptions} />
|
</Card.Title>
|
||||||
</Card.Title>
|
<Form onSubmit={onLoginSubmit} className={'d-flex gap-3 flex-column'}>
|
||||||
<Form onSubmit={onLoginSubmit} className={'d-flex gap-3 flex-column'}>
|
<UsernameField onChange={onUsernameChange} isInvalid={!!error} value={username} />
|
||||||
<UsernameField onChange={onUsernameChange} isInvalid={!!error} value={username} />
|
<PasswordField onChange={onPasswordChange} invalid={!!error} />
|
||||||
<PasswordField onChange={onPasswordChange} invalid={!!error} />
|
<Alert className='small' show={!!error} variant='danger'>
|
||||||
<Alert className='small' show={!!error} variant='danger'>
|
<Trans i18nKey={error} />
|
||||||
<Trans i18nKey={error} />
|
</Alert>
|
||||||
</Alert>
|
<Button type='submit' variant='primary'>
|
||||||
<Button type='submit' variant='primary'>
|
<Trans i18nKey='login.signIn' />
|
||||||
<Trans i18nKey='login.signIn' />
|
</Button>
|
||||||
</Button>
|
</Form>
|
||||||
<ShowIf condition={allowRegister}>
|
</Card.Body>
|
||||||
<Trans i18nKey={'login.register.question'} />
|
|
||||||
<Link href={'/register'} passHref={true}>
|
|
||||||
<Button type='button' variant='secondary' className={'d-block w-100'}>
|
|
||||||
<Trans i18nKey='login.register.title' />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</ShowIf>
|
|
||||||
</Form>
|
|
||||||
</Card.Body>
|
|
||||||
</Card>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useMemo } from 'react'
|
||||||
|
import { Card } from 'react-bootstrap'
|
||||||
|
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
|
||||||
|
import { AuthProviderType } from '../../../api/config/types'
|
||||||
|
import { LocalLoginCardBody } from './local-login-card-body'
|
||||||
|
import { LocalRegisterCardBody } from './register/local-register-card-body'
|
||||||
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the card that processes local logins and registers.
|
||||||
|
*/
|
||||||
|
export const LocalLoginCard: React.FC = () => {
|
||||||
|
const frontendConfig = useFrontendConfig()
|
||||||
|
|
||||||
|
const localLoginEnabled = useMemo(() => {
|
||||||
|
return frontendConfig.authProviders.some((provider) => provider.type === AuthProviderType.LOCAL)
|
||||||
|
}, [frontendConfig])
|
||||||
|
|
||||||
|
if (!localLoginEnabled) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<LocalLoginCardBody />
|
||||||
|
<ShowIf condition={frontendConfig.allowRegister}>
|
||||||
|
<hr className={'m-0'} />
|
||||||
|
<LocalRegisterCardBody />
|
||||||
|
</ShowIf>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { Button } from 'react-bootstrap'
|
||||||
|
import { Trans } from 'react-i18next'
|
||||||
|
|
||||||
|
interface LocalRegisterButtonProps {
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a button that triggers the register process.
|
||||||
|
*
|
||||||
|
* @param onClick The callback that is executed when the button is clicked
|
||||||
|
*/
|
||||||
|
export const LocalRegisterButton: React.FC<LocalRegisterButtonProps> = ({ onClick }) => {
|
||||||
|
return (
|
||||||
|
<Button type='button' variant='secondary' onClick={onClick}>
|
||||||
|
<Trans i18nKey='login.register.title' />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
|
import { Card } from 'react-bootstrap'
|
||||||
|
import { Trans } from 'react-i18next'
|
||||||
|
import { LocalRegisterForm } from './local-register-form'
|
||||||
|
import { LocalRegisterButton } from './local-register-button'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a bootstrap card body for the register process that switches from a button to the form.
|
||||||
|
*/
|
||||||
|
export const LocalRegisterCardBody: React.FC = () => {
|
||||||
|
const [showForm, setShowForm] = useState(false)
|
||||||
|
|
||||||
|
const doShowForm = useCallback(() => {
|
||||||
|
setShowForm(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const content = useMemo(() => {
|
||||||
|
if (showForm) {
|
||||||
|
return <LocalRegisterForm />
|
||||||
|
} else {
|
||||||
|
return <LocalRegisterButton onClick={doShowForm} />
|
||||||
|
}
|
||||||
|
}, [doShowForm, showForm])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card.Body>
|
||||||
|
<Card.Title>
|
||||||
|
<Trans i18nKey={'login.register.question'} />
|
||||||
|
</Card.Title>
|
||||||
|
{content}
|
||||||
|
</Card.Body>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client'
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import type { NextPage } from 'next'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import type { FormEvent } from 'react'
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
|
import { Button, Form } from 'react-bootstrap'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
|
||||||
|
import type { ApiError } from '../../../../api/common/api-error'
|
||||||
|
import { doLocalRegister } from '../../../../api/auth/local'
|
||||||
|
import { useLowercaseOnInputChange } from '../../../../hooks/common/use-lowercase-on-input-change'
|
||||||
|
import { useOnInputChange } from '../../../../hooks/common/use-on-input-change'
|
||||||
|
import { UsernameLabelField } from '../../../common/fields/username-label-field'
|
||||||
|
import { DisplayNameField } from '../../../common/fields/display-name-field'
|
||||||
|
import { NewPasswordField } from '../../../common/fields/new-password-field'
|
||||||
|
import { PasswordAgainField } from '../../../common/fields/password-again-field'
|
||||||
|
import { RegisterInfos } from '../../../register-page/register-infos'
|
||||||
|
import { RegisterError } from '../../../register-page/register-error'
|
||||||
|
import { fetchAndSetUser } from '../../utils/fetch-and-set-user'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the registration process with fields for username, display name, password, password retype and information about terms and conditions.
|
||||||
|
*/
|
||||||
|
export const LocalRegisterForm: NextPage = () => {
|
||||||
|
useTranslation()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const [username, setUsername] = useState('')
|
||||||
|
const [displayName, setDisplayName] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [passwordAgain, setPasswordAgain] = useState('')
|
||||||
|
const [error, setError] = useState<ApiError>()
|
||||||
|
|
||||||
|
const { dispatchUiNotification } = useUiNotifications()
|
||||||
|
|
||||||
|
const doRegisterSubmit = useCallback(
|
||||||
|
(event: FormEvent) => {
|
||||||
|
doLocalRegister(username, displayName, password)
|
||||||
|
.then(() => fetchAndSetUser())
|
||||||
|
.then(() => dispatchUiNotification('login.register.success.title', 'login.register.success.message', {}))
|
||||||
|
.then(() => router.push('/'))
|
||||||
|
.catch((error: ApiError) => setError(error))
|
||||||
|
event.preventDefault()
|
||||||
|
},
|
||||||
|
[username, displayName, password, dispatchUiNotification, router]
|
||||||
|
)
|
||||||
|
|
||||||
|
const ready = useMemo(() => {
|
||||||
|
return username.trim() !== '' && displayName.trim() !== '' && password.trim() !== '' && password === passwordAgain
|
||||||
|
}, [username, password, displayName, passwordAgain])
|
||||||
|
|
||||||
|
const isWeakPassword = useMemo(() => {
|
||||||
|
return error?.backendErrorName === 'PasswordTooWeakError'
|
||||||
|
}, [error])
|
||||||
|
|
||||||
|
const isValidUsername = useMemo(() => Boolean(username.trim()), [username])
|
||||||
|
|
||||||
|
const onUsernameChange = useLowercaseOnInputChange(setUsername)
|
||||||
|
const onDisplayNameChange = useOnInputChange(setDisplayName)
|
||||||
|
const onPasswordChange = useOnInputChange(setPassword)
|
||||||
|
const onPasswordAgainChange = useOnInputChange(setPasswordAgain)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={doRegisterSubmit} className={'d-flex flex-column gap-3'}>
|
||||||
|
<UsernameLabelField onChange={onUsernameChange} value={username} isValid={isValidUsername} />
|
||||||
|
<DisplayNameField onChange={onDisplayNameChange} value={displayName} />
|
||||||
|
<NewPasswordField onChange={onPasswordChange} value={password} hasError={isWeakPassword} />
|
||||||
|
<PasswordAgainField
|
||||||
|
password={password}
|
||||||
|
onChange={onPasswordAgainChange}
|
||||||
|
value={passwordAgain}
|
||||||
|
hasError={isWeakPassword}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RegisterInfos />
|
||||||
|
|
||||||
|
<Button variant='primary' type='submit' disabled={!ready}>
|
||||||
|
<Trans i18nKey='login.register.title' />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<RegisterError error={error} />
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,13 +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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { AuthProvider } from '../../../../api/config/types'
|
import styles from './via-one-click.module.scss'
|
||||||
import { AuthProviderType } from '../../../../api/config/types'
|
|
||||||
import { Logger } from '../../../../utils/logger'
|
|
||||||
import { IconGitlab } from '../../../common/icons/additional/icon-gitlab'
|
|
||||||
import styles from '../via-one-click.module.scss'
|
|
||||||
import type { Icon } from 'react-bootstrap-icons'
|
import type { Icon } from 'react-bootstrap-icons'
|
||||||
import {
|
import {
|
||||||
Dropbox as IconDropbox,
|
Dropbox as IconDropbox,
|
||||||
|
@ -19,6 +15,10 @@ import {
|
||||||
PersonRolodex as IconPersonRolodex,
|
PersonRolodex as IconPersonRolodex,
|
||||||
Twitter as IconTwitter
|
Twitter as IconTwitter
|
||||||
} from 'react-bootstrap-icons'
|
} from 'react-bootstrap-icons'
|
||||||
|
import { Logger } from '../../../utils/logger'
|
||||||
|
import type { AuthProvider } from '../../../api/config/types'
|
||||||
|
import { AuthProviderType } from '../../../api/config/types'
|
||||||
|
import { IconGitlab } from '../../common/icons/additional/icon-gitlab'
|
||||||
|
|
||||||
export interface OneClickMetadata {
|
export interface OneClickMetadata {
|
||||||
name: string
|
name: string
|
|
@ -1,12 +1,12 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { AuthProvider, AuthProviderWithCustomName } from '../../../api/config/types'
|
import type { AuthProvider, AuthProviderWithCustomName } from '../../../api/config/types'
|
||||||
import { IconButton } from '../../common/icon-button/icon-button'
|
import { IconButton } from '../../common/icon-button/icon-button'
|
||||||
import { getOneClickProviderMetadata } from './utils/get-one-click-provider-metadata'
|
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
|
import { getOneClickProviderMetadata } from './get-one-click-provider-metadata'
|
||||||
|
|
||||||
export interface ViaOneClickProps {
|
export interface ViaOneClickProps {
|
||||||
provider: AuthProvider
|
provider: AuthProvider
|
||||||
|
@ -17,7 +17,7 @@ export interface ViaOneClickProps {
|
||||||
*
|
*
|
||||||
* @param provider The one-click login provider. In case of ones that can be defined multiple times, an identifier and a label is required.
|
* @param provider The one-click login provider. In case of ones that can be defined multiple times, an identifier and a label is required.
|
||||||
*/
|
*/
|
||||||
export const ViaOneClick: React.FC<ViaOneClickProps> = ({ provider }) => {
|
export const OneClickLoginButton: React.FC<ViaOneClickProps> = ({ provider }) => {
|
||||||
const { className, icon, url, name } = useMemo(() => getOneClickProviderMetadata(provider), [provider])
|
const { className, icon, url, name } = useMemo(() => getOneClickProviderMetadata(provider), [provider])
|
||||||
const text = (provider as AuthProviderWithCustomName).providerName || name
|
const text = (provider as AuthProviderWithCustomName).providerName || name
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useMemo } from 'react'
|
||||||
|
import { Card } from 'react-bootstrap'
|
||||||
|
import { Trans } from 'react-i18next'
|
||||||
|
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
|
||||||
|
import { OneClickLoginButton } from './one-click-login-button'
|
||||||
|
import { filterOneClickProviders } from '../utils/filter-one-click-providers'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a card that contains buttons to all one click auth providers.
|
||||||
|
*/
|
||||||
|
export const OneClickLoginCard: React.FC = () => {
|
||||||
|
const authProviders = useFrontendConfig().authProviders
|
||||||
|
|
||||||
|
const oneClickProviders = useMemo(() => {
|
||||||
|
return filterOneClickProviders(authProviders).map((provider, index) => (
|
||||||
|
<div className={'p-2 d-flex flex-column social-button-container'} key={index}>
|
||||||
|
<OneClickLoginButton provider={provider} />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}, [authProviders])
|
||||||
|
|
||||||
|
return oneClickProviders.length === 0 ? null : (
|
||||||
|
<Card>
|
||||||
|
<Card.Body>
|
||||||
|
<Card.Title>
|
||||||
|
<Trans i18nKey='login.signInVia' values={{ service: '' }} />
|
||||||
|
</Card.Title>
|
||||||
|
{oneClickProviders}
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,12 +1,17 @@
|
||||||
/*
|
/*
|
||||||
* 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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useTranslatedText } from '../../../../hooks/common/use-translated-text'
|
import type { ChangeEvent } from 'react'
|
||||||
import type { AuthFieldProps } from './fields'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Form } from 'react-bootstrap'
|
import { Form } from 'react-bootstrap'
|
||||||
|
import { useTranslatedText } from '../../hooks/common/use-translated-text'
|
||||||
|
|
||||||
|
export interface AuthFieldProps {
|
||||||
|
onChange: (event: ChangeEvent<HTMLInputElement>) => void
|
||||||
|
invalid: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders an input field for the password of a user.
|
* Renders an input field for the password of a user.
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { Redirect } from '../common/redirect'
|
||||||
|
import { useSingleStringUrlParameter } from '../../hooks/common/use-single-string-url-parameter'
|
||||||
|
|
||||||
|
const defaultFallback = '/history'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects the browser to the relative URL that is provided via "redirectBackTo" URL parameter.
|
||||||
|
* If no parameter has been provided or if the URL is not relative then "/history" will be used.
|
||||||
|
*/
|
||||||
|
export const RedirectToParamOrHistory: React.FC = () => {
|
||||||
|
const redirectBackUrl = useSingleStringUrlParameter('redirectBackTo', defaultFallback)
|
||||||
|
|
||||||
|
const cleanedUrl =
|
||||||
|
redirectBackUrl.startsWith('/') && !redirectBackUrl.startsWith('//') ? redirectBackUrl : defaultFallback
|
||||||
|
|
||||||
|
return <Redirect to={cleanedUrl} />
|
||||||
|
}
|
|
@ -3,8 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { AuthProvider } from '../../../api/config/types'
|
|
||||||
import { authProviderTypeOneClick } from '../../../api/config/types'
|
|
||||||
import { getMe } from '../../../api/me'
|
import { getMe } from '../../../api/me'
|
||||||
import { setUser } from '../../../redux/user/methods'
|
import { setUser } from '../../../redux/user/methods'
|
||||||
|
|
||||||
|
@ -21,13 +20,3 @@ export const fetchAndSetUser: () => Promise<void> = async () => {
|
||||||
email: me.email
|
email: me.email
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter to apply to a list of auth providers to get only one-click providers.
|
|
||||||
*
|
|
||||||
* @param provider The provider to test whether it is a one-click provider or not.
|
|
||||||
* @return {@link true} when the provider is a one-click one, {@link false} otherwise.
|
|
||||||
*/
|
|
||||||
export const filterOneClickProviders = (provider: AuthProvider): boolean => {
|
|
||||||
return authProviderTypeOneClick.includes(provider.type)
|
|
||||||
}
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { AuthProvider } from '../../../api/config/types'
|
||||||
|
import { authProviderTypeOneClick } from '../../../api/config/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the given auth providers to one-click providers only.
|
||||||
|
* @param authProviders The auth providers to filter
|
||||||
|
* @return only one click auth providers
|
||||||
|
*/
|
||||||
|
export const filterOneClickProviders = (authProviders: AuthProvider[]) => {
|
||||||
|
return authProviders.filter((provider: AuthProvider): boolean => authProviderTypeOneClick.includes(provider.type))
|
||||||
|
}
|
|
@ -7,12 +7,12 @@ import { updateDisplayName } from '../../../api/me'
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
||||||
import { DisplayNameField } from '../../common/fields/display-name-field'
|
import { DisplayNameField } from '../../common/fields/display-name-field'
|
||||||
import { fetchAndSetUser } from '../../login-page/auth/utils'
|
|
||||||
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
|
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
|
||||||
import type { FormEvent } from 'react'
|
import type { FormEvent } from 'react'
|
||||||
import React, { useCallback, useMemo, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import { Button, Card, Form } from 'react-bootstrap'
|
import { Button, Card, Form } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { fetchAndSetUser } from '../../login-page/utils/fetch-and-set-user'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Profile page section for changing the current display name.
|
* Profile page section for changing the current display name.
|
||||||
|
|
13
frontend/src/pages/api/private/auth/local/login.ts
Normal file
13
frontend/src/pages/api/private/auth/local/login.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
|
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
res.status(200).setHeader('Set-Cookie', ['mock-session=1; Path=/']).json({})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handler
|
13
frontend/src/pages/api/private/auth/logout.ts
Normal file
13
frontend/src/pages/api/private/auth/logout.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
|
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
res.setHeader('Set-Cookie', 'mock-session=0; Path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT').status(200).json({})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handler
|
|
@ -8,6 +8,11 @@ import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
const handler = (req: NextApiRequest, res: NextApiResponse): void => {
|
||||||
|
const cookieSet = req.headers?.['cookie']?.split(';').find((value) => value.trim() === 'mock-session=1') !== undefined
|
||||||
|
if (!cookieSet) {
|
||||||
|
res.status(403).json({})
|
||||||
|
return
|
||||||
|
}
|
||||||
respondToMatchingRequest<LoginUserInfo>(HttpMethod.GET, req, res, {
|
respondToMatchingRequest<LoginUserInfo>(HttpMethod.GET, req, res, {
|
||||||
username: 'mock',
|
username: 'mock',
|
||||||
photo: '/public/img/avatar.png',
|
photo: '/public/img/avatar.png',
|
||||||
|
|
|
@ -7,11 +7,6 @@ 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 type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
respondToMatchingRequest<MediaUpload[]>(HttpMethod.GET, req, res, [
|
respondToMatchingRequest<MediaUpload[]>(HttpMethod.GET, req, res, [
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue