fix(frontend): refactor api error handling

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-01-14 22:35:37 +01:00
parent e93144eb40
commit 57bfca7b15
44 changed files with 387 additions and 465 deletions

View file

@ -27,9 +27,6 @@ exports[`Note loading boundary shows an error 1`] = `
</span>
<span>
children:
<span>
This is a mock for CreateNonExistingNoteHint
</span>
</span>
</div>
`;

View file

@ -32,7 +32,7 @@ export const NoteLoadingBoundary: React.FC<PropsWithChildren> = ({ children }) =
}
return (
<CommonErrorPage titleI18nKey={`${error.message}.title`} descriptionI18nKey={`${error.message}.description`}>
<ShowIf condition={error.message === 'api.note.notFound'}>
<ShowIf condition={error.message === 'api.error.note.not_found'}>
<CreateNonExistingNoteHint onNoteCreated={loadNoteFromServer} />
</ShowIf>
</CommonErrorPage>

View file

@ -1,41 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AuthError as AuthErrorType } from '../../../../api/auth/types'
import React, { useMemo } from 'react'
import { Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
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 {@link 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>
)
}

View file

@ -4,15 +4,13 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { doLdapLogin } from '../../../api/auth/ldap'
import { AuthError as AuthErrorType } from '../../../api/auth/types'
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
import { AuthError } from './auth-error/auth-error'
import { PasswordField } from './fields/password-field'
import { UsernameField } from './fields/username-field'
import { fetchAndSetUser } from './utils'
import type { FormEvent } from 'react'
import React, { useCallback, useState } from 'react'
import { Button, Card, Form } from 'react-bootstrap'
import { Alert, Button, Card, Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
export interface ViaLdapProps {
@ -28,19 +26,13 @@ export const ViaLdap: React.FC<ViaLdapProps> = ({ providerName, identifier }) =>
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<AuthErrorType>()
const [error, setError] = useState<string>()
const onLoginSubmit = useCallback(
(event: FormEvent) => {
doLdapLogin(identifier, username, password)
.then(() => fetchAndSetUser())
.catch((error: Error) => {
setError(
Object.values(AuthErrorType).includes(error.message as AuthErrorType)
? (error.message as AuthErrorType)
: AuthErrorType.OTHER
)
})
.catch((error: Error) => setError(error.message))
event.preventDefault()
},
[username, password, identifier]
@ -58,7 +50,9 @@ export const ViaLdap: React.FC<ViaLdapProps> = ({ providerName, identifier }) =>
<Form onSubmit={onLoginSubmit}>
<UsernameField onChange={onUsernameChange} invalid={!!error} />
<PasswordField onChange={onPasswordChange} invalid={!!error} />
<AuthError error={error} />
<Alert className='small' show={!!error} variant='danger'>
<Trans i18nKey={error} />
</Alert>
<Button type='submit' variant='primary'>
<Trans i18nKey='login.signIn' />

View file

@ -4,18 +4,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { doLocalLogin } from '../../../api/auth/local'
import { AuthError as AuthErrorType } from '../../../api/auth/types'
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
import { ShowIf } from '../../common/show-if/show-if'
import { AuthError } from './auth-error/auth-error'
import { PasswordField } from './fields/password-field'
import { UsernameField } from './fields/username-field'
import { fetchAndSetUser } from './utils'
import Link from 'next/link'
import type { FormEvent } from 'react'
import React, { useCallback, useState } from 'react'
import { Button, Card, Form } from 'react-bootstrap'
import { Alert, Button, Card, Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
/**
@ -25,20 +23,14 @@ export const ViaLocal: React.FC = () => {
const { t } = useTranslation()
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<AuthErrorType>()
const [error, setError] = useState<string>()
const allowRegister = useApplicationState((state) => state.config.allowRegister)
const onLoginSubmit = useCallback(
(event: FormEvent) => {
doLocalLogin(username, password)
.then(() => fetchAndSetUser())
.catch((error: Error) => {
setError(
Object.values(AuthErrorType).includes(error.message as AuthErrorType)
? (error.message as AuthErrorType)
: AuthErrorType.OTHER
)
})
.catch((error: Error) => setError(error.message))
event.preventDefault()
},
[username, password]
@ -56,7 +48,9 @@ export const ViaLocal: React.FC = () => {
<Form onSubmit={onLoginSubmit}>
<UsernameField onChange={onUsernameChange} invalid={!!error} />
<PasswordField onChange={onPasswordChange} invalid={!!error} />
<AuthError error={error} />
<Alert className='small' show={!!error} variant='danger'>
<Trans i18nKey={error} />
</Alert>
<div className='flex flex-row' dir='auto'>
<Button type='submit' variant='primary' className='mx-2'>

View file

@ -4,22 +4,22 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { doLocalPasswordChange } from '../../../api/auth/local'
import { ErrorToI18nKeyMapper } from '../../../api/common/error-to-i18n-key-mapper'
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
import { CurrentPasswordField } from '../../common/fields/current-password-field'
import { NewPasswordField } from '../../common/fields/new-password-field'
import { PasswordAgainField } from '../../common/fields/password-again-field'
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
import type { FormEvent } from 'react'
import React, { useCallback, useMemo, useRef, useState } from 'react'
import { Button, Card, Form } from 'react-bootstrap'
import { Alert, Button, Card, Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useAsyncFn } from 'react-use'
/**
* Profile page section for changing the password when using internal login.
*/
export const ProfileChangePassword: React.FC = () => {
useTranslation()
const { showErrorNotification, dispatchUiNotification } = useUiNotifications()
const [oldPassword, setOldPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [newPasswordAgain, setNewPasswordAgain] = useState('')
@ -30,36 +30,44 @@ export const ProfileChangePassword: React.FC = () => {
const onChangeNewPassword = useOnInputChange(setNewPassword)
const onChangeNewPasswordAgain = useOnInputChange(setNewPasswordAgain)
const [{ error, loading, value: changeSucceeded }, doRequest] = useAsyncFn(async (): Promise<boolean> => {
try {
await doLocalPasswordChange(oldPassword, newPassword)
return true
} catch (error) {
const foundI18nKey = new ErrorToI18nKeyMapper(error as Error, 'login.auth.error')
.withHttpCode(401, 'invalidCredentials')
.withBackendErrorName('loginDisabled', 'loginDisabled')
.withBackendErrorName('passwordTooWeak', 'passwordTooWeak')
.orFallbackI18nKey('other')
return Promise.reject(foundI18nKey)
} finally {
if (formRef.current) {
formRef.current.reset()
}
setOldPassword('')
setNewPassword('')
setNewPasswordAgain('')
}
}, [oldPassword, newPassword])
const onSubmitPasswordChange = useCallback(
(event: FormEvent) => {
event.preventDefault()
doLocalPasswordChange(oldPassword, newPassword)
.then(() =>
dispatchUiNotification('profile.changePassword.successTitle', 'profile.changePassword.successText', {
icon: 'check'
})
)
.catch(showErrorNotification('profile.changePassword.failed'))
.finally(() => {
if (formRef.current) {
formRef.current.reset()
}
setOldPassword('')
setNewPassword('')
setNewPasswordAgain('')
})
void doRequest()
},
[oldPassword, newPassword, showErrorNotification, dispatchUiNotification]
[doRequest]
)
const ready = useMemo(() => {
return (
!loading &&
oldPassword.trim() !== '' &&
newPassword.trim() !== '' &&
newPasswordAgain.trim() !== '' &&
newPassword === newPasswordAgain
)
}, [oldPassword, newPassword, newPasswordAgain])
}, [loading, oldPassword, newPassword, newPasswordAgain])
return (
<Card className='bg-dark mb-4'>
@ -71,6 +79,12 @@ export const ProfileChangePassword: React.FC = () => {
<CurrentPasswordField onChange={onChangeOldPassword} value={oldPassword} />
<NewPasswordField onChange={onChangeNewPassword} value={newPassword} />
<PasswordAgainField password={newPassword} onChange={onChangeNewPasswordAgain} value={newPasswordAgain} />
<Alert className='small' show={!!error && !loading} variant={'danger'}>
<Trans i18nKey={error?.message} />
</Alert>
<Alert className='small' show={changeSucceeded && !loading} variant={'success'}>
<Trans i18nKey={'profile.changePassword.successText'} />
</Alert>
<Button type='submit' variant='primary' disabled={!ready}>
<Trans i18nKey='common.save' />

View file

@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ErrorToI18nKeyMapper } from '../../api/common/error-to-i18n-key-mapper'
import React, { useMemo } from 'react'
import { Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
interface RegisterErrorProps {
error?: Error
}
export const RegisterError: React.FC<RegisterErrorProps> = ({ error }) => {
useTranslation()
const errorI18nKey = useMemo(() => {
if (!error) {
return undefined
}
return new ErrorToI18nKeyMapper(error, 'login.register.error')
.withHttpCode(409, 'usernameExisting')
.withBackendErrorName('registrationDisabled', 'registrationDisabled')
.withBackendErrorName('passwordTooWeak', 'passwordTooWeak')
.orFallbackI18nKey('other')
}, [error])
return (
<Alert className='small' show={!!errorI18nKey} variant='danger'>
<Trans i18nKey={errorI18nKey} />
</Alert>
)
}

View file

@ -1,41 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { RegisterError as RegisterErrorType } from '../../../api/auth/types'
import React, { useMemo } from 'react'
import { Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
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 {@link undefined} when no error should be rendered.
*/
export const RegisterError: React.FC<RegisterErrorProps> = ({ error }) => {
useTranslation()
const errorMessageI18nKey = useMemo(() => {
switch (error) {
case RegisterErrorType.PASSWORD_TOO_WEAK:
return 'login.register.error.passwordTooWeak'
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>
)
}

View file

@ -1,11 +1,11 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
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 { useApplicationState } from '../../hooks/common/use-application-state'
import { TranslatedExternalLink } from '../common/links/translated-external-link'
import { ShowIf } from '../common/show-if/show-if'
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'