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,
|
google: false,
|
||||||
saml: false,
|
saml: false,
|
||||||
oauth2: false,
|
oauth2: false,
|
||||||
internal: false,
|
local: false
|
||||||
openid: false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const initLoggedOutTestWithCustomAuthProviders = (
|
const initLoggedOutTestWithCustomAuthProviders = (
|
||||||
|
@ -50,7 +49,7 @@ describe('When logged-out ', () => {
|
||||||
describe('and an interactive auth-provider is enabled, ', () => {
|
describe('and an interactive auth-provider is enabled, ', () => {
|
||||||
it('sign-in button points to login route: internal', () => {
|
it('sign-in button points to login route: internal', () => {
|
||||||
initLoggedOutTestWithCustomAuthProviders(cy, {
|
initLoggedOutTestWithCustomAuthProviders(cy, {
|
||||||
internal: true
|
local: true
|
||||||
})
|
})
|
||||||
cy.getById('sign-in-button').should('be.visible').should('have.attr', 'href', '/login')
|
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')
|
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, ', () => {
|
describe('and only one one-click auth-provider is enabled, ', () => {
|
||||||
|
@ -78,7 +70,7 @@ describe('When logged-out ', () => {
|
||||||
cy.getById('sign-in-button')
|
cy.getById('sign-in-button')
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
// The absolute URL is used because it is defined as API base URL absolute.
|
// 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', () => {
|
it('sign-in button points to login route', () => {
|
||||||
initLoggedOutTestWithCustomAuthProviders(cy, {
|
initLoggedOutTestWithCustomAuthProviders(cy, {
|
||||||
saml: true,
|
saml: true,
|
||||||
internal: true
|
local: true
|
||||||
})
|
})
|
||||||
cy.getById('sign-in-button').should('be.visible').should('have.attr', 'href', '/login')
|
cy.getById('sign-in-button').should('be.visible').should('have.attr', 'href', '/login')
|
||||||
})
|
})
|
||||||
|
|
|
@ -25,8 +25,7 @@ export const authProviders = {
|
||||||
google: true,
|
google: true,
|
||||||
saml: true,
|
saml: true,
|
||||||
oauth2: true,
|
oauth2: true,
|
||||||
internal: true,
|
local: true
|
||||||
openid: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|
|
@ -494,13 +494,16 @@
|
||||||
"signInVia": "Sign in via {{service}}",
|
"signInVia": "Sign in via {{service}}",
|
||||||
"signIn": "Sign In",
|
"signIn": "Sign In",
|
||||||
"signOut": "Sign Out",
|
"signOut": "Sign Out",
|
||||||
|
"logoutFailed": "There was an error logging you out.\nClose your browser window to ensure session data is removed.",
|
||||||
"auth": {
|
"auth": {
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"error": {
|
"error": {
|
||||||
"openIdLogin": "Invalid OpenID provided",
|
"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": {
|
"register": {
|
||||||
|
@ -510,6 +513,7 @@
|
||||||
"passwordInfo": "Choose a unique and secure password. It must contain at least 8 characters.",
|
"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:",
|
"infoTermsPrivacy": "With the registration of my user account I agree to the following terms:",
|
||||||
"error": {
|
"error": {
|
||||||
|
"registrationDisabled": "The registration is disabled",
|
||||||
"usernameExisting": "There is already an account with this username.",
|
"usernameExisting": "There is already an account with this username.",
|
||||||
"other": "There was an error while registering your account. Just try it again."
|
"other": "There was an error while registering your account. Just try it again."
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,7 @@
|
||||||
"google": true,
|
"google": true,
|
||||||
"saml": true,
|
"saml": true,
|
||||||
"oauth2": true,
|
"oauth2": true,
|
||||||
"internal": true,
|
"local": true
|
||||||
"openid": true
|
|
||||||
},
|
},
|
||||||
"allowRegister": true,
|
"allowRegister": true,
|
||||||
"branding": {
|
"branding": {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
"id": "mockUser",
|
"username": "mockUser",
|
||||||
"photo": "/mock-backend/public/img/avatar.png",
|
"photo": "/mock-backend/public/img/avatar.png",
|
||||||
"name": "Test",
|
"displayName": "Test",
|
||||||
"status": "ok",
|
"email": "mock@hedgedoc.dev"
|
||||||
"provider": "internal"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,62 +3,31 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { RegisterError } from '../../components/register-page/register-page'
|
|
||||||
import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
|
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> => {
|
export enum AuthError {
|
||||||
const response = await fetch(getApiUrl() + 'auth/internal', {
|
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,
|
...defaultFetchConfig,
|
||||||
method: 'POST',
|
method: 'DELETE'
|
||||||
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
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expectResponseCode(response)
|
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
|
google: boolean
|
||||||
saml: boolean
|
saml: boolean
|
||||||
oauth2: boolean
|
oauth2: boolean
|
||||||
internal: boolean
|
local: boolean
|
||||||
openid: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomAuthNames {
|
export interface CustomAuthNames {
|
||||||
|
|
|
@ -4,16 +4,21 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
|
||||||
import { isMockMode } from '../../utils/test-modes'
|
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' : ''}`, {
|
const response = await fetch(getApiUrl() + `me${isMockMode() ? '-get' : ''}`, {
|
||||||
...defaultFetchConfig
|
...defaultFetchConfig
|
||||||
})
|
})
|
||||||
expectResponseCode(response)
|
expectResponseCode(response)
|
||||||
return (await response.json()) as UserResponse
|
return (await response.json()) as UserInfoDto
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateDisplayName = async (displayName: string): Promise<void> => {
|
export const updateDisplayName = async (displayName: string): Promise<void> => {
|
||||||
|
@ -28,19 +33,6 @@ export const updateDisplayName = async (displayName: string): Promise<void> => {
|
||||||
expectResponseCode(response)
|
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> => {
|
export const deleteUser = async (): Promise<void> => {
|
||||||
const response = await fetch(getApiUrl() + 'me', {
|
const response = await fetch(getApiUrl() + 'me', {
|
||||||
...defaultFetchConfig,
|
...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 {
|
export interface UserInfoDto {
|
||||||
userName: string
|
username: string
|
||||||
displayName: string
|
displayName: string
|
||||||
photo: string
|
photo: string
|
||||||
email: 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 (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<DocumentInfobar
|
<DocumentInfobar
|
||||||
changedAuthor={noteDetails.lastChange.userName ?? ''}
|
changedAuthor={noteDetails.lastChange.username ?? ''}
|
||||||
changedTime={noteDetails.lastChange.timestamp}
|
changedTime={noteDetails.lastChange.timestamp}
|
||||||
createdAuthor={'Test'}
|
createdAuthor={'Test'}
|
||||||
createdTime={noteDetails.createTime}
|
createdTime={noteDetails.createTime}
|
||||||
|
|
|
@ -28,7 +28,7 @@ const allSupportedLinks = [
|
||||||
|
|
||||||
const getUserName = (): string => {
|
const getUserName = (): string => {
|
||||||
const user = store.getState().user
|
const user = store.getState().user
|
||||||
return user ? user.name : 'Anonymous'
|
return user ? user.displayName : 'Anonymous'
|
||||||
}
|
}
|
||||||
|
|
||||||
const linkAndExtraTagHint = (editor: Editor): Promise<Hints | null> => {
|
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 { Trans, useTranslation } from 'react-i18next'
|
||||||
import { LinkContainer } from 'react-router-bootstrap'
|
import { LinkContainer } from 'react-router-bootstrap'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { getApiUrl } from '../../../api/utils'
|
|
||||||
import { INTERACTIVE_LOGIN_METHODS } from '../../../api/auth'
|
import { INTERACTIVE_LOGIN_METHODS } from '../../../api/auth'
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
import { cypressId } from '../../../utils/cypress-attribute'
|
import { cypressId } from '../../../utils/cypress-attribute'
|
||||||
|
import { useBackendBaseUrl } from '../../../hooks/common/use-backend-base-url'
|
||||||
|
|
||||||
export type SignInButtonProps = Omit<ButtonProps, 'href'>
|
export type SignInButtonProps = Omit<ButtonProps, 'href'>
|
||||||
|
|
||||||
export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props }) => {
|
export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const backendBaseUrl = useBackendBaseUrl()
|
||||||
const authProviders = useApplicationState((state) => state.config.authProviders)
|
const authProviders = useApplicationState((state) => state.config.authProviders)
|
||||||
const authEnabled = useMemo(() => Object.values(authProviders).includes(true), [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))
|
const activeOneClickProviders = activeProviders.filter((entry) => !INTERACTIVE_LOGIN_METHODS.includes(entry))
|
||||||
|
|
||||||
if (activeProviders.length === 1 && activeOneClickProviders.length === 1) {
|
if (activeProviders.length === 1 && activeOneClickProviders.length === 1) {
|
||||||
return `${getApiUrl()}auth/${activeOneClickProviders[0]}`
|
return `${backendBaseUrl}auth/${activeOneClickProviders[0]}`
|
||||||
}
|
}
|
||||||
return '/login'
|
return '/login'
|
||||||
}, [authProviders])
|
}, [authProviders, backendBaseUrl])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShowIf condition={authEnabled}>
|
<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 { Dropdown } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { LinkContainer } from 'react-router-bootstrap'
|
import { LinkContainer } from 'react-router-bootstrap'
|
||||||
import { clearUser } from '../../../redux/user/methods'
|
|
||||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||||
import { UserAvatar } from '../../common/user-avatar/user-avatar'
|
import { UserAvatar } from '../../common/user-avatar/user-avatar'
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
import { cypressId } from '../../../utils/cypress-attribute'
|
import { cypressId } from '../../../utils/cypress-attribute'
|
||||||
|
import { SignOutDropdownButton } from './sign-out-dropdown-button'
|
||||||
|
|
||||||
export const UserDropdown: React.FC = () => {
|
export const UserDropdown: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
@ -25,7 +25,7 @@ export const UserDropdown: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<Dropdown alignRight>
|
<Dropdown alignRight>
|
||||||
<Dropdown.Toggle size='sm' variant='dark' {...cypressId('user-dropdown')} className={'d-flex align-items-center'}>
|
<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.Toggle>
|
||||||
|
|
||||||
<Dropdown.Menu className='text-start'>
|
<Dropdown.Menu className='text-start'>
|
||||||
|
@ -41,15 +41,7 @@ export const UserDropdown: React.FC = () => {
|
||||||
<Trans i18nKey='profile.userProfile' />
|
<Trans i18nKey='profile.userProfile' />
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
<Dropdown.Item
|
<SignOutDropdownButton />
|
||||||
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>
|
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
</Dropdown>
|
</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 { getMe } from '../../../api/me'
|
||||||
import { setUser } from '../../../redux/user/methods'
|
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 () => {
|
export const fetchAndSetUser: () => Promise<void> = async () => {
|
||||||
|
try {
|
||||||
const me = await getMe()
|
const me = await getMe()
|
||||||
setUser({
|
setUser({
|
||||||
id: me.id,
|
username: me.username,
|
||||||
name: me.name,
|
displayName: me.displayName,
|
||||||
photo: me.photo,
|
photo: me.photo,
|
||||||
provider: me.provider
|
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 type { FormEvent } from 'react'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useMemo, 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 { Trans, useTranslation } from 'react-i18next'
|
||||||
import { doLdapLogin } from '../../../api/auth'
|
import { doLdapLogin } from '../../../api/auth/ldap'
|
||||||
import { fetchAndSetUser } from './utils'
|
import { fetchAndSetUser } from './utils'
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
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 = () => {
|
export const ViaLdap: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
useTranslation()
|
||||||
const ldapCustomName = useApplicationState((state) => state.config.customAuthNames.ldap)
|
const ldapCustomName = useApplicationState((state) => state.config.customAuthNames.ldap)
|
||||||
|
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = 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(
|
const onLoginSubmit = useCallback(
|
||||||
(event: FormEvent) => {
|
(event: FormEvent) => {
|
||||||
doLdapLogin(username, password)
|
doLdapLogin(username, password)
|
||||||
.then(() => fetchAndSetUser())
|
.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()
|
event.preventDefault()
|
||||||
},
|
},
|
||||||
[username, password]
|
[username, password]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onUsernameChange = useOnInputChange(setUsername)
|
||||||
|
const onPasswordChange = useOnInputChange(setPassword)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='bg-dark mb-4'>
|
<Card className='bg-dark mb-4'>
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
|
@ -40,33 +59,9 @@ export const ViaLdap: React.FC = () => {
|
||||||
<Trans i18nKey='login.signInVia' values={{ service: name }} />
|
<Trans i18nKey='login.signInVia' values={{ service: name }} />
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
<Form onSubmit={onLoginSubmit}>
|
<Form onSubmit={onLoginSubmit}>
|
||||||
<Form.Group controlId='ldap-username'>
|
<UsernameField onChange={onUsernameChange} invalid={!!error} />
|
||||||
<Form.Control
|
<PasswordField onChange={onPasswordChange} invalid={!!error} />
|
||||||
isInvalid={error}
|
<AuthError error={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>
|
|
||||||
|
|
||||||
<Button type='submit' variant='primary'>
|
<Button type='submit' variant='primary'>
|
||||||
<Trans i18nKey='login.signIn' />
|
<Trans i18nKey='login.signIn' />
|
||||||
|
|
|
@ -4,33 +4,54 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 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 { Trans, useTranslation } from 'react-i18next'
|
||||||
import { Link } from 'react-router-dom'
|
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 { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { fetchAndSetUser } from './utils'
|
import { fetchAndSetUser } from './utils'
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
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 { t } = useTranslation()
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState<AuthErrorType>()
|
||||||
const allowRegister = useApplicationState((state) => state.config.allowRegister)
|
const allowRegister = useApplicationState((state) => state.config.allowRegister)
|
||||||
|
|
||||||
const onLoginSubmit = useCallback(
|
const onLoginSubmit = useCallback(
|
||||||
(event: FormEvent) => {
|
(event: FormEvent) => {
|
||||||
doInternalLogin(username, password)
|
doLocalLogin(username, password)
|
||||||
.then(() => fetchAndSetUser())
|
.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()
|
event.preventDefault()
|
||||||
},
|
},
|
||||||
[username, password]
|
[username, password]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onUsernameChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setUsername(event.target.value)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onPasswordChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setPassword(event.target.value)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='bg-dark mb-4'>
|
<Card className='bg-dark mb-4'>
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
|
@ -38,33 +59,9 @@ export const ViaInternal: React.FC = () => {
|
||||||
<Trans i18nKey='login.signInVia' values={{ service: t('login.auth.username') }} />
|
<Trans i18nKey='login.signInVia' values={{ service: t('login.auth.username') }} />
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
<Form onSubmit={onLoginSubmit}>
|
<Form onSubmit={onLoginSubmit}>
|
||||||
<Form.Group controlId='internal-username'>
|
<UsernameField onChange={onUsernameChange} invalid={!!error} />
|
||||||
<Form.Control
|
<PasswordField onChange={onPasswordChange} invalid={!!error} />
|
||||||
isInvalid={error}
|
<AuthError error={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>
|
|
||||||
|
|
||||||
<div className='flex flex-row' dir='auto'>
|
<div className='flex flex-row' dir='auto'>
|
||||||
<Button type='submit' variant='primary' className='mx-2'>
|
<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
|
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 { Card, Col, Row } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { Redirect } from 'react-router'
|
import { Redirect } from 'react-router'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
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 { ViaLdap } from './auth/via-ldap'
|
||||||
import { OneClickType, ViaOneClick } from './auth/via-one-click'
|
import { OneClickType, ViaOneClick } from './auth/via-one-click'
|
||||||
import { ViaOpenId } from './auth/via-openid'
|
|
||||||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
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 = () => {
|
export const LoginPage: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const authProviders = useApplicationState((state) => state.config.authProviders)
|
const authProviders = useApplicationState((state) => state.config.authProviders)
|
||||||
|
@ -33,7 +36,8 @@ export const LoginPage: React.FC = () => {
|
||||||
authProviders.twitter
|
authProviders.twitter
|
||||||
]
|
]
|
||||||
|
|
||||||
const oneClickCustomName: (type: OneClickType) => string | undefined = (type) => {
|
const oneClickCustomName = useCallback(
|
||||||
|
(type: OneClickType): string | undefined => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case OneClickType.SAML:
|
case OneClickType.SAML:
|
||||||
return customSamlAuthName
|
return customSamlAuthName
|
||||||
|
@ -42,7 +46,19 @@ export const LoginPage: React.FC = () => {
|
||||||
default:
|
default:
|
||||||
return undefined
|
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) {
|
if (userLoggedIn) {
|
||||||
// TODO Redirect to previous page?
|
// TODO Redirect to previous page?
|
||||||
|
@ -53,17 +69,14 @@ export const LoginPage: React.FC = () => {
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className='my-3'>
|
<div className='my-3'>
|
||||||
<Row className='h-100 flex justify-content-center'>
|
<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}>
|
<Col xs={12} sm={10} lg={4}>
|
||||||
<ShowIf condition={authProviders.internal}>
|
<ShowIf condition={authProviders.local}>
|
||||||
<ViaInternal />
|
<ViaLocal />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<ShowIf condition={authProviders.ldap}>
|
<ShowIf condition={authProviders.ldap}>
|
||||||
<ViaLdap />
|
<ViaLdap />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<ShowIf condition={authProviders.openid}>
|
|
||||||
<ViaOpenId />
|
|
||||||
</ShowIf>
|
|
||||||
</Col>
|
</Col>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<ShowIf condition={oneClickProviders.includes(true)}>
|
<ShowIf condition={oneClickProviders.includes(true)}>
|
||||||
|
@ -73,13 +86,7 @@ export const LoginPage: React.FC = () => {
|
||||||
<Card.Title>
|
<Card.Title>
|
||||||
<Trans i18nKey='login.signInVia' values={{ service: '' }} />
|
<Trans i18nKey='login.signInVia' values={{ service: '' }} />
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
{Object.values(OneClickType)
|
{oneClickButtonsDom}
|
||||||
.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>
|
|
||||||
))}
|
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -32,7 +32,7 @@ export const ProfilePage: React.FC = () => {
|
||||||
<Row className='h-100 flex justify-content-center'>
|
<Row className='h-100 flex justify-content-center'>
|
||||||
<Col lg={6}>
|
<Col lg={6}>
|
||||||
<ProfileDisplayName />
|
<ProfileDisplayName />
|
||||||
<ShowIf condition={userProvider === LoginProvider.INTERNAL}>
|
<ShowIf condition={userProvider === LoginProvider.LOCAL}>
|
||||||
<ProfileChangePassword />
|
<ProfileChangePassword />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<ProfileAccessTokens />
|
<ProfileAccessTokens />
|
||||||
|
|
|
@ -4,14 +4,16 @@
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
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 React, { useCallback, useMemo, useState } from 'react'
|
||||||
import { Button, Card, Form } from 'react-bootstrap'
|
import { Button, Card, Form } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { changePassword } from '../../../api/me'
|
import { doLocalPasswordChange } from '../../../api/auth/local'
|
||||||
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
||||||
|
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
||||||
const REGEX_VALID_PASSWORD = /^[^\s].{5,}$/
|
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.
|
* 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 [newPassword, setNewPassword] = useState('')
|
||||||
const [newPasswordAgain, setNewPasswordAgain] = useState('')
|
const [newPasswordAgain, setNewPasswordAgain] = useState('')
|
||||||
|
|
||||||
const newPasswordValid = useMemo(() => {
|
const onChangeOldPassword = useOnInputChange(setOldPassword)
|
||||||
return REGEX_VALID_PASSWORD.test(newPassword)
|
const onChangeNewPassword = useOnInputChange(setNewPassword)
|
||||||
}, [newPassword])
|
const onChangeNewPasswordAgain = useOnInputChange(setNewPasswordAgain)
|
||||||
|
|
||||||
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 onSubmitPasswordChange = useCallback(
|
const onSubmitPasswordChange = useCallback(
|
||||||
(event: FormEvent) => {
|
(event: FormEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
changePassword(oldPassword, newPassword).catch(showErrorNotification('profile.changePassword.failed'))
|
doLocalPasswordChange(oldPassword, newPassword).catch(showErrorNotification('profile.changePassword.failed'))
|
||||||
},
|
},
|
||||||
[oldPassword, newPassword]
|
[oldPassword, newPassword]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ready = useMemo(() => {
|
||||||
|
return (
|
||||||
|
oldPassword.trim() !== '' &&
|
||||||
|
newPassword.trim() !== '' &&
|
||||||
|
newPasswordAgain.trim() !== '' &&
|
||||||
|
newPassword === newPasswordAgain
|
||||||
|
)
|
||||||
|
}, [oldPassword, newPassword, newPasswordAgain])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='bg-dark mb-4'>
|
<Card className='bg-dark mb-4'>
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
|
@ -57,53 +52,11 @@ export const ProfileChangePassword: React.FC = () => {
|
||||||
<Trans i18nKey='profile.changePassword.title' />
|
<Trans i18nKey='profile.changePassword.title' />
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
<Form onSubmit={onSubmitPasswordChange} className='text-left'>
|
<Form onSubmit={onSubmitPasswordChange} className='text-left'>
|
||||||
<Form.Group controlId='oldPassword'>
|
<CurrentPasswordField onChange={onChangeOldPassword} value={oldPassword} />
|
||||||
<Form.Label>
|
<NewPasswordField onChange={onChangeNewPassword} value={newPassword} />
|
||||||
<Trans i18nKey='profile.changePassword.old' />
|
<PasswordAgainField password={newPassword} onChange={onChangeNewPasswordAgain} value={newPasswordAgain} />
|
||||||
</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>
|
|
||||||
|
|
||||||
<Button type='submit' variant='primary' disabled={!newPasswordValid || !newPasswordAgainValid}>
|
<Button type='submit' variant='primary' disabled={!ready}>
|
||||||
<Trans i18nKey='common.save' />
|
<Trans i18nKey='common.save' />
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
@ -4,33 +4,26 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ChangeEvent, FormEvent } from 'react'
|
import type { FormEvent } from 'react'
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import { Button, Card, Form } from 'react-bootstrap'
|
import { Button, Card, Form } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { updateDisplayName } from '../../../api/me'
|
import { updateDisplayName } from '../../../api/me'
|
||||||
import { fetchAndSetUser } from '../../login-page/auth/utils'
|
import { fetchAndSetUser } from '../../login-page/auth/utils'
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
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.
|
* Profile page section for changing the current display name.
|
||||||
*/
|
*/
|
||||||
export const ProfileDisplayName: React.FC = () => {
|
export const ProfileDisplayName: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
useTranslation()
|
||||||
const userName = useApplicationState((state) => state.user?.name)
|
const userName = useApplicationState((state) => state.user?.displayName)
|
||||||
const [displayName, setDisplayName] = useState('')
|
const [displayName, setDisplayName] = useState(userName ?? '')
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (userName !== undefined) {
|
|
||||||
setDisplayName(userName)
|
|
||||||
}
|
|
||||||
}, [userName])
|
|
||||||
|
|
||||||
const onChangeDisplayName = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setDisplayName(event.target.value)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
|
const onChangeDisplayName = useOnInputChange(setDisplayName)
|
||||||
const onSubmitNameChange = useCallback(
|
const onSubmitNameChange = useCallback(
|
||||||
(event: FormEvent) => {
|
(event: FormEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
@ -42,8 +35,8 @@ export const ProfileDisplayName: React.FC = () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const formSubmittable = useMemo(() => {
|
const formSubmittable = useMemo(() => {
|
||||||
return displayName.trim() !== ''
|
return displayName.trim() !== '' && displayName !== userName
|
||||||
}, [displayName])
|
}, [displayName, userName])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='bg-dark mb-4'>
|
<Card className='bg-dark mb-4'>
|
||||||
|
@ -52,24 +45,7 @@ export const ProfileDisplayName: React.FC = () => {
|
||||||
<Trans i18nKey='profile.userProfile' />
|
<Trans i18nKey='profile.userProfile' />
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
<Form onSubmit={onSubmitNameChange} className='text-left'>
|
<Form onSubmit={onSubmitNameChange} className='text-left'>
|
||||||
<Form.Group controlId='displayName'>
|
<DisplayNameField onChange={onChangeDisplayName} value={displayName} initialValue={userName} />
|
||||||
<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>
|
|
||||||
|
|
||||||
<Button type='submit' variant='primary' disabled={!formSubmittable}>
|
<Button type='submit' variant='primary' disabled={!formSubmittable}>
|
||||||
<Trans i18nKey='common.save' />
|
<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 type { FormEvent } from 'react'
|
||||||
import React, { Fragment, useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import { Alert, Button, Card, Col, Form, Row } from 'react-bootstrap'
|
import { Button, Card, Col, Form, Row } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { Redirect } from 'react-router'
|
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 { 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 { fetchAndSetUser } from '../login-page/auth/utils'
|
||||||
import { Logger } from '../../utils/logger'
|
import { RegisterError as RegisterErrorType } from '../../api/auth'
|
||||||
|
import { RegisterInfos } from './register-infos/register-infos'
|
||||||
const log = new Logger('RegisterPage')
|
import { UsernameField } from '../common/fields/username-field'
|
||||||
|
import { DisplayNameField } from '../common/fields/display-name-field'
|
||||||
export enum RegisterError {
|
import { NewPasswordField } from '../common/fields/new-password-field'
|
||||||
NONE = 'none',
|
import { PasswordAgainField } from '../common/fields/password-again-field'
|
||||||
USERNAME_EXISTING = 'usernameExisting',
|
import { useOnInputChange } from '../../hooks/common/use-on-input-change'
|
||||||
OTHER = 'other'
|
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 = () => {
|
export const RegisterPage: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
useTranslation()
|
||||||
const allowRegister = useApplicationState((state) => state.config.allowRegister)
|
const allowRegister = useApplicationState((state) => state.config.allowRegister)
|
||||||
const specialUrls = useApplicationState((state) => state.config.specialUrls)
|
|
||||||
const userExists = useApplicationState((state) => !!state.user)
|
const userExists = useApplicationState((state) => !!state.user)
|
||||||
|
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
|
const [displayName, setDisplayName] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [passwordAgain, setPasswordAgain] = useState('')
|
const [passwordAgain, setPasswordAgain] = useState('')
|
||||||
const [error, setError] = useState(RegisterError.NONE)
|
const [error, setError] = useState<RegisterErrorType>()
|
||||||
const [ready, setReady] = useState(false)
|
|
||||||
|
|
||||||
const doRegisterSubmit = useCallback(
|
const doRegisterSubmit = useCallback(
|
||||||
(event: FormEvent) => {
|
(event: FormEvent) => {
|
||||||
doInternalRegister(username, password)
|
doLocalRegister(username, displayName, password)
|
||||||
.then(() => fetchAndSetUser())
|
.then(() => fetchAndSetUser())
|
||||||
.catch((err: Error) => {
|
.catch((error: Error) => {
|
||||||
log.error(err)
|
setError(
|
||||||
setError(err.message === RegisterError.USERNAME_EXISTING ? err.message : RegisterError.OTHER)
|
Object.values(RegisterErrorType).includes(error.message as RegisterErrorType)
|
||||||
|
? (error.message as RegisterErrorType)
|
||||||
|
: RegisterErrorType.OTHER
|
||||||
|
)
|
||||||
})
|
})
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
},
|
},
|
||||||
[username, password]
|
[username, displayName, password]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
const ready = useMemo(() => {
|
||||||
setReady(username !== '' && password !== '' && password.length >= 8 && password === passwordAgain)
|
return (
|
||||||
}, [username, password, passwordAgain])
|
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) {
|
if (!allowRegister) {
|
||||||
return <Redirect to={'/login'} />
|
return <Redirect to={'/login'} />
|
||||||
|
@ -62,7 +75,6 @@ export const RegisterPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<div className='my-3'>
|
<div className='my-3'>
|
||||||
<h1 className='mb-4'>
|
<h1 className='mb-4'>
|
||||||
<Trans i18nKey='login.register.title' />
|
<Trans i18nKey='login.register.title' />
|
||||||
|
@ -72,92 +84,23 @@ export const RegisterPage: React.FC = () => {
|
||||||
<Card className='bg-dark mb-4 text-start'>
|
<Card className='bg-dark mb-4 text-start'>
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
<Form onSubmit={doRegisterSubmit}>
|
<Form onSubmit={doRegisterSubmit}>
|
||||||
<Form.Group controlId='username'>
|
<UsernameField onChange={onUsernameChange} value={username} />
|
||||||
<Form.Label>
|
<DisplayNameField onChange={onDisplayNameChange} value={displayName} />
|
||||||
<Trans i18nKey='login.auth.username' />
|
<NewPasswordField onChange={onPasswordChange} value={password} />
|
||||||
</Form.Label>
|
<PasswordAgainField password={password} onChange={onPasswordAgainChange} value={passwordAgain} />
|
||||||
<Form.Control
|
|
||||||
type='text'
|
<RegisterInfos />
|
||||||
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}>
|
<Button variant='primary' type='submit' block={true} disabled={!ready}>
|
||||||
<Trans i18nKey='login.register.title' />
|
<Trans i18nKey='login.register.title' />
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
<br />
|
|
||||||
<Alert show={error !== RegisterError.NONE} variant='danger'>
|
<RegisterError error={error} />
|
||||||
<Trans i18nKey={`login.register.error.${error}`} />
|
|
||||||
</Alert>
|
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
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,
|
google: false,
|
||||||
saml: false,
|
saml: false,
|
||||||
oauth2: false,
|
oauth2: false,
|
||||||
internal: false,
|
local: false
|
||||||
openid: false
|
|
||||||
},
|
},
|
||||||
branding: {
|
branding: {
|
||||||
name: '',
|
name: '',
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
|
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import type { NoteDetails } from './types/note-details'
|
import type { NoteDetails } from './types/note-details'
|
||||||
import type { SlideOptions } from './types/slide-show-options'
|
|
||||||
import { NoteTextDirection, NoteType } from './types/note-details'
|
import { NoteTextDirection, NoteType } from './types/note-details'
|
||||||
|
import type { SlideOptions } from './types/slide-show-options'
|
||||||
|
|
||||||
export const initialSlideOptions: SlideOptions = {
|
export const initialSlideOptions: SlideOptions = {
|
||||||
transition: 'zoom',
|
transition: 'zoom',
|
||||||
|
@ -30,7 +30,7 @@ export const initialState: NoteDetails = {
|
||||||
createTime: DateTime.fromSeconds(0),
|
createTime: DateTime.fromSeconds(0),
|
||||||
lastChange: {
|
lastChange: {
|
||||||
timestamp: DateTime.fromSeconds(0),
|
timestamp: DateTime.fromSeconds(0),
|
||||||
userName: ''
|
username: ''
|
||||||
},
|
},
|
||||||
alias: '',
|
alias: '',
|
||||||
viewCount: 0,
|
viewCount: 0,
|
||||||
|
|
|
@ -182,7 +182,7 @@ const convertNoteDtoToNoteDetails = (note: NoteDto): NoteDetails => {
|
||||||
noteTitle: initialState.noteTitle,
|
noteTitle: initialState.noteTitle,
|
||||||
createTime: DateTime.fromISO(note.metadata.createTime),
|
createTime: DateTime.fromISO(note.metadata.createTime),
|
||||||
lastChange: {
|
lastChange: {
|
||||||
userName: note.metadata.updateUser.userName,
|
username: note.metadata.updateUser.username,
|
||||||
timestamp: DateTime.fromISO(note.metadata.updateTime)
|
timestamp: DateTime.fromISO(note.metadata.updateTime)
|
||||||
},
|
},
|
||||||
firstHeading: initialState.firstHeading,
|
firstHeading: initialState.firstHeading,
|
||||||
|
|
|
@ -19,7 +19,7 @@ export interface NoteDetails {
|
||||||
id: string
|
id: string
|
||||||
createTime: DateTime
|
createTime: DateTime
|
||||||
lastChange: {
|
lastChange: {
|
||||||
userName: string
|
username: string
|
||||||
timestamp: DateTime
|
timestamp: DateTime
|
||||||
}
|
}
|
||||||
viewCount: number
|
viewCount: number
|
||||||
|
|
|
@ -23,8 +23,9 @@ export interface ClearUserAction extends Action<UserActionType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserState {
|
export interface UserState {
|
||||||
id: string
|
username: string
|
||||||
name: string
|
displayName: string
|
||||||
|
email: string
|
||||||
photo: string
|
photo: string
|
||||||
provider: LoginProvider
|
provider: LoginProvider
|
||||||
}
|
}
|
||||||
|
@ -38,9 +39,8 @@ export enum LoginProvider {
|
||||||
GOOGLE = 'google',
|
GOOGLE = 'google',
|
||||||
SAML = 'saml',
|
SAML = 'saml',
|
||||||
OAUTH2 = 'oauth2',
|
OAUTH2 = 'oauth2',
|
||||||
INTERNAL = 'internal',
|
LOCAL = 'local',
|
||||||
LDAP = 'ldap',
|
LDAP = 'ldap'
|
||||||
OPENID = 'openid'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OptionalUserState = UserState | null
|
export type OptionalUserState = UserState | null
|
||||||
|
|
Loading…
Reference in a new issue