1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

refactor: improve password error handling (#1118)

* refactor: improve password error handling

* Update src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.tsx

Co-authored-by: Thomas Heartman <thomas@getunleash.ai>

Co-authored-by: Thomas Heartman <thomas@getunleash.ai>
This commit is contained in:
olav 2022-06-28 09:47:22 +02:00 committed by GitHub
parent 38cfc549f2
commit 566d0613a4
3 changed files with 36 additions and 50 deletions

View File

@ -1,13 +1,14 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { Avatar, TextField, Typography, Alert } from '@mui/material'; import { Avatar, TextField, Typography } from '@mui/material';
import { trim } from 'component/common/util'; import { trim } from 'component/common/util';
import { modalStyles } from 'component/admin/users/util'; import { modalStyles } from 'component/admin/users/util';
import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue } from 'component/common/Dialogue/Dialogue';
import PasswordChecker from 'component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker'; import PasswordChecker, {
PASSWORD_FORMAT_MESSAGE,
} from 'component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker';
import { useThemeStyles } from 'themes/themeStyles'; import { useThemeStyles } from 'themes/themeStyles';
import PasswordMatcher from 'component/user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher'; import PasswordMatcher from 'component/user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { IUser } from 'interfaces/user'; import { IUser } from 'interfaces/user';
interface IChangePasswordProps { interface IChangePasswordProps {
@ -24,47 +25,41 @@ const ChangePassword = ({
user, user,
}: IChangePasswordProps) => { }: IChangePasswordProps) => {
const [data, setData] = useState<Record<string, string>>({}); const [data, setData] = useState<Record<string, string>>({});
const [error, setError] = useState<Record<string, string>>({}); const [error, setError] = useState<string>();
const [validPassword, setValidPassword] = useState(false); const [validPassword, setValidPassword] = useState(false);
const { classes: themeStyles } = useThemeStyles(); const { classes: themeStyles } = useThemeStyles();
const updateField: React.ChangeEventHandler<HTMLInputElement> = event => { const updateField: React.ChangeEventHandler<HTMLInputElement> = event => {
setError({}); setError(undefined);
setData({ ...data, [event.target.name]: trim(event.target.value) }); setData({ ...data, [event.target.name]: trim(event.target.value) });
}; };
const submit = async (event: React.SyntheticEvent) => { const submit = async (event: React.SyntheticEvent) => {
event.preventDefault(); event.preventDefault();
if (data.password !== data.confirm) {
return;
}
if (!validPassword) { if (!validPassword) {
if (!data.password || data.password.length < 8) { setError(PASSWORD_FORMAT_MESSAGE);
setError({
password:
'You must specify a password with at least 8 chars.',
});
return; return;
} }
if (!(data.password === data.confirm)) {
setError({ confirm: 'Passwords does not match' });
return;
}
}
try { try {
await changePassword(user.id, data.password); await changePassword(user.id, data.password);
setData({}); setData({});
closeDialog(); closeDialog();
} catch (error: unknown) { } catch (error: unknown) {
const msg = console.warn(error);
(error instanceof Error && error.message) || setError(PASSWORD_FORMAT_MESSAGE);
'Could not update password';
setError({ general: msg });
} }
}; };
const onCancel = (event: React.SyntheticEvent) => { const onCancel = (event: React.SyntheticEvent) => {
event.preventDefault(); event.preventDefault();
setData({}); setData({});
setError(undefined);
closeDialog(); closeDialog();
}; };
@ -77,6 +72,7 @@ const ChangePassword = ({
primaryButtonText="Save" primaryButtonText="Save"
title="Update password" title="Update password"
secondaryButtonText="Cancel" secondaryButtonText="Cancel"
maxWidth="xs"
> >
<form <form
onSubmit={submit} onSubmit={submit}
@ -85,10 +81,6 @@ const ChangePassword = ({
themeStyles.flexColumn themeStyles.flexColumn
)} )}
> >
<ConditionallyRender
condition={Boolean(error.general)}
show={<Alert severity="error">{error.general}</Alert>}
/>
<Typography variant="subtitle1"> <Typography variant="subtitle1">
Changing password for user Changing password for user
</Typography> </Typography>
@ -117,7 +109,8 @@ const ChangePassword = ({
name="password" name="password"
type="password" type="password"
value={data.password} value={data.password}
helperText={error.password} error={Boolean(error)}
helperText={error}
onChange={updateField} onChange={updateField}
variant="outlined" variant="outlined"
size="small" size="small"
@ -127,8 +120,6 @@ const ChangePassword = ({
name="confirm" name="confirm"
type="password" type="password"
value={data.confirm} value={data.confirm}
error={error.confirm !== undefined}
helperText={error.confirm}
onChange={updateField} onChange={updateField}
variant="outlined" variant="outlined"
size="small" size="small"

View File

@ -3,10 +3,10 @@ import { Button, Typography } from '@mui/material';
import classnames from 'classnames'; import classnames from 'classnames';
import { useStyles } from './EditProfile.styles'; import { useStyles } from './EditProfile.styles';
import { useThemeStyles } from 'themes/themeStyles'; import { useThemeStyles } from 'themes/themeStyles';
import PasswordChecker from 'component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker'; import PasswordChecker, {
PASSWORD_FORMAT_MESSAGE,
} from 'component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker';
import PasswordMatcher from 'component/user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher'; import PasswordMatcher from 'component/user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher';
import { Alert } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import useLoading from 'hooks/useLoading'; import useLoading from 'hooks/useLoading';
import { import {
BAD_REQUEST, BAD_REQUEST,
@ -17,6 +17,7 @@ import {
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import PasswordField from 'component/common/PasswordField/PasswordField'; import PasswordField from 'component/common/PasswordField/PasswordField';
import { headers } from 'utils/apiUtils'; import { headers } from 'utils/apiUtils';
import { formatUnknownError } from 'utils/formatUnknownError';
interface IEditProfileProps { interface IEditProfileProps {
setEditingProfile: React.Dispatch<React.SetStateAction<boolean>>; setEditingProfile: React.Dispatch<React.SetStateAction<boolean>>;
@ -31,7 +32,7 @@ const EditProfile = ({
const { classes: themeStyles } = useThemeStyles(); const { classes: themeStyles } = useThemeStyles();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [validPassword, setValidPassword] = useState(false); const [validPassword, setValidPassword] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState<string>();
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const ref = useLoading(loading); const ref = useLoading(loading);
@ -39,13 +40,13 @@ const EditProfile = ({
const submit = async (e: SyntheticEvent) => { const submit = async (e: SyntheticEvent) => {
e.preventDefault(); e.preventDefault();
if (!validPassword || password !== confirmPassword) { if (password !== confirmPassword) {
setError( return;
'Password is not valid, or your passwords do not match. Please provide a password with length over 10 characters, an uppercase letter, a lowercase letter, a number and a symbol.' } else if (!validPassword) {
); setError(PASSWORD_FORMAT_MESSAGE);
} else { } else {
setLoading(true); setLoading(true);
setError(''); setError(undefined);
try { try {
const path = formatApiPath('api/admin/user/change-password'); const path = formatApiPath('api/admin/user/change-password');
const res = await fetch(path, { const res = await fetch(path, {
@ -55,8 +56,8 @@ const EditProfile = ({
credentials: 'include', credentials: 'include',
}); });
handleResponse(res); handleResponse(res);
} catch (e: any) { } catch (error: unknown) {
setError(e); setError(formatUnknownError(error));
} }
} }
setLoading(false); setLoading(false);
@ -64,9 +65,7 @@ const EditProfile = ({
const handleResponse = (res: Response) => { const handleResponse = (res: Response) => {
if (res.status === BAD_REQUEST) { if (res.status === BAD_REQUEST) {
setError( setError(PASSWORD_FORMAT_MESSAGE);
'Password could not be accepted. Please make sure you are inputting a valid password.'
);
} }
if (res.status === UNAUTHORIZED) { if (res.status === UNAUTHORIZED) {
@ -94,14 +93,6 @@ const EditProfile = ({
> >
Update password Update password
</Typography> </Typography>
<ConditionallyRender
condition={Boolean(error)}
show={
<Alert data-loading severity="error">
{error}
</Alert>
}
/>
<form <form
className={classnames(styles.form, themeStyles.contentSpacingY)} className={classnames(styles.form, themeStyles.contentSpacingY)}
> >
@ -115,6 +106,8 @@ const EditProfile = ({
label="Password" label="Password"
name="password" name="password"
value={password} value={password}
error={Boolean(error)}
helperText={error}
autoComplete="new-password" autoComplete="new-password"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setPassword(e.target.value) setPassword(e.target.value)

View File

@ -35,6 +35,8 @@ const LOWERCASE_ERROR =
'The password must contain at least one lowercase letter.'; 'The password must contain at least one lowercase letter.';
const REPEATING_CHARACTER_ERROR = const REPEATING_CHARACTER_ERROR =
'The password may not contain sequences of three or more repeated characters.'; 'The password may not contain sequences of three or more repeated characters.';
export const PASSWORD_FORMAT_MESSAGE =
'The password must be at least 10 characters long and must include an uppercase letter, a lowercase letter, a number, and a symbol.';
const PasswordChecker = ({ const PasswordChecker = ({
password, password,
@ -159,7 +161,7 @@ const PasswordChecker = ({
<> <>
<Typography variant="body2" className={styles.title} data-loading> <Typography variant="body2" className={styles.title} data-loading>
Please set a strong password Please set a strong password
<HelpIcon tooltip="Your password needs to be at least ten characters long, and include an uppercase letter, a lowercase letter, a number and a symbol to be a valid OWASP password" /> <HelpIcon tooltip={PASSWORD_FORMAT_MESSAGE} />
</Typography> </Typography>
<div <div
className={styles.container} className={styles.container}