mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 11:16:31 -05: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
|
@ -14,8 +14,7 @@ const authProvidersDisabled = {
|
|||
google: false,
|
||||
saml: false,
|
||||
oauth2: false,
|
||||
internal: false,
|
||||
openid: false
|
||||
local: false
|
||||
}
|
||||
|
||||
const initLoggedOutTestWithCustomAuthProviders = (
|
||||
|
@ -50,7 +49,7 @@ describe('When logged-out ', () => {
|
|||
describe('and an interactive auth-provider is enabled, ', () => {
|
||||
it('sign-in button points to login route: internal', () => {
|
||||
initLoggedOutTestWithCustomAuthProviders(cy, {
|
||||
internal: true
|
||||
local: true
|
||||
})
|
||||
cy.getById('sign-in-button').should('be.visible').should('have.attr', 'href', '/login')
|
||||
})
|
||||
|
@ -61,13 +60,6 @@ describe('When logged-out ', () => {
|
|||
})
|
||||
cy.getById('sign-in-button').should('be.visible').should('have.attr', 'href', '/login')
|
||||
})
|
||||
|
||||
it('sign-in button points to login route: openid', () => {
|
||||
initLoggedOutTestWithCustomAuthProviders(cy, {
|
||||
openid: true
|
||||
})
|
||||
cy.getById('sign-in-button').should('be.visible').should('have.attr', 'href', '/login')
|
||||
})
|
||||
})
|
||||
|
||||
describe('and only one one-click auth-provider is enabled, ', () => {
|
||||
|
@ -78,7 +70,7 @@ describe('When logged-out ', () => {
|
|||
cy.getById('sign-in-button')
|
||||
.should('be.visible')
|
||||
// The absolute URL is used because it is defined as API base URL absolute.
|
||||
.should('have.attr', 'href', '/mock-backend/api/private/auth/saml')
|
||||
.should('have.attr', 'href', '/mock-backend/auth/saml')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -96,7 +88,7 @@ describe('When logged-out ', () => {
|
|||
it('sign-in button points to login route', () => {
|
||||
initLoggedOutTestWithCustomAuthProviders(cy, {
|
||||
saml: true,
|
||||
internal: true
|
||||
local: true
|
||||
})
|
||||
cy.getById('sign-in-button').should('be.visible').should('have.attr', 'href', '/login')
|
||||
})
|
||||
|
|
|
@ -25,8 +25,7 @@ export const authProviders = {
|
|||
google: true,
|
||||
saml: true,
|
||||
oauth2: true,
|
||||
internal: true,
|
||||
openid: true
|
||||
local: true
|
||||
}
|
||||
|
||||
export const config = {
|
||||
|
|
|
@ -494,13 +494,16 @@
|
|||
"signInVia": "Sign in via {{service}}",
|
||||
"signIn": "Sign In",
|
||||
"signOut": "Sign Out",
|
||||
"logoutFailed": "There was an error logging you out.\nClose your browser window to ensure session data is removed.",
|
||||
"auth": {
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"username": "Username",
|
||||
"error": {
|
||||
"openIdLogin": "Invalid OpenID provided",
|
||||
"usernamePassword": "Invalid username or password"
|
||||
"usernamePassword": "Invalid username or password",
|
||||
"loginDisabled": "The login is disabled",
|
||||
"other": "There was an error logging you in."
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
|
@ -510,6 +513,7 @@
|
|||
"passwordInfo": "Choose a unique and secure password. It must contain at least 8 characters.",
|
||||
"infoTermsPrivacy": "With the registration of my user account I agree to the following terms:",
|
||||
"error": {
|
||||
"registrationDisabled": "The registration is disabled",
|
||||
"usernameExisting": "There is already an account with this username.",
|
||||
"other": "There was an error while registering your account. Just try it again."
|
||||
}
|
||||
|
|
|
@ -10,8 +10,7 @@
|
|||
"google": true,
|
||||
"saml": true,
|
||||
"oauth2": true,
|
||||
"internal": true,
|
||||
"openid": true
|
||||
"local": true
|
||||
},
|
||||
"allowRegister": true,
|
||||
"branding": {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"id": "mockUser",
|
||||
"username": "mockUser",
|
||||
"photo": "/mock-backend/public/img/avatar.png",
|
||||
"name": "Test",
|
||||
"status": "ok",
|
||||
"provider": "internal"
|
||||
"displayName": "Test",
|
||||
"email": "mock@hedgedoc.dev"
|
||||
}
|
||||
|
|
|
@ -3,62 +3,31 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { RegisterError } from '../../components/register-page/register-page'
|
||||
import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
|
||||
|
||||
export const INTERACTIVE_LOGIN_METHODS = ['internal', 'ldap', 'openid']
|
||||
export const INTERACTIVE_LOGIN_METHODS = ['local', 'ldap']
|
||||
|
||||
export const doInternalLogin = async (username: string, password: string): Promise<void> => {
|
||||
const response = await fetch(getApiUrl() + 'auth/internal', {
|
||||
export enum AuthError {
|
||||
INVALID_CREDENTIALS = 'invalidCredentials',
|
||||
LOGIN_DISABLED = 'loginDisabled',
|
||||
OPENID_ERROR = 'openIdError',
|
||||
OTHER = 'other'
|
||||
}
|
||||
|
||||
export enum RegisterError {
|
||||
USERNAME_EXISTING = 'usernameExisting',
|
||||
REGISTRATION_DISABLED = 'registrationDisabled',
|
||||
OTHER = 'other'
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests to logout the current user.
|
||||
* @throws Error if logout is not possible.
|
||||
*/
|
||||
export const doLogout = async (): Promise<void> => {
|
||||
const response = await fetch(getApiUrl() + 'auth/logout', {
|
||||
...defaultFetchConfig,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password
|
||||
})
|
||||
})
|
||||
|
||||
expectResponseCode(response)
|
||||
}
|
||||
|
||||
export const doInternalRegister = async (username: string, password: string): Promise<void> => {
|
||||
const response = await fetch(getApiUrl() + 'auth/register', {
|
||||
...defaultFetchConfig,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password
|
||||
})
|
||||
})
|
||||
|
||||
if (response.status === 409) {
|
||||
throw new Error(RegisterError.USERNAME_EXISTING)
|
||||
}
|
||||
|
||||
expectResponseCode(response)
|
||||
}
|
||||
|
||||
export const doLdapLogin = async (username: string, password: string): Promise<void> => {
|
||||
const response = await fetch(getApiUrl() + 'auth/ldap', {
|
||||
...defaultFetchConfig,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password
|
||||
})
|
||||
})
|
||||
|
||||
expectResponseCode(response)
|
||||
}
|
||||
|
||||
export const doOpenIdLogin = async (openId: string): Promise<void> => {
|
||||
const response = await fetch(getApiUrl() + 'auth/openid', {
|
||||
...defaultFetchConfig,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
openId: openId
|
||||
})
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
expectResponseCode(response)
|
||||
|
|
25
src/api/auth/ldap.ts
Normal file
25
src/api/auth/ldap.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
|
||||
|
||||
/**
|
||||
* Requests to login a user via LDAP credentials.
|
||||
* @param username The username with which to try the login.
|
||||
* @param password The password of the user.
|
||||
*/
|
||||
export const doLdapLogin = async (username: string, password: string): Promise<void> => {
|
||||
const response = await fetch(getApiUrl() + 'auth/ldap', {
|
||||
...defaultFetchConfig,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password
|
||||
})
|
||||
})
|
||||
|
||||
expectResponseCode(response)
|
||||
}
|
94
src/api/auth/local.ts
Normal file
94
src/api/auth/local.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
|
||||
import { AuthError, RegisterError } from './index'
|
||||
|
||||
/**
|
||||
* Requests to do a local login with a provided username and password.
|
||||
* @param username The username for which the login should be tried.
|
||||
* @param password The password which should be used to login.
|
||||
* @throws {AuthError.INVALID_CREDENTIALS} when the username or password is wrong.
|
||||
* @throws {AuthError.LOGIN_DISABLED} when the local login is disabled on the backend.
|
||||
*/
|
||||
export const doLocalLogin = async (username: string, password: string): Promise<void> => {
|
||||
const response = await fetch(getApiUrl() + 'auth/local/login', {
|
||||
...defaultFetchConfig,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password
|
||||
})
|
||||
})
|
||||
|
||||
if (response.status === 400) {
|
||||
throw new Error(AuthError.LOGIN_DISABLED)
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new Error(AuthError.INVALID_CREDENTIALS)
|
||||
}
|
||||
|
||||
expectResponseCode(response, 201)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests to register a new local user in the backend.
|
||||
* @param username The username of the new user.
|
||||
* @param displayName The display name of the new user.
|
||||
* @param password The password of the new user.
|
||||
* @throws {RegisterError.USERNAME_EXISTING} when there is already an existing user with the same user name.
|
||||
* @throws {RegisterError.REGISTRATION_DISABLED} when the registration of local users has been disabled on the backend.
|
||||
*/
|
||||
export const doLocalRegister = async (username: string, displayName: string, password: string): Promise<void> => {
|
||||
const response = await fetch(getApiUrl() + 'auth/local', {
|
||||
...defaultFetchConfig,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
displayName,
|
||||
password
|
||||
})
|
||||
})
|
||||
|
||||
if (response.status === 409) {
|
||||
throw new Error(RegisterError.USERNAME_EXISTING)
|
||||
}
|
||||
|
||||
if (response.status === 400) {
|
||||
throw new Error(RegisterError.REGISTRATION_DISABLED)
|
||||
}
|
||||
|
||||
expectResponseCode(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests to update the user's current password to a new one.
|
||||
* @param currentPassword The current password of the user for confirmation.
|
||||
* @param newPassword The new password of the user.
|
||||
* @throws {AuthError.INVALID_CREDENTIALS} when the current password is wrong.
|
||||
* @throws {AuthError.LOGIN_DISABLED} when local login is disabled on the backend.
|
||||
*/
|
||||
export const doLocalPasswordChange = async (currentPassword: string, newPassword: string): Promise<void> => {
|
||||
const response = await fetch(getApiUrl() + 'auth/local', {
|
||||
...defaultFetchConfig,
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
currentPassword,
|
||||
newPassword
|
||||
})
|
||||
})
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new Error(AuthError.INVALID_CREDENTIALS)
|
||||
}
|
||||
|
||||
if (response.status === 400) {
|
||||
throw new Error(AuthError.LOGIN_DISABLED)
|
||||
}
|
||||
|
||||
expectResponseCode(response)
|
||||
}
|
3
src/api/config/types.d.ts
vendored
3
src/api/config/types.d.ts
vendored
|
@ -46,8 +46,7 @@ export interface AuthProvidersState {
|
|||
google: boolean
|
||||
saml: boolean
|
||||
oauth2: boolean
|
||||
internal: boolean
|
||||
openid: boolean
|
||||
local: boolean
|
||||
}
|
||||
|
||||
export interface CustomAuthNames {
|
||||
|
|
|
@ -4,16 +4,21 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { UserResponse } from '../users/types'
|
||||
import type { UserInfoDto } from '../users/types'
|
||||
import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
|
||||
import { isMockMode } from '../../utils/test-modes'
|
||||
|
||||
export const getMe = async (): Promise<UserResponse> => {
|
||||
/**
|
||||
* Returns metadata about the currently signed-in user from the API.
|
||||
* @throws Error when the user is not signed-in.
|
||||
* @return The user metadata.
|
||||
*/
|
||||
export const getMe = async (): Promise<UserInfoDto> => {
|
||||
const response = await fetch(getApiUrl() + `me${isMockMode() ? '-get' : ''}`, {
|
||||
...defaultFetchConfig
|
||||
})
|
||||
expectResponseCode(response)
|
||||
return (await response.json()) as UserResponse
|
||||
return (await response.json()) as UserInfoDto
|
||||
}
|
||||
|
||||
export const updateDisplayName = async (displayName: string): Promise<void> => {
|
||||
|
@ -28,19 +33,6 @@ export const updateDisplayName = async (displayName: string): Promise<void> => {
|
|||
expectResponseCode(response)
|
||||
}
|
||||
|
||||
export const changePassword = async (oldPassword: string, newPassword: string): Promise<void> => {
|
||||
const response = await fetch(getApiUrl() + 'me/password', {
|
||||
...defaultFetchConfig,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
oldPassword,
|
||||
newPassword
|
||||
})
|
||||
})
|
||||
|
||||
expectResponseCode(response)
|
||||
}
|
||||
|
||||
export const deleteUser = async (): Promise<void> => {
|
||||
const response = await fetch(getApiUrl() + 'me', {
|
||||
...defaultFetchConfig,
|
||||
|
|
2
src/api/users/types.d.ts
vendored
2
src/api/users/types.d.ts
vendored
|
@ -14,7 +14,7 @@ export interface UserResponse {
|
|||
}
|
||||
|
||||
export interface UserInfoDto {
|
||||
userName: string
|
||||
username: string
|
||||
displayName: string
|
||||
photo: string
|
||||
email: string
|
||||
|
|
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>
|
||||
)
|
||||
}
|
||||
|
|
22
src/hooks/common/use-on-input-change.ts
Normal file
22
src/hooks/common/use-on-input-change.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
/**
|
||||
* Takes an input change event and sends the event value to a state setter.
|
||||
* @param setter The setter method for the state.
|
||||
* @return Hook that can be used as callback for onChange.
|
||||
*/
|
||||
export const useOnInputChange = (setter: (value: string) => void): ((event: ChangeEvent<HTMLInputElement>) => void) => {
|
||||
return useCallback(
|
||||
(event) => {
|
||||
setter(event.target.value)
|
||||
},
|
||||
[setter]
|
||||
)
|
||||
}
|
|
@ -22,8 +22,7 @@ export const initialState: Config = {
|
|||
google: false,
|
||||
saml: false,
|
||||
oauth2: false,
|
||||
internal: false,
|
||||
openid: false
|
||||
local: false
|
||||
},
|
||||
branding: {
|
||||
name: '',
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import { DateTime } from 'luxon'
|
||||
import type { NoteDetails } from './types/note-details'
|
||||
import type { SlideOptions } from './types/slide-show-options'
|
||||
import { NoteTextDirection, NoteType } from './types/note-details'
|
||||
import type { SlideOptions } from './types/slide-show-options'
|
||||
|
||||
export const initialSlideOptions: SlideOptions = {
|
||||
transition: 'zoom',
|
||||
|
@ -30,7 +30,7 @@ export const initialState: NoteDetails = {
|
|||
createTime: DateTime.fromSeconds(0),
|
||||
lastChange: {
|
||||
timestamp: DateTime.fromSeconds(0),
|
||||
userName: ''
|
||||
username: ''
|
||||
},
|
||||
alias: '',
|
||||
viewCount: 0,
|
||||
|
|
|
@ -182,7 +182,7 @@ const convertNoteDtoToNoteDetails = (note: NoteDto): NoteDetails => {
|
|||
noteTitle: initialState.noteTitle,
|
||||
createTime: DateTime.fromISO(note.metadata.createTime),
|
||||
lastChange: {
|
||||
userName: note.metadata.updateUser.userName,
|
||||
username: note.metadata.updateUser.username,
|
||||
timestamp: DateTime.fromISO(note.metadata.updateTime)
|
||||
},
|
||||
firstHeading: initialState.firstHeading,
|
||||
|
|
|
@ -19,7 +19,7 @@ export interface NoteDetails {
|
|||
id: string
|
||||
createTime: DateTime
|
||||
lastChange: {
|
||||
userName: string
|
||||
username: string
|
||||
timestamp: DateTime
|
||||
}
|
||||
viewCount: number
|
||||
|
|
|
@ -23,8 +23,9 @@ export interface ClearUserAction extends Action<UserActionType> {
|
|||
}
|
||||
|
||||
export interface UserState {
|
||||
id: string
|
||||
name: string
|
||||
username: string
|
||||
displayName: string
|
||||
email: string
|
||||
photo: string
|
||||
provider: LoginProvider
|
||||
}
|
||||
|
@ -38,9 +39,8 @@ export enum LoginProvider {
|
|||
GOOGLE = 'google',
|
||||
SAML = 'saml',
|
||||
OAUTH2 = 'oauth2',
|
||||
INTERNAL = 'internal',
|
||||
LDAP = 'ldap',
|
||||
OPENID = 'openid'
|
||||
LOCAL = 'local',
|
||||
LDAP = 'ldap'
|
||||
}
|
||||
|
||||
export type OptionalUserState = UserState | null
|
||||
|
|
Loading…
Reference in a new issue