diff --git a/frontend/cypress/e2e/history.spec.ts b/frontend/cypress/e2e/history.spec.ts index 1afda8a82..0c29ad6ec 100644 --- a/frontend/cypress/e2e/history.spec.ts +++ b/frontend/cypress/e2e/history.spec.ts @@ -147,7 +147,7 @@ describe('History', () => { body: [] }) cy.visitHistory() - cy.logout() + cy.logOut() cy.fixture('history.json').as('history') cy.fixture('history-2.json').as('history-2') diff --git a/frontend/cypress/e2e/intro.spec.ts b/frontend/cypress/e2e/intro.spec.ts index 993bbd289..34004bc8a 100644 --- a/frontend/cypress/e2e/intro.spec.ts +++ b/frontend/cypress/e2e/intro.spec.ts @@ -8,6 +8,7 @@ describe('Intro page', () => { beforeEach(() => { cy.intercept('/public/intro.md', 'test content') + cy.logOut() cy.visitHome() }) @@ -25,15 +26,4 @@ describe('Intro page', () => { 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') - }) - }) }) diff --git a/frontend/cypress/e2e/signInButton.spec.ts b/frontend/cypress/e2e/signInButton.spec.ts index 738fa1320..6adf5ab2b 100644 --- a/frontend/cypress/e2e/signInButton.spec.ts +++ b/frontend/cypress/e2e/signInButton.spec.ts @@ -7,18 +7,33 @@ import type { AuthProvider } from '../../src/api/config/types' import { AuthProviderType } from '../../src/api/config/types' const initLoggedOutTestWithCustomAuthProviders = (cy: Cypress.cy, enabledProviders: AuthProvider[]) => { + cy.logOut() cy.loadConfig({ authProviders: enabledProviders }) - cy.visitHome() - cy.logout() + cy.visitHistory() } describe('When logged-in, ', () => { 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') }) + 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 ', () => { diff --git a/frontend/cypress/support/config.ts b/frontend/cypress/support/config.ts index 32435ae81..8f6aaefd0 100644 --- a/frontend/cypress/support/config.ts +++ b/frontend/cypress/support/config.ts @@ -8,7 +8,9 @@ import { HttpMethod } from '../../src/handler-utils/respond-to-matching-request' declare namespace Cypress { interface Chainable { - loadConfig(): Chainable + loadConfig(additionalConfig?: Partial): Chainable, + logIn: Chainable, + logOut: Chainable } } @@ -84,8 +86,17 @@ Cypress.Commands.add('loadConfig', (additionalConfig?: Partial) = 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(() => { cy.loadConfig() + cy.logIn() cy.intercept('GET', '/public/motd.md', { body: '404 Not Found!', diff --git a/frontend/cypress/support/e2e.ts b/frontend/cypress/support/e2e.ts index a6d2acdd4..45bb9d096 100644 --- a/frontend/cypress/support/e2e.ts +++ b/frontend/cypress/support/e2e.ts @@ -22,7 +22,6 @@ import './config' import './fill' import './get-by-id' import './get-iframe-content' -import './logout' import './visit' import './visit-test-editor' import 'cypress-commands' diff --git a/frontend/cypress/support/logout.ts b/frontend/cypress/support/logout.ts deleted file mode 100644 index 9d95f3b6a..000000000 --- a/frontend/cypress/support/logout.ts +++ /dev/null @@ -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 - } -} - -Cypress.Commands.add('logout', () => { - cy.getByCypressId('user-dropdown').click() - cy.getByCypressId('user-dropdown-sign-out-button').click() -}) diff --git a/frontend/locales/en.json b/frontend/locales/en.json index ca8c23bcd..b055e14c6 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -554,6 +554,9 @@ "signIn": "Sign In", "signOut": "Sign Out", "logoutFailed": "There was an error logging you out.\nClose your browser window to ensure session data is removed.", + "guest": { + "title": "Continue as guest" + }, "auth": { "email": "Email", "password": "Password", diff --git a/frontend/next.config.js b/frontend/next.config.js index 129bda01c..2cf5a5de7 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -89,7 +89,7 @@ const rawNextConfig = { return Promise.resolve([ { source: '/', - destination: '/intro', + destination: '/login', permanent: true } ]) diff --git a/frontend/src/app/(editor)/@appBar/intro/page.tsx b/frontend/src/app/(editor)/@appBar/intro/page.tsx deleted file mode 100644 index 64a74d30f..000000000 --- a/frontend/src/app/(editor)/@appBar/intro/page.tsx +++ /dev/null @@ -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 Landing -} diff --git a/frontend/src/app/(editor)/@appBar/login/page.tsx b/frontend/src/app/(editor)/@appBar/login/page.tsx index 1929d6a3a..7cb4e8c87 100644 --- a/frontend/src/app/(editor)/@appBar/login/page.tsx +++ b/frontend/src/app/(editor)/@appBar/login/page.tsx @@ -1,11 +1,29 @@ +'use client' /* * 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' +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() { - return Login + return ( + + + + + ) } diff --git a/frontend/src/app/(editor)/intro/page.tsx b/frontend/src/app/(editor)/intro/page.tsx deleted file mode 100644 index 3207285ef..000000000 --- a/frontend/src/app/(editor)/intro/page.tsx +++ /dev/null @@ -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 ( - - -
-

- -

-

- -

-
- -
- -
-
-
- ) -} - -export default IntroPage diff --git a/frontend/src/app/(editor)/login/page.tsx b/frontend/src/app/(editor)/login/page.tsx index 670f1b4d9..1c1c0654c 100644 --- a/frontend/src/app/(editor)/login/page.tsx +++ b/frontend/src/app/(editor)/login/page.tsx @@ -5,89 +5,55 @@ * * 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' -/** - * Renders the login page with buttons and fields for the enabled auth providers. - * Redirects the user to the history page if they are already logged in. - */ +import type { NextPage } from 'next' +import { EditorToRendererCommunicatorContextProvider } from '../../../components/editor-page/render-context/editor-to-renderer-communicator-context-provider' +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 = () => { - useTranslation() - const authProviders = useFrontendConfig().authProviders 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 ( - - ) - }) - }, [authProviders]) - - const localLoginEnabled = useMemo(() => { - return authProviders.some((provider) => provider.type === AuthProviderType.LOCAL) - }, [authProviders]) - - const oneClickProviders = useMemo(() => { - return authProviders.filter(filterOneClickProviders).map((provider, index) => ( -
- -
- )) - }, [authProviders]) - if (userLoggedIn) { - return + return } return ( - -
- - 0 || localLoginEnabled}> - - - - - {ldapProviders} - - - 0}> - - - - - - - {oneClickProviders} - - - - - -
-
+ + + + +
+ +
+ +
+
+ +
+ +
+
+ + + + + + + +
+
) } diff --git a/frontend/src/app/(editor)/register/page.tsx b/frontend/src/app/(editor)/register/page.tsx deleted file mode 100644 index 41cd3f92e..000000000 --- a/frontend/src/app/(editor)/register/page.tsx +++ /dev/null @@ -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() - - 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 - } - - if (!allowRegister) { - return - } - - return ( - -
-

- -

- - - - -
- - - - - - - - - - - -
-
- -
-
-
- ) -} - -export default RegisterPage diff --git a/frontend/src/components/application-loader/initializers/index.ts b/frontend/src/components/application-loader/initializers/index.ts index 82d197664..eeaeb6c22 100644 --- a/frontend/src/components/application-loader/initializers/index.ts +++ b/frontend/src/components/application-loader/initializers/index.ts @@ -6,10 +6,10 @@ 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 { loadDarkMode } from './load-dark-mode' import { setUpI18n } from './setupI18n' import { loadFromLocalStorage } from '../../../redux/editor/methods' +import { fetchAndSetUser } from '../../login-page/utils/fetch-and-set-user' const logger = new Logger('Application Loader') diff --git a/frontend/src/components/landing-layout/navigation/sign-in-button.tsx b/frontend/src/components/landing-layout/navigation/sign-in-button.tsx index ebea3ad9f..3340adb80 100644 --- a/frontend/src/components/landing-layout/navigation/sign-in-button.tsx +++ b/frontend/src/components/landing-layout/navigation/sign-in-button.tsx @@ -7,13 +7,13 @@ import { useTranslatedText } from '../../../hooks/common/use-translated-text' 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' import Link from 'next/link' import React, { useMemo } from 'react' import { Button } from 'react-bootstrap' import type { ButtonProps } from 'react-bootstrap/Button' 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 @@ -28,7 +28,7 @@ export const SignInButton: React.FC = ({ variant, ...props }) const authProviders = useFrontendConfig().authProviders const loginLink = useMemo(() => { - const oneClickProviders = authProviders.filter(filterOneClickProviders) + const oneClickProviders = filterOneClickProviders(authProviders) if (authProviders.length === 1 && oneClickProviders.length === 1) { const metadata = getOneClickProviderMetadata(oneClickProviders[0]) return metadata.url diff --git a/frontend/src/components/layout/app-bar/app-bar-elements/help-dropdown/help-dropdown.tsx b/frontend/src/components/layout/app-bar/app-bar-elements/help-dropdown/help-dropdown.tsx index 5e4943834..daefbf234 100644 --- a/frontend/src/components/layout/app-bar/app-bar-elements/help-dropdown/help-dropdown.tsx +++ b/frontend/src/components/layout/app-bar/app-bar-elements/help-dropdown/help-dropdown.tsx @@ -21,7 +21,7 @@ export const HelpDropdown: React.FC = () => { useTranslation() return ( - + diff --git a/frontend/src/components/layout/app-bar/base-app-bar.tsx b/frontend/src/components/layout/app-bar/base-app-bar.tsx index 65b238fd5..4919c1f2b 100644 --- a/frontend/src/components/layout/app-bar/base-app-bar.tsx +++ b/frontend/src/components/layout/app-bar/base-app-bar.tsx @@ -15,13 +15,17 @@ import type { PropsWithChildren } from 'react' import React from 'react' import { Nav, Navbar } from 'react-bootstrap' 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. */ export const BaseAppBar: React.FC = ({ children }) => { return ( - + diff --git a/frontend/src/components/login-page/auth/fields/fields.ts b/frontend/src/components/login-page/auth/fields/fields.ts deleted file mode 100644 index 26707be5a..000000000 --- a/frontend/src/components/login-page/auth/fields/fields.ts +++ /dev/null @@ -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) => void - invalid: boolean -} diff --git a/frontend/src/components/login-page/guest/guest-card.tsx b/frontend/src/components/login-page/guest/guest-card.tsx new file mode 100644 index 000000000..c40c3008d --- /dev/null +++ b/frontend/src/components/login-page/guest/guest-card.tsx @@ -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 ( + + + + + +
+ + +
+
+
+ ) +} diff --git a/frontend/src/components/login-page/auth/via-ldap.tsx b/frontend/src/components/login-page/ldap/ldap-login-card.tsx similarity index 84% rename from frontend/src/components/login-page/auth/via-ldap.tsx rename to frontend/src/components/login-page/ldap/ldap-login-card.tsx index d3dcc1a72..39cd54211 100644 --- a/frontend/src/components/login-page/auth/via-ldap.tsx +++ b/frontend/src/components/login-page/ldap/ldap-login-card.tsx @@ -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 */ @@ -7,12 +7,12 @@ import { doLdapLogin } from '../../../api/auth/ldap' import { useLowercaseOnInputChange } from '../../../hooks/common/use-lowercase-on-input-change' import { useOnInputChange } from '../../../hooks/common/use-on-input-change' import { UsernameField } from '../../common/fields/username-field' -import { PasswordField } from './fields/password-field' -import { fetchAndSetUser } from './utils' import type { FormEvent } from 'react' import React, { useCallback, useState } from 'react' import { Alert, Button, Card, Form } from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' +import { fetchAndSetUser } from '../utils/fetch-and-set-user' +import { PasswordField } from '../password-field' export interface ViaLdapProps { providerName: string @@ -22,7 +22,7 @@ export interface ViaLdapProps { /** * Renders the LDAP login box with username and password field. */ -export const ViaLdap: React.FC = ({ providerName, identifier }) => { +export const LdapLoginCard: React.FC = ({ providerName, identifier }) => { useTranslation() const [username, setUsername] = useState('') @@ -43,12 +43,12 @@ export const ViaLdap: React.FC = ({ providerName, identifier }) => const onPasswordChange = useOnInputChange(setPassword) return ( - + -
+ diff --git a/frontend/src/components/login-page/ldap/ldap-login-cards.tsx b/frontend/src/components/login-page/ldap/ldap-login-cards.tsx new file mode 100644 index 000000000..b5a94289b --- /dev/null +++ b/frontend/src/components/login-page/ldap/ldap-login-cards.tsx @@ -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 ( + + ) + }) + }, [authProviders]) + + return ldapProviders.length === 0 ? null : {ldapProviders} +} diff --git a/frontend/src/components/login-page/auth/via-local.tsx b/frontend/src/components/login-page/local-login/local-login-card-body.tsx similarity index 56% rename from frontend/src/components/login-page/auth/via-local.tsx rename to frontend/src/components/login-page/local-login/local-login-card-body.tsx index 33630afa5..8fe0d64a9 100644 --- a/frontend/src/components/login-page/auth/via-local.tsx +++ b/frontend/src/components/login-page/local-login/local-login-card-body.tsx @@ -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 { useOnInputChange } from '../../../hooks/common/use-on-input-change' 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 React, { useCallback, useMemo, useState } from 'react' import { Alert, Button, Card, Form } from 'react-bootstrap' 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. */ -export const ViaLocal: React.FC = () => { +export const LocalLoginCardBody: React.FC = () => { const { t } = useTranslation() const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState() - const allowRegister = useFrontendConfig().allowRegister const onLoginSubmit = useCallback( (event: FormEvent) => { + event.preventDefault() doLocalLogin(username, password) .then(() => fetchAndSetUser()) .catch((error: Error) => { @@ -40,7 +37,6 @@ export const ViaLocal: React.FC = () => { .orFallbackI18nKey('other') setError(errorI18nKey) }) - event.preventDefault() }, [username, password] ) @@ -51,30 +47,20 @@ export const ViaLocal: React.FC = () => { const translationOptions = useMemo(() => ({ service: t('login.auth.username') }), [t]) return ( - - - - - - - - - - - - - - - - - - - - - + + + + +
+ + + + + + + +
) } diff --git a/frontend/src/components/login-page/local-login/local-login-card.tsx b/frontend/src/components/login-page/local-login/local-login-card.tsx new file mode 100644 index 000000000..0f6e03353 --- /dev/null +++ b/frontend/src/components/login-page/local-login/local-login-card.tsx @@ -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 ( + + + +
+ +
+
+ ) +} diff --git a/frontend/src/components/login-page/local-login/register/local-register-button.tsx b/frontend/src/components/login-page/local-login/register/local-register-button.tsx new file mode 100644 index 000000000..95b3685ab --- /dev/null +++ b/frontend/src/components/login-page/local-login/register/local-register-button.tsx @@ -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 = ({ onClick }) => { + return ( + + ) +} diff --git a/frontend/src/components/login-page/local-login/register/local-register-card-body.tsx b/frontend/src/components/login-page/local-login/register/local-register-card-body.tsx new file mode 100644 index 000000000..d8195735f --- /dev/null +++ b/frontend/src/components/login-page/local-login/register/local-register-card-body.tsx @@ -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 + } else { + return + } + }, [doShowForm, showForm]) + + return ( + + + + + {content} + + ) +} diff --git a/frontend/src/components/login-page/local-login/register/local-register-form.tsx b/frontend/src/components/login-page/local-login/register/local-register-form.tsx new file mode 100644 index 000000000..9d7272e91 --- /dev/null +++ b/frontend/src/components/login-page/local-login/register/local-register-form.tsx @@ -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() + + 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 ( +
+ + + + + + + + + + + + ) +} diff --git a/frontend/src/components/login-page/auth/utils/get-one-click-provider-metadata.ts b/frontend/src/components/login-page/one-click/get-one-click-provider-metadata.ts similarity index 88% rename from frontend/src/components/login-page/auth/utils/get-one-click-provider-metadata.ts rename to frontend/src/components/login-page/one-click/get-one-click-provider-metadata.ts index 08335f3c8..0a8b81d9f 100644 --- a/frontend/src/components/login-page/auth/utils/get-one-click-provider-metadata.ts +++ b/frontend/src/components/login-page/one-click/get-one-click-provider-metadata.ts @@ -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 */ -import type { AuthProvider } from '../../../../api/config/types' -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 styles from './via-one-click.module.scss' import type { Icon } from 'react-bootstrap-icons' import { Dropbox as IconDropbox, @@ -19,6 +15,10 @@ import { PersonRolodex as IconPersonRolodex, Twitter as IconTwitter } 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 { name: string diff --git a/frontend/src/components/login-page/auth/via-one-click.tsx b/frontend/src/components/login-page/one-click/one-click-login-button.tsx similarity index 79% rename from frontend/src/components/login-page/auth/via-one-click.tsx rename to frontend/src/components/login-page/one-click/one-click-login-button.tsx index c29d1bee6..75b804572 100644 --- a/frontend/src/components/login-page/auth/via-one-click.tsx +++ b/frontend/src/components/login-page/one-click/one-click-login-button.tsx @@ -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 */ import type { AuthProvider, AuthProviderWithCustomName } from '../../../api/config/types' import { IconButton } from '../../common/icon-button/icon-button' -import { getOneClickProviderMetadata } from './utils/get-one-click-provider-metadata' import React, { useMemo } from 'react' +import { getOneClickProviderMetadata } from './get-one-click-provider-metadata' export interface ViaOneClickProps { 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. */ -export const ViaOneClick: React.FC = ({ provider }) => { +export const OneClickLoginButton: React.FC = ({ provider }) => { const { className, icon, url, name } = useMemo(() => getOneClickProviderMetadata(provider), [provider]) const text = (provider as AuthProviderWithCustomName).providerName || name diff --git a/frontend/src/components/login-page/one-click/one-click-login-card.tsx b/frontend/src/components/login-page/one-click/one-click-login-card.tsx new file mode 100644 index 000000000..4fb4489c5 --- /dev/null +++ b/frontend/src/components/login-page/one-click/one-click-login-card.tsx @@ -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) => ( +
+ +
+ )) + }, [authProviders]) + + return oneClickProviders.length === 0 ? null : ( + + + + + + {oneClickProviders} + + + ) +} diff --git a/frontend/src/components/login-page/auth/via-one-click.module.scss b/frontend/src/components/login-page/one-click/via-one-click.module.scss similarity index 100% rename from frontend/src/components/login-page/auth/via-one-click.module.scss rename to frontend/src/components/login-page/one-click/via-one-click.module.scss diff --git a/frontend/src/components/login-page/auth/fields/password-field.tsx b/frontend/src/components/login-page/password-field.tsx similarity index 71% rename from frontend/src/components/login-page/auth/fields/password-field.tsx rename to frontend/src/components/login-page/password-field.tsx index a2063b32d..5de5d86e8 100644 --- a/frontend/src/components/login-page/auth/fields/password-field.tsx +++ b/frontend/src/components/login-page/password-field.tsx @@ -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 */ -import { useTranslatedText } from '../../../../hooks/common/use-translated-text' -import type { AuthFieldProps } from './fields' +import type { ChangeEvent } from 'react' import React from 'react' import { Form } from 'react-bootstrap' +import { useTranslatedText } from '../../hooks/common/use-translated-text' + +export interface AuthFieldProps { + onChange: (event: ChangeEvent) => void + invalid: boolean +} /** * Renders an input field for the password of a user. diff --git a/frontend/src/components/login-page/redirect-to-param-or-history.tsx b/frontend/src/components/login-page/redirect-to-param-or-history.tsx new file mode 100644 index 000000000..615f1dc13 --- /dev/null +++ b/frontend/src/components/login-page/redirect-to-param-or-history.tsx @@ -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 +} diff --git a/frontend/src/components/login-page/auth/utils.ts b/frontend/src/components/login-page/utils/fetch-and-set-user.ts similarity index 52% rename from frontend/src/components/login-page/auth/utils.ts rename to frontend/src/components/login-page/utils/fetch-and-set-user.ts index 14acb8c9b..6734bb3ee 100644 --- a/frontend/src/components/login-page/auth/utils.ts +++ b/frontend/src/components/login-page/utils/fetch-and-set-user.ts @@ -3,8 +3,7 @@ * * 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 { setUser } from '../../../redux/user/methods' @@ -21,13 +20,3 @@ export const fetchAndSetUser: () => Promise = async () => { 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) -} diff --git a/frontend/src/components/login-page/utils/filter-one-click-providers.ts b/frontend/src/components/login-page/utils/filter-one-click-providers.ts new file mode 100644 index 000000000..bb02c657e --- /dev/null +++ b/frontend/src/components/login-page/utils/filter-one-click-providers.ts @@ -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)) +} diff --git a/frontend/src/components/profile-page/settings/profile-display-name.tsx b/frontend/src/components/profile-page/settings/profile-display-name.tsx index 2a1a7ea56..f1ce0111a 100644 --- a/frontend/src/components/profile-page/settings/profile-display-name.tsx +++ b/frontend/src/components/profile-page/settings/profile-display-name.tsx @@ -7,12 +7,12 @@ import { updateDisplayName } from '../../../api/me' import { useApplicationState } from '../../../hooks/common/use-application-state' import { useOnInputChange } from '../../../hooks/common/use-on-input-change' import { DisplayNameField } from '../../common/fields/display-name-field' -import { fetchAndSetUser } from '../../login-page/auth/utils' import { useUiNotifications } from '../../notifications/ui-notification-boundary' import type { FormEvent } from 'react' import React, { useCallback, useMemo, useState } from 'react' import { Button, Card, Form } from 'react-bootstrap' 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. diff --git a/frontend/src/pages/api/private/auth/local/login.ts b/frontend/src/pages/api/private/auth/local/login.ts new file mode 100644 index 000000000..40dc07a53 --- /dev/null +++ b/frontend/src/pages/api/private/auth/local/login.ts @@ -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 diff --git a/frontend/src/pages/api/private/auth/logout.ts b/frontend/src/pages/api/private/auth/logout.ts new file mode 100644 index 000000000..e24a08988 --- /dev/null +++ b/frontend/src/pages/api/private/auth/logout.ts @@ -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 diff --git a/frontend/src/pages/api/private/me/index.ts b/frontend/src/pages/api/private/me/index.ts index 19f96cf33..130ae8641 100644 --- a/frontend/src/pages/api/private/me/index.ts +++ b/frontend/src/pages/api/private/me/index.ts @@ -8,6 +8,11 @@ import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/ import type { NextApiRequest, NextApiResponse } from 'next' 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(HttpMethod.GET, req, res, { username: 'mock', photo: '/public/img/avatar.png', diff --git a/frontend/src/pages/api/private/me/media.ts b/frontend/src/pages/api/private/me/media.ts index 7bce6b3c7..7f2909f13 100644 --- a/frontend/src/pages/api/private/me/media.ts +++ b/frontend/src/pages/api/private/me/media.ts @@ -7,11 +7,6 @@ import type { MediaUpload } from '../../../../api/media/types' import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request' 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) => { respondToMatchingRequest(HttpMethod.GET, req, res, [ {