mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-22 03:05:19 -04:00
Refactor login components and adjust login API routes (#1678)
* Refactor login components and adjust API routes Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Adjust API /me response and redux state Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Fix moved function Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Update cypress tests Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Adjust mock response Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Integrate new common fields and hook into profile page Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Remove openid Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Fix config mock Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de> Co-authored-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
fe640268c5
commit
eab189c3c6
44 changed files with 911 additions and 507 deletions
35
src/components/common/fields/current-password-field.tsx
Normal file
35
src/components/common/fields/current-password-field.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import type { CommonFieldProps } from './fields'
|
||||
import { Form } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Renders an input field for the current password when changing passwords.
|
||||
* @param onChange Hook that is called when the entered password changes.
|
||||
*/
|
||||
export const CurrentPasswordField: React.FC<CommonFieldProps> = ({ onChange }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<Trans i18nKey='profile.changePassword.old' />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type='password'
|
||||
size='sm'
|
||||
onChange={onChange}
|
||||
placeholder={t('login.auth.password')}
|
||||
className='bg-dark text-light'
|
||||
autoComplete='current-password'
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
)
|
||||
}
|
50
src/components/common/fields/display-name-field.tsx
Normal file
50
src/components/common/fields/display-name-field.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react'
|
||||
import type { CommonFieldProps } from './fields'
|
||||
import { Form } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
interface DisplayNameFieldProps extends CommonFieldProps {
|
||||
initialValue?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an input field for the display name when registering.
|
||||
* @param onChange Hook that is called when the entered display name changes.
|
||||
* @param value The currently entered display name.
|
||||
* @param initialValue The initial input field value.
|
||||
*/
|
||||
export const DisplayNameField: React.FC<DisplayNameFieldProps> = ({ onChange, value, initialValue }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
return value.trim() !== '' && value !== initialValue
|
||||
}, [value, initialValue])
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<Trans i18nKey='profile.displayName' />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type='text'
|
||||
size='sm'
|
||||
value={value}
|
||||
isValid={isValid}
|
||||
onChange={onChange}
|
||||
placeholder={t('profile.displayName')}
|
||||
className='bg-dark text-light'
|
||||
autoComplete='name'
|
||||
required
|
||||
/>
|
||||
<Form.Text>
|
||||
<Trans i18nKey='profile.displayNameInfo' />
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
)
|
||||
}
|
12
src/components/common/fields/fields.ts
Normal file
12
src/components/common/fields/fields.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { ChangeEvent } from 'react'
|
||||
|
||||
export interface CommonFieldProps {
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => void
|
||||
value: string
|
||||
}
|
45
src/components/common/fields/new-password-field.tsx
Normal file
45
src/components/common/fields/new-password-field.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react'
|
||||
import type { CommonFieldProps } from './fields'
|
||||
import { Form } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Renders an input field for the new password when registering.
|
||||
* @param onChange Hook that is called when the entered password changes.
|
||||
* @param value The currently entered password.
|
||||
*/
|
||||
export const NewPasswordField: React.FC<CommonFieldProps> = ({ onChange, value }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
return value.trim() !== '' && value.length >= 8
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<Trans i18nKey='profile.changePassword.new' />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type='password'
|
||||
size='sm'
|
||||
isValid={isValid}
|
||||
onChange={onChange}
|
||||
placeholder={t('login.auth.password')}
|
||||
className='bg-dark text-light'
|
||||
minLength={8}
|
||||
autoComplete='new-password'
|
||||
required
|
||||
/>
|
||||
<Form.Text>
|
||||
<Trans i18nKey='login.register.passwordInfo' />
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
)
|
||||
}
|
51
src/components/common/fields/password-again-field.tsx
Normal file
51
src/components/common/fields/password-again-field.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react'
|
||||
import type { CommonFieldProps } from './fields'
|
||||
import { Form } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
interface PasswordAgainFieldProps extends CommonFieldProps {
|
||||
password: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an input field for typing the new password again when registering.
|
||||
* @param onChange Hook that is called when the entered retype of the password changes.
|
||||
* @param value The currently entered retype of the password.
|
||||
* @param password The password entered into the password input field.
|
||||
*/
|
||||
export const PasswordAgainField: React.FC<PasswordAgainFieldProps> = ({ onChange, value, password }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isInvalid = useMemo(() => {
|
||||
return value !== '' && password !== value
|
||||
}, [password, value])
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
return password !== '' && password === value
|
||||
}, [password, value])
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<Trans i18nKey='login.register.passwordAgain' />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type='password'
|
||||
size='sm'
|
||||
isInvalid={isInvalid}
|
||||
isValid={isValid}
|
||||
onChange={onChange}
|
||||
placeholder={t('login.register.passwordAgain')}
|
||||
className='bg-dark text-light'
|
||||
autoComplete='new-password'
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
)
|
||||
}
|
46
src/components/common/fields/username-field.tsx
Normal file
46
src/components/common/fields/username-field.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react'
|
||||
import type { CommonFieldProps } from './fields'
|
||||
import { Form } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Renders an input field for the username when registering.
|
||||
* @param onChange Hook that is called when the entered username changes.
|
||||
* @param value The currently entered username.
|
||||
*/
|
||||
export const UsernameField: React.FC<CommonFieldProps> = ({ onChange, value }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
return value?.trim() !== ''
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<Trans i18nKey='login.auth.username' />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type='text'
|
||||
size='sm'
|
||||
value={value}
|
||||
isValid={isValid}
|
||||
onChange={onChange}
|
||||
placeholder={t('login.auth.username')}
|
||||
className='bg-dark text-light'
|
||||
autoComplete='username'
|
||||
autoFocus={true}
|
||||
required
|
||||
/>
|
||||
<Form.Text>
|
||||
<Trans i18nKey='login.register.usernameInfo' />
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
)
|
||||
}
|
|
@ -24,7 +24,7 @@ export const DocumentReadOnlyPageContent: React.FC = () => {
|
|||
return (
|
||||
<Fragment>
|
||||
<DocumentInfobar
|
||||
changedAuthor={noteDetails.lastChange.userName ?? ''}
|
||||
changedAuthor={noteDetails.lastChange.username ?? ''}
|
||||
changedTime={noteDetails.lastChange.timestamp}
|
||||
createdAuthor={'Test'}
|
||||
createdTime={noteDetails.createTime}
|
||||
|
|
|
@ -28,7 +28,7 @@ const allSupportedLinks = [
|
|||
|
||||
const getUserName = (): string => {
|
||||
const user = store.getState().user
|
||||
return user ? user.name : 'Anonymous'
|
||||
return user ? user.displayName : 'Anonymous'
|
||||
}
|
||||
|
||||
const linkAndExtraTagHint = (editor: Editor): Promise<Hints | null> => {
|
||||
|
|
|
@ -10,15 +10,16 @@ import type { ButtonProps } from 'react-bootstrap/Button'
|
|||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { LinkContainer } from 'react-router-bootstrap'
|
||||
import { ShowIf } from '../../common/show-if/show-if'
|
||||
import { getApiUrl } from '../../../api/utils'
|
||||
import { INTERACTIVE_LOGIN_METHODS } from '../../../api/auth'
|
||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||
import { cypressId } from '../../../utils/cypress-attribute'
|
||||
import { useBackendBaseUrl } from '../../../hooks/common/use-backend-base-url'
|
||||
|
||||
export type SignInButtonProps = Omit<ButtonProps, 'href'>
|
||||
|
||||
export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props }) => {
|
||||
const { t } = useTranslation()
|
||||
const backendBaseUrl = useBackendBaseUrl()
|
||||
const authProviders = useApplicationState((state) => state.config.authProviders)
|
||||
const authEnabled = useMemo(() => Object.values(authProviders).includes(true), [authProviders])
|
||||
|
||||
|
@ -29,10 +30,10 @@ export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props })
|
|||
const activeOneClickProviders = activeProviders.filter((entry) => !INTERACTIVE_LOGIN_METHODS.includes(entry))
|
||||
|
||||
if (activeProviders.length === 1 && activeOneClickProviders.length === 1) {
|
||||
return `${getApiUrl()}auth/${activeOneClickProviders[0]}`
|
||||
return `${backendBaseUrl}auth/${activeOneClickProviders[0]}`
|
||||
}
|
||||
return '/login'
|
||||
}, [authProviders])
|
||||
}, [authProviders, backendBaseUrl])
|
||||
|
||||
return (
|
||||
<ShowIf condition={authEnabled}>
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react'
|
||||
import { clearUser } from '../../../redux/user/methods'
|
||||
import { cypressId } from '../../../utils/cypress-attribute'
|
||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import { doLogout } from '../../../api/auth'
|
||||
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
||||
|
||||
/**
|
||||
* Renders a sign-out button as a dropdown item for the user-dropdown.
|
||||
*/
|
||||
export const SignOutDropdownButton: React.FC = () => {
|
||||
useTranslation()
|
||||
|
||||
const onSignOut = useCallback(() => {
|
||||
clearUser()
|
||||
doLogout().catch(showErrorNotification('login.logoutFailed'))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Dropdown.Item dir='auto' onClick={onSignOut} {...cypressId('user-dropdown-sign-out-button')}>
|
||||
<ForkAwesomeIcon icon='sign-out' fixedWidth={true} className='mx-2' />
|
||||
<Trans i18nKey='login.signOut' />
|
||||
</Dropdown.Item>
|
||||
)
|
||||
}
|
|
@ -8,11 +8,11 @@ import React from 'react'
|
|||
import { Dropdown } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { LinkContainer } from 'react-router-bootstrap'
|
||||
import { clearUser } from '../../../redux/user/methods'
|
||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||
import { UserAvatar } from '../../common/user-avatar/user-avatar'
|
||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||
import { cypressId } from '../../../utils/cypress-attribute'
|
||||
import { SignOutDropdownButton } from './sign-out-dropdown-button'
|
||||
|
||||
export const UserDropdown: React.FC = () => {
|
||||
useTranslation()
|
||||
|
@ -25,7 +25,7 @@ export const UserDropdown: React.FC = () => {
|
|||
return (
|
||||
<Dropdown alignRight>
|
||||
<Dropdown.Toggle size='sm' variant='dark' {...cypressId('user-dropdown')} className={'d-flex align-items-center'}>
|
||||
<UserAvatar name={user.name} photo={user.photo} />
|
||||
<UserAvatar name={user.displayName} photo={user.photo} />
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu className='text-start'>
|
||||
|
@ -41,15 +41,7 @@ export const UserDropdown: React.FC = () => {
|
|||
<Trans i18nKey='profile.userProfile' />
|
||||
</Dropdown.Item>
|
||||
</LinkContainer>
|
||||
<Dropdown.Item
|
||||
dir='auto'
|
||||
onClick={() => {
|
||||
clearUser()
|
||||
}}
|
||||
{...cypressId('user-dropdown-sign-out-button')}>
|
||||
<ForkAwesomeIcon icon='sign-out' fixedWidth={true} className='mx-2' />
|
||||
<Trans i18nKey='login.signOut' />
|
||||
</Dropdown.Item>
|
||||
<SignOutDropdownButton />
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)
|
||||
|
|
41
src/components/login-page/auth/auth-error/auth-error.tsx
Normal file
41
src/components/login-page/auth/auth-error/auth-error.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react'
|
||||
import { AuthError as AuthErrorType } from '../../../../api/auth'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
|
||||
export interface AuthErrorProps {
|
||||
error?: AuthErrorType
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an error message for auth fields when an error is present.
|
||||
* @param error The error to render. Can be {@code undefined} when no error should be rendered.
|
||||
*/
|
||||
export const AuthError: React.FC<AuthErrorProps> = ({ error }) => {
|
||||
useTranslation()
|
||||
|
||||
const errorMessageI18nKey = useMemo(() => {
|
||||
switch (error) {
|
||||
case AuthErrorType.INVALID_CREDENTIALS:
|
||||
return 'login.auth.error.usernamePassword'
|
||||
case AuthErrorType.LOGIN_DISABLED:
|
||||
return 'login.auth.error.loginDisabled'
|
||||
case AuthErrorType.OPENID_ERROR:
|
||||
return 'login.auth.error.openIdLogin'
|
||||
default:
|
||||
return 'login.auth.error.other'
|
||||
}
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<Alert className='small' show={!!error} variant='danger'>
|
||||
<Trans i18nKey={errorMessageI18nKey} />
|
||||
</Alert>
|
||||
)
|
||||
}
|
12
src/components/login-page/auth/fields/fields.ts
Normal file
12
src/components/login-page/auth/fields/fields.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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
|
||||
}
|
29
src/components/login-page/auth/fields/openid-field.tsx
Normal file
29
src/components/login-page/auth/fields/openid-field.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { Form } from 'react-bootstrap'
|
||||
import type { AuthFieldProps } from './fields'
|
||||
|
||||
/**
|
||||
* Renders an input field for the OpenID URL.
|
||||
* @param onChange Hook that is called when the entered OpenID URL changes.
|
||||
* @param invalid True when the entered OpenID URL is invalid, false otherwise.
|
||||
*/
|
||||
export const OpenidField: React.FC<AuthFieldProps> = ({ onChange, invalid }) => {
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
isInvalid={invalid}
|
||||
type='text'
|
||||
size='sm'
|
||||
placeholder={'OpenID'}
|
||||
onChange={onChange}
|
||||
className='bg-dark text-light'
|
||||
/>
|
||||
</Form.Group>
|
||||
)
|
||||
}
|
33
src/components/login-page/auth/fields/password-field.tsx
Normal file
33
src/components/login-page/auth/fields/password-field.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import type { AuthFieldProps } from './fields'
|
||||
import { Form } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Renders an input field for the password of a user.
|
||||
* @param onChange Hook that is called when the entered password changes.
|
||||
* @param invalid True when the entered password is invalid, false otherwise.
|
||||
*/
|
||||
export const PasswordField: React.FC<AuthFieldProps> = ({ onChange, invalid }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
isInvalid={invalid}
|
||||
type='password'
|
||||
size='sm'
|
||||
placeholder={t('login.auth.password')}
|
||||
onChange={onChange}
|
||||
className='bg-dark text-light'
|
||||
autoComplete='current-password'
|
||||
/>
|
||||
</Form.Group>
|
||||
)
|
||||
}
|
33
src/components/login-page/auth/fields/username-field.tsx
Normal file
33
src/components/login-page/auth/fields/username-field.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import type { AuthFieldProps } from './fields'
|
||||
import { Form } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Renders an input field for a username.
|
||||
* @param onChange Hook that is called when the input is changed.
|
||||
* @param invalid True indicates that the username is invalid, false otherwise.
|
||||
*/
|
||||
export const UsernameField: React.FC<AuthFieldProps> = ({ onChange, invalid }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
isInvalid={invalid}
|
||||
type='text'
|
||||
size='sm'
|
||||
placeholder={t('login.auth.username')}
|
||||
onChange={onChange}
|
||||
className='bg-dark text-light'
|
||||
autoComplete='username'
|
||||
/>
|
||||
</Form.Group>
|
||||
)
|
||||
}
|
|
@ -6,13 +6,22 @@
|
|||
|
||||
import { getMe } from '../../../api/me'
|
||||
import { setUser } from '../../../redux/user/methods'
|
||||
import { LoginProvider } from '../../../redux/user/types'
|
||||
|
||||
/**
|
||||
* Fetches metadata about the currently signed-in user from the API and stores it into the redux.
|
||||
*/
|
||||
export const fetchAndSetUser: () => Promise<void> = async () => {
|
||||
const me = await getMe()
|
||||
setUser({
|
||||
id: me.id,
|
||||
name: me.name,
|
||||
photo: me.photo,
|
||||
provider: me.provider
|
||||
})
|
||||
try {
|
||||
const me = await getMe()
|
||||
setUser({
|
||||
username: me.username,
|
||||
displayName: me.displayName,
|
||||
photo: me.photo,
|
||||
provider: LoginProvider.LOCAL, // TODO Use real provider instead
|
||||
email: me.email
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,34 +5,53 @@
|
|||
*/
|
||||
|
||||
import type { FormEvent } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { Alert, Button, Card, Form } from 'react-bootstrap'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { Button, Card, Form } from 'react-bootstrap'
|
||||
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { doLdapLogin } from '../../../api/auth'
|
||||
import { doLdapLogin } from '../../../api/auth/ldap'
|
||||
import { fetchAndSetUser } from './utils'
|
||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||
import { AuthError as AuthErrorType } from '../../../api/auth'
|
||||
import { UsernameField } from './fields/username-field'
|
||||
import { PasswordField } from './fields/password-field'
|
||||
import { AuthError } from './auth-error/auth-error'
|
||||
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
||||
|
||||
/**
|
||||
* Renders the LDAP login box with username and password field.
|
||||
*/
|
||||
export const ViaLdap: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
useTranslation()
|
||||
const ldapCustomName = useApplicationState((state) => state.config.customAuthNames.ldap)
|
||||
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState(false)
|
||||
const [error, setError] = useState<AuthErrorType>()
|
||||
|
||||
const name = ldapCustomName ? `${ldapCustomName} (LDAP)` : 'LDAP'
|
||||
const name = useMemo(() => {
|
||||
return ldapCustomName ? `${ldapCustomName} (LDAP)` : 'LDAP'
|
||||
}, [ldapCustomName])
|
||||
|
||||
const onLoginSubmit = useCallback(
|
||||
(event: FormEvent) => {
|
||||
doLdapLogin(username, password)
|
||||
.then(() => fetchAndSetUser())
|
||||
.catch(() => setError(true))
|
||||
.catch((error: Error) => {
|
||||
setError(
|
||||
Object.values(AuthErrorType).includes(error.message as AuthErrorType)
|
||||
? (error.message as AuthErrorType)
|
||||
: AuthErrorType.OTHER
|
||||
)
|
||||
})
|
||||
event.preventDefault()
|
||||
},
|
||||
[username, password]
|
||||
)
|
||||
|
||||
const onUsernameChange = useOnInputChange(setUsername)
|
||||
const onPasswordChange = useOnInputChange(setPassword)
|
||||
|
||||
return (
|
||||
<Card className='bg-dark mb-4'>
|
||||
<Card.Body>
|
||||
|
@ -40,33 +59,9 @@ export const ViaLdap: React.FC = () => {
|
|||
<Trans i18nKey='login.signInVia' values={{ service: name }} />
|
||||
</Card.Title>
|
||||
<Form onSubmit={onLoginSubmit}>
|
||||
<Form.Group controlId='ldap-username'>
|
||||
<Form.Control
|
||||
isInvalid={error}
|
||||
type='text'
|
||||
size='sm'
|
||||
placeholder={t('login.auth.username')}
|
||||
onChange={(event) => setUsername(event.currentTarget.value)}
|
||||
className='bg-dark text-light'
|
||||
autoComplete='username'
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId='ldap-password'>
|
||||
<Form.Control
|
||||
isInvalid={error}
|
||||
type='password'
|
||||
size='sm'
|
||||
placeholder={t('login.auth.password')}
|
||||
onChange={(event) => setPassword(event.currentTarget.value)}
|
||||
className='bg-dark text-light'
|
||||
autoComplete='current-password'
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Alert className='small' show={error} variant='danger'>
|
||||
<Trans i18nKey='login.auth.error.usernamePassword' />
|
||||
</Alert>
|
||||
<UsernameField onChange={onUsernameChange} invalid={!!error} />
|
||||
<PasswordField onChange={onPasswordChange} invalid={!!error} />
|
||||
<AuthError error={error} />
|
||||
|
||||
<Button type='submit' variant='primary'>
|
||||
<Trans i18nKey='login.signIn' />
|
||||
|
|
|
@ -4,33 +4,54 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { FormEvent } from 'react'
|
||||
import type { ChangeEvent, FormEvent } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { Alert, Button, Card, Form } from 'react-bootstrap'
|
||||
import { Button, Card, Form } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { doInternalLogin } from '../../../api/auth'
|
||||
import { doLocalLogin } from '../../../api/auth/local'
|
||||
import { ShowIf } from '../../common/show-if/show-if'
|
||||
import { fetchAndSetUser } from './utils'
|
||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||
import { AuthError as AuthErrorType } from '../../../api/auth'
|
||||
import { UsernameField } from './fields/username-field'
|
||||
import { PasswordField } from './fields/password-field'
|
||||
import { AuthError } from './auth-error/auth-error'
|
||||
|
||||
export const ViaInternal: React.FC = () => {
|
||||
/**
|
||||
* Renders the local login box with username and password field and the optional button for registering a new user.
|
||||
*/
|
||||
export const ViaLocal: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState(false)
|
||||
const [error, setError] = useState<AuthErrorType>()
|
||||
const allowRegister = useApplicationState((state) => state.config.allowRegister)
|
||||
|
||||
const onLoginSubmit = useCallback(
|
||||
(event: FormEvent) => {
|
||||
doInternalLogin(username, password)
|
||||
doLocalLogin(username, password)
|
||||
.then(() => fetchAndSetUser())
|
||||
.catch(() => setError(true))
|
||||
.catch((error: Error) => {
|
||||
setError(
|
||||
Object.values(AuthErrorType).includes(error.message as AuthErrorType)
|
||||
? (error.message as AuthErrorType)
|
||||
: AuthErrorType.OTHER
|
||||
)
|
||||
})
|
||||
event.preventDefault()
|
||||
},
|
||||
[username, password]
|
||||
)
|
||||
|
||||
const onUsernameChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setUsername(event.target.value)
|
||||
}, [])
|
||||
|
||||
const onPasswordChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setPassword(event.target.value)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Card className='bg-dark mb-4'>
|
||||
<Card.Body>
|
||||
|
@ -38,33 +59,9 @@ export const ViaInternal: React.FC = () => {
|
|||
<Trans i18nKey='login.signInVia' values={{ service: t('login.auth.username') }} />
|
||||
</Card.Title>
|
||||
<Form onSubmit={onLoginSubmit}>
|
||||
<Form.Group controlId='internal-username'>
|
||||
<Form.Control
|
||||
isInvalid={error}
|
||||
type='text'
|
||||
size='sm'
|
||||
placeholder={t('login.auth.username')}
|
||||
onChange={(event) => setUsername(event.currentTarget.value)}
|
||||
className='bg-dark text-light'
|
||||
autoComplete='username'
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId='internal-password'>
|
||||
<Form.Control
|
||||
isInvalid={error}
|
||||
type='password'
|
||||
size='sm'
|
||||
placeholder={t('login.auth.password')}
|
||||
onChange={(event) => setPassword(event.currentTarget.value)}
|
||||
className='bg-dark text-light'
|
||||
autoComplete='current-password'
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Alert className='small' show={error} variant='danger'>
|
||||
<Trans i18nKey='login.auth.error.usernamePassword' />
|
||||
</Alert>
|
||||
<UsernameField onChange={onUsernameChange} invalid={!!error} />
|
||||
<PasswordField onChange={onPasswordChange} invalid={!!error} />
|
||||
<AuthError error={error} />
|
||||
|
||||
<div className='flex flex-row' dir='auto'>
|
||||
<Button type='submit' variant='primary' className='mx-2'>
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { FormEvent } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { Alert, Button, Card, Form } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { doOpenIdLogin } from '../../../api/auth'
|
||||
import { fetchAndSetUser } from './utils'
|
||||
|
||||
export const ViaOpenId: React.FC = () => {
|
||||
useTranslation()
|
||||
const [openId, setOpenId] = useState('')
|
||||
const [error, setError] = useState(false)
|
||||
const doAsyncLogin: () => Promise<void> = async () => {
|
||||
await doOpenIdLogin(openId)
|
||||
await fetchAndSetUser()
|
||||
}
|
||||
|
||||
const onFormSubmit = (event: FormEvent) => {
|
||||
doAsyncLogin().catch(() => setError(true))
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className='bg-dark mb-4'>
|
||||
<Card.Body>
|
||||
<Card.Title>
|
||||
<Trans i18nKey='login.signInVia' values={{ service: 'OpenID' }} />
|
||||
</Card.Title>
|
||||
|
||||
<Form onSubmit={onFormSubmit}>
|
||||
<Form.Group controlId='openid'>
|
||||
<Form.Control
|
||||
isInvalid={error}
|
||||
type='text'
|
||||
size='sm'
|
||||
placeholder={'OpenID'}
|
||||
onChange={(event) => setOpenId(event.currentTarget.value)}
|
||||
className='bg-dark text-light'
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Alert className='small' show={error} variant='danger'>
|
||||
<Trans i18nKey='login.auth.error.openIdLogin' />
|
||||
</Alert>
|
||||
|
||||
<Button type='submit' variant='primary'>
|
||||
<Trans i18nKey='login.signIn' />
|
||||
</Button>
|
||||
</Form>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
)
|
||||
}
|
|
@ -4,17 +4,20 @@
|
|||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react'
|
||||
import React, { Fragment, useCallback, useMemo } from 'react'
|
||||
import { Card, Col, Row } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Redirect } from 'react-router'
|
||||
import { ShowIf } from '../common/show-if/show-if'
|
||||
import { ViaInternal } from './auth/via-internal'
|
||||
import { ViaLocal } from './auth/via-local'
|
||||
import { ViaLdap } from './auth/via-ldap'
|
||||
import { OneClickType, ViaOneClick } from './auth/via-one-click'
|
||||
import { ViaOpenId } from './auth/via-openid'
|
||||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const LoginPage: React.FC = () => {
|
||||
useTranslation()
|
||||
const authProviders = useApplicationState((state) => state.config.authProviders)
|
||||
|
@ -33,16 +36,29 @@ export const LoginPage: React.FC = () => {
|
|||
authProviders.twitter
|
||||
]
|
||||
|
||||
const oneClickCustomName: (type: OneClickType) => string | undefined = (type) => {
|
||||
switch (type) {
|
||||
case OneClickType.SAML:
|
||||
return customSamlAuthName
|
||||
case OneClickType.OAUTH2:
|
||||
return customOauthAuthName
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
const oneClickCustomName = useCallback(
|
||||
(type: OneClickType): string | undefined => {
|
||||
switch (type) {
|
||||
case OneClickType.SAML:
|
||||
return customSamlAuthName
|
||||
case OneClickType.OAUTH2:
|
||||
return customOauthAuthName
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
[customOauthAuthName, customSamlAuthName]
|
||||
)
|
||||
|
||||
const oneClickButtonsDom = useMemo(() => {
|
||||
return Object.values(OneClickType)
|
||||
.filter((value) => authProviders[value])
|
||||
.map((value) => (
|
||||
<div className='p-2 d-flex flex-column social-button-container' key={value}>
|
||||
<ViaOneClick oneClickType={value} optionalName={oneClickCustomName(value)} />
|
||||
</div>
|
||||
))
|
||||
}, [authProviders, oneClickCustomName])
|
||||
|
||||
if (userLoggedIn) {
|
||||
// TODO Redirect to previous page?
|
||||
|
@ -53,17 +69,14 @@ export const LoginPage: React.FC = () => {
|
|||
<Fragment>
|
||||
<div className='my-3'>
|
||||
<Row className='h-100 flex justify-content-center'>
|
||||
<ShowIf condition={authProviders.internal || authProviders.ldap || authProviders.openid}>
|
||||
<ShowIf condition={authProviders.local || authProviders.ldap}>
|
||||
<Col xs={12} sm={10} lg={4}>
|
||||
<ShowIf condition={authProviders.internal}>
|
||||
<ViaInternal />
|
||||
<ShowIf condition={authProviders.local}>
|
||||
<ViaLocal />
|
||||
</ShowIf>
|
||||
<ShowIf condition={authProviders.ldap}>
|
||||
<ViaLdap />
|
||||
</ShowIf>
|
||||
<ShowIf condition={authProviders.openid}>
|
||||
<ViaOpenId />
|
||||
</ShowIf>
|
||||
</Col>
|
||||
</ShowIf>
|
||||
<ShowIf condition={oneClickProviders.includes(true)}>
|
||||
|
@ -73,13 +86,7 @@ export const LoginPage: React.FC = () => {
|
|||
<Card.Title>
|
||||
<Trans i18nKey='login.signInVia' values={{ service: '' }} />
|
||||
</Card.Title>
|
||||
{Object.values(OneClickType)
|
||||
.filter((value) => authProviders[value])
|
||||
.map((value) => (
|
||||
<div className='p-2 d-flex flex-column social-button-container' key={value}>
|
||||
<ViaOneClick oneClickType={value} optionalName={oneClickCustomName(value)} />
|
||||
</div>
|
||||
))}
|
||||
{oneClickButtonsDom}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
|
|
|
@ -32,7 +32,7 @@ export const ProfilePage: React.FC = () => {
|
|||
<Row className='h-100 flex justify-content-center'>
|
||||
<Col lg={6}>
|
||||
<ProfileDisplayName />
|
||||
<ShowIf condition={userProvider === LoginProvider.INTERNAL}>
|
||||
<ShowIf condition={userProvider === LoginProvider.LOCAL}>
|
||||
<ProfileChangePassword />
|
||||
</ShowIf>
|
||||
<ProfileAccessTokens />
|
||||
|
|
|
@ -4,14 +4,16 @@
|
|||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { ChangeEvent, FormEvent } from 'react'
|
||||
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 { changePassword } from '../../../api/me'
|
||||
import { doLocalPasswordChange } from '../../../api/auth/local'
|
||||
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
||||
|
||||
const REGEX_VALID_PASSWORD = /^[^\s].{5,}$/
|
||||
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
||||
import { NewPasswordField } from '../../common/fields/new-password-field'
|
||||
import { PasswordAgainField } from '../../common/fields/password-again-field'
|
||||
import { CurrentPasswordField } from '../../common/fields/current-password-field'
|
||||
|
||||
/**
|
||||
* Profile page section for changing the password when using internal login.
|
||||
|
@ -22,34 +24,27 @@ export const ProfileChangePassword: React.FC = () => {
|
|||
const [newPassword, setNewPassword] = useState('')
|
||||
const [newPasswordAgain, setNewPasswordAgain] = useState('')
|
||||
|
||||
const newPasswordValid = useMemo(() => {
|
||||
return REGEX_VALID_PASSWORD.test(newPassword)
|
||||
}, [newPassword])
|
||||
|
||||
const newPasswordAgainValid = useMemo(() => {
|
||||
return newPassword === newPasswordAgain
|
||||
}, [newPassword, newPasswordAgain])
|
||||
|
||||
const onChangeOldPassword = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setOldPassword(event.target.value)
|
||||
}, [])
|
||||
|
||||
const onChangeNewPassword = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setNewPassword(event.target.value)
|
||||
}, [])
|
||||
|
||||
const onChangeNewPasswordAgain = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setNewPasswordAgain(event.target.value)
|
||||
}, [])
|
||||
const onChangeOldPassword = useOnInputChange(setOldPassword)
|
||||
const onChangeNewPassword = useOnInputChange(setNewPassword)
|
||||
const onChangeNewPasswordAgain = useOnInputChange(setNewPasswordAgain)
|
||||
|
||||
const onSubmitPasswordChange = useCallback(
|
||||
(event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
changePassword(oldPassword, newPassword).catch(showErrorNotification('profile.changePassword.failed'))
|
||||
doLocalPasswordChange(oldPassword, newPassword).catch(showErrorNotification('profile.changePassword.failed'))
|
||||
},
|
||||
[oldPassword, newPassword]
|
||||
)
|
||||
|
||||
const ready = useMemo(() => {
|
||||
return (
|
||||
oldPassword.trim() !== '' &&
|
||||
newPassword.trim() !== '' &&
|
||||
newPasswordAgain.trim() !== '' &&
|
||||
newPassword === newPasswordAgain
|
||||
)
|
||||
}, [oldPassword, newPassword, newPasswordAgain])
|
||||
|
||||
return (
|
||||
<Card className='bg-dark mb-4'>
|
||||
<Card.Body>
|
||||
|
@ -57,53 +52,11 @@ export const ProfileChangePassword: React.FC = () => {
|
|||
<Trans i18nKey='profile.changePassword.title' />
|
||||
</Card.Title>
|
||||
<Form onSubmit={onSubmitPasswordChange} className='text-left'>
|
||||
<Form.Group controlId='oldPassword'>
|
||||
<Form.Label>
|
||||
<Trans i18nKey='profile.changePassword.old' />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type='password'
|
||||
size='sm'
|
||||
className='bg-dark text-light'
|
||||
autoComplete='current-password'
|
||||
required
|
||||
onChange={onChangeOldPassword}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId='newPassword'>
|
||||
<Form.Label>
|
||||
<Trans i18nKey='profile.changePassword.new' />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type='password'
|
||||
size='sm'
|
||||
className='bg-dark text-light'
|
||||
autoComplete='new-password'
|
||||
required
|
||||
onChange={onChangeNewPassword}
|
||||
isValid={newPasswordValid}
|
||||
/>
|
||||
<Form.Text>
|
||||
<Trans i18nKey='profile.changePassword.info' />
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<Form.Group controlId='newPasswordAgain'>
|
||||
<Form.Label>
|
||||
<Trans i18nKey='profile.changePassword.newAgain' />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type='password'
|
||||
size='sm'
|
||||
className='bg-dark text-light'
|
||||
required
|
||||
autoComplete='new-password'
|
||||
onChange={onChangeNewPasswordAgain}
|
||||
isValid={newPasswordAgainValid}
|
||||
isInvalid={newPasswordAgain !== '' && !newPasswordAgainValid}
|
||||
/>
|
||||
</Form.Group>
|
||||
<CurrentPasswordField onChange={onChangeOldPassword} value={oldPassword} />
|
||||
<NewPasswordField onChange={onChangeNewPassword} value={newPassword} />
|
||||
<PasswordAgainField password={newPassword} onChange={onChangeNewPasswordAgain} value={newPasswordAgain} />
|
||||
|
||||
<Button type='submit' variant='primary' disabled={!newPasswordValid || !newPasswordAgainValid}>
|
||||
<Button type='submit' variant='primary' disabled={!ready}>
|
||||
<Trans i18nKey='common.save' />
|
||||
</Button>
|
||||
</Form>
|
||||
|
|
|
@ -4,33 +4,26 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { ChangeEvent, FormEvent } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
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 { updateDisplayName } from '../../../api/me'
|
||||
import { fetchAndSetUser } from '../../login-page/auth/utils'
|
||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
||||
import { DisplayNameField } from '../../common/fields/display-name-field'
|
||||
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
||||
|
||||
/**
|
||||
* Profile page section for changing the current display name.
|
||||
*/
|
||||
export const ProfileDisplayName: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const userName = useApplicationState((state) => state.user?.name)
|
||||
const [displayName, setDisplayName] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (userName !== undefined) {
|
||||
setDisplayName(userName)
|
||||
}
|
||||
}, [userName])
|
||||
|
||||
const onChangeDisplayName = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setDisplayName(event.target.value)
|
||||
}, [])
|
||||
useTranslation()
|
||||
const userName = useApplicationState((state) => state.user?.displayName)
|
||||
const [displayName, setDisplayName] = useState(userName ?? '')
|
||||
|
||||
const onChangeDisplayName = useOnInputChange(setDisplayName)
|
||||
const onSubmitNameChange = useCallback(
|
||||
(event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
|
@ -42,8 +35,8 @@ export const ProfileDisplayName: React.FC = () => {
|
|||
)
|
||||
|
||||
const formSubmittable = useMemo(() => {
|
||||
return displayName.trim() !== ''
|
||||
}, [displayName])
|
||||
return displayName.trim() !== '' && displayName !== userName
|
||||
}, [displayName, userName])
|
||||
|
||||
return (
|
||||
<Card className='bg-dark mb-4'>
|
||||
|
@ -52,24 +45,7 @@ export const ProfileDisplayName: React.FC = () => {
|
|||
<Trans i18nKey='profile.userProfile' />
|
||||
</Card.Title>
|
||||
<Form onSubmit={onSubmitNameChange} className='text-left'>
|
||||
<Form.Group controlId='displayName'>
|
||||
<Form.Label>
|
||||
<Trans i18nKey='profile.displayName' />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type='text'
|
||||
size='sm'
|
||||
placeholder={t('profile.displayName')}
|
||||
value={displayName}
|
||||
className='bg-dark text-light'
|
||||
onChange={onChangeDisplayName}
|
||||
isValid={formSubmittable}
|
||||
required
|
||||
/>
|
||||
<Form.Text>
|
||||
<Trans i18nKey='profile.displayNameInfo' />
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<DisplayNameField onChange={onChangeDisplayName} value={displayName} initialValue={userName} />
|
||||
|
||||
<Button type='submit' variant='primary' disabled={!formSubmittable}>
|
||||
<Trans i18nKey='common.save' />
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react'
|
||||
import { RegisterError as RegisterErrorType } from '../../../api/auth'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
|
||||
export interface RegisterErrorProps {
|
||||
error?: RegisterErrorType
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an error message for registration fields when an error is present.
|
||||
* @param error The error to render. Can be {@code undefined} when no error should be rendered.
|
||||
*/
|
||||
export const RegisterError: React.FC<RegisterErrorProps> = ({ error }) => {
|
||||
useTranslation()
|
||||
|
||||
const errorMessageI18nKey = useMemo(() => {
|
||||
switch (error) {
|
||||
case RegisterErrorType.REGISTRATION_DISABLED:
|
||||
return 'login.register.error.registrationDisabled'
|
||||
case RegisterErrorType.USERNAME_EXISTING:
|
||||
return 'login.register.error.usernameExisting'
|
||||
default:
|
||||
return 'login.register.error.other'
|
||||
}
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<Alert className='small' show={!!error} variant='danger'>
|
||||
<Trans i18nKey={errorMessageI18nKey} />
|
||||
</Alert>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { ShowIf } from '../../common/show-if/show-if'
|
||||
import { TranslatedExternalLink } from '../../common/links/translated-external-link'
|
||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||
|
||||
/**
|
||||
* Renders the links to information and conditions on registering an account.
|
||||
*/
|
||||
export const RegisterInfos: React.FC = () => {
|
||||
useTranslation()
|
||||
const specialUrls = useApplicationState((state) => state.config.specialUrls)
|
||||
|
||||
return (
|
||||
<ShowIf condition={!!specialUrls.termsOfUse || !!specialUrls.privacy}>
|
||||
<Trans i18nKey='login.register.infoTermsPrivacy' />
|
||||
<ul>
|
||||
<ShowIf condition={!!specialUrls.termsOfUse}>
|
||||
<li>
|
||||
<TranslatedExternalLink i18nKey='landing.footer.termsOfUse' href={specialUrls.termsOfUse ?? ''} />
|
||||
</li>
|
||||
</ShowIf>
|
||||
<ShowIf condition={!!specialUrls.privacy}>
|
||||
<li>
|
||||
<TranslatedExternalLink i18nKey='landing.footer.privacy' href={specialUrls.privacy ?? ''} />
|
||||
</li>
|
||||
</ShowIf>
|
||||
</ul>
|
||||
</ShowIf>
|
||||
)
|
||||
}
|
|
@ -5,53 +5,66 @@
|
|||
*/
|
||||
|
||||
import type { FormEvent } from 'react'
|
||||
import React, { Fragment, useCallback, useEffect, useState } from 'react'
|
||||
import { Alert, Button, Card, Col, Form, Row } from 'react-bootstrap'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { Button, Card, Col, Form, Row } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Redirect } from 'react-router'
|
||||
import { doInternalRegister } from '../../api/auth'
|
||||
import { doLocalRegister } from '../../api/auth/local'
|
||||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
||||
import { TranslatedExternalLink } from '../common/links/translated-external-link'
|
||||
import { ShowIf } from '../common/show-if/show-if'
|
||||
import { fetchAndSetUser } from '../login-page/auth/utils'
|
||||
import { Logger } from '../../utils/logger'
|
||||
|
||||
const log = new Logger('RegisterPage')
|
||||
|
||||
export enum RegisterError {
|
||||
NONE = 'none',
|
||||
USERNAME_EXISTING = 'usernameExisting',
|
||||
OTHER = 'other'
|
||||
}
|
||||
import { RegisterError as RegisterErrorType } from '../../api/auth'
|
||||
import { RegisterInfos } from './register-infos/register-infos'
|
||||
import { UsernameField } from '../common/fields/username-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 { useOnInputChange } from '../../hooks/common/use-on-input-change'
|
||||
import { RegisterError } from './register-error/register-error'
|
||||
|
||||
/**
|
||||
* Renders the registration page with fields for username, display name, password, password retype and information about terms and conditions.
|
||||
*/
|
||||
export const RegisterPage: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
useTranslation()
|
||||
const allowRegister = useApplicationState((state) => state.config.allowRegister)
|
||||
const specialUrls = useApplicationState((state) => state.config.specialUrls)
|
||||
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(RegisterError.NONE)
|
||||
const [ready, setReady] = useState(false)
|
||||
const [error, setError] = useState<RegisterErrorType>()
|
||||
|
||||
const doRegisterSubmit = useCallback(
|
||||
(event: FormEvent) => {
|
||||
doInternalRegister(username, password)
|
||||
doLocalRegister(username, displayName, password)
|
||||
.then(() => fetchAndSetUser())
|
||||
.catch((err: Error) => {
|
||||
log.error(err)
|
||||
setError(err.message === RegisterError.USERNAME_EXISTING ? err.message : RegisterError.OTHER)
|
||||
.catch((error: Error) => {
|
||||
setError(
|
||||
Object.values(RegisterErrorType).includes(error.message as RegisterErrorType)
|
||||
? (error.message as RegisterErrorType)
|
||||
: RegisterErrorType.OTHER
|
||||
)
|
||||
})
|
||||
event.preventDefault()
|
||||
},
|
||||
[username, password]
|
||||
[username, displayName, password]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setReady(username !== '' && password !== '' && password.length >= 8 && password === passwordAgain)
|
||||
}, [username, password, passwordAgain])
|
||||
const ready = useMemo(() => {
|
||||
return (
|
||||
username.trim() !== '' &&
|
||||
displayName.trim() !== '' &&
|
||||
password.trim() !== '' &&
|
||||
password.length >= 8 &&
|
||||
password === passwordAgain
|
||||
)
|
||||
}, [username, password, displayName, passwordAgain])
|
||||
|
||||
const onUsernameChange = useOnInputChange(setUsername)
|
||||
const onDisplayNameChange = useOnInputChange(setDisplayName)
|
||||
const onPasswordChange = useOnInputChange(setPassword)
|
||||
const onPasswordAgainChange = useOnInputChange(setPasswordAgain)
|
||||
|
||||
if (!allowRegister) {
|
||||
return <Redirect to={'/login'} />
|
||||
|
@ -62,102 +75,32 @@ export const RegisterPage: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<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='bg-dark mb-4 text-start'>
|
||||
<Card.Body>
|
||||
<Form onSubmit={doRegisterSubmit}>
|
||||
<Form.Group controlId='username'>
|
||||
<Form.Label>
|
||||
<Trans i18nKey='login.auth.username' />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type='text'
|
||||
size='sm'
|
||||
value={username}
|
||||
isValid={username !== ''}
|
||||
onChange={(event) => setUsername(event.target.value)}
|
||||
placeholder={t('login.auth.username')}
|
||||
className='bg-dark text-light'
|
||||
autoComplete='username'
|
||||
autoFocus={true}
|
||||
required
|
||||
/>
|
||||
<Form.Text>
|
||||
<Trans i18nKey='login.register.usernameInfo' />
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<Form.Group controlId='password'>
|
||||
<Form.Label>
|
||||
<Trans i18nKey='login.auth.password' />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type='password'
|
||||
size='sm'
|
||||
isValid={password !== '' && password.length >= 8}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder={t('login.auth.password')}
|
||||
className='bg-dark text-light'
|
||||
minLength={8}
|
||||
autoComplete='new-password'
|
||||
required
|
||||
/>
|
||||
<Form.Text>
|
||||
<Trans i18nKey='login.register.passwordInfo' />
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<Form.Group controlId='re-password'>
|
||||
<Form.Label>
|
||||
<Trans i18nKey='login.register.passwordAgain' />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type='password'
|
||||
size='sm'
|
||||
isInvalid={passwordAgain !== '' && password !== passwordAgain}
|
||||
isValid={passwordAgain !== '' && password === passwordAgain}
|
||||
onChange={(event) => setPasswordAgain(event.target.value)}
|
||||
placeholder={t('login.register.passwordAgain')}
|
||||
className='bg-dark text-light'
|
||||
autoComplete='new-password'
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<ShowIf condition={!!specialUrls.termsOfUse || !!specialUrls.privacy}>
|
||||
<Trans i18nKey='login.register.infoTermsPrivacy' />
|
||||
<ul>
|
||||
<ShowIf condition={!!specialUrls.termsOfUse}>
|
||||
<li>
|
||||
<TranslatedExternalLink
|
||||
i18nKey='landing.footer.termsOfUse'
|
||||
href={specialUrls.termsOfUse ?? ''}
|
||||
/>
|
||||
</li>
|
||||
</ShowIf>
|
||||
<ShowIf condition={!!specialUrls.privacy}>
|
||||
<li>
|
||||
<TranslatedExternalLink i18nKey='landing.footer.privacy' href={specialUrls.privacy ?? ''} />
|
||||
</li>
|
||||
</ShowIf>
|
||||
</ul>
|
||||
</ShowIf>
|
||||
<Button variant='primary' type='submit' block={true} disabled={!ready}>
|
||||
<Trans i18nKey='login.register.title' />
|
||||
</Button>
|
||||
</Form>
|
||||
<br />
|
||||
<Alert show={error !== RegisterError.NONE} variant='danger'>
|
||||
<Trans i18nKey={`login.register.error.${error}`} />
|
||||
</Alert>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Fragment>
|
||||
<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='bg-dark mb-4 text-start'>
|
||||
<Card.Body>
|
||||
<Form onSubmit={doRegisterSubmit}>
|
||||
<UsernameField onChange={onUsernameChange} value={username} />
|
||||
<DisplayNameField onChange={onDisplayNameChange} value={displayName} />
|
||||
<NewPasswordField onChange={onPasswordChange} value={password} />
|
||||
<PasswordAgainField password={password} onChange={onPasswordAgainChange} value={passwordAgain} />
|
||||
|
||||
<RegisterInfos />
|
||||
|
||||
<Button variant='primary' type='submit' block={true} disabled={!ready}>
|
||||
<Trans i18nKey='login.register.title' />
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
<RegisterError error={error} />
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue