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:
parent
38cfc549f2
commit
566d0613a4
@ -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"
|
||||||
|
@ -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)
|
||||||
|
@ -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}
|
||||||
|
Loading…
Reference in New Issue
Block a user