Add current password requirement to edit own profile password

This commit is contained in:
Alexandre Bodin 2021-10-21 13:20:57 +02:00
parent a639e92022
commit 15e18be98c
8 changed files with 154 additions and 20 deletions

View File

@ -47,6 +47,7 @@ const FieldActionWrapper = styled(FieldAction)`
const ProfilePage = () => {
const [passwordShown, setPasswordShown] = useState(false);
const [passwordConfirmShown, setPasswordConfirmShown] = useState(false);
const [currentPasswordShown, setCurrentPasswordShown] = useState(false);
const { changeLocale, localeNames } = useLocalesProvider();
const { setUserDisplayName } = useAppInfos();
const queryClient = useQueryClient();
@ -88,14 +89,8 @@ const ProfilePage = () => {
type: 'success',
message: { id: 'notification.success.saved', defaultMessage: 'Saved' },
});
unlockApp();
},
onError: () => {
toggleNotification({
type: 'warning',
message: { id: 'notification.error', defaultMessage: 'An error occured' },
});
onSettled: () => {
unlockApp();
},
refetchActive: true,
@ -103,11 +98,27 @@ const ProfilePage = () => {
const { isLoading: isSubmittingForm } = submitMutation;
const handleSubmit = async body => {
const handleSubmit = async (body, { setErrors }) => {
lockApp();
const username = body.username || null;
await submitMutation.mutateAsync({ ...body, username });
submitMutation.mutate(
{ ...body, username },
{
onError: error => {
const res = error?.response?.data;
if (res?.data) {
return setErrors(res.data);
}
return toggleNotification({
type: 'warning',
message: { id: 'notification.error', defaultMessage: 'An error occured' },
});
},
}
);
};
const fieldsToPick = ['email', 'firstname', 'lastname', 'username', 'preferedLanguage'];
@ -250,6 +261,51 @@ const ProfilePage = () => {
defaultMessage: 'Change password',
})}
</H3>
<Grid gap={5}>
<GridItem s={12} col={6}>
<TextInput
error={
errors.currentPassword
? formatMessage({
id: errors.currentPassword,
defaultMessage: errors.currentPassword,
})
: ''
}
onChange={handleChange}
value={values.currentPassword || ''}
label={formatMessage({
id: 'Auth.form.currentPassword.label',
defaultMessage: 'Current Password',
})}
name="currentPassword"
type={currentPasswordShown ? 'text' : 'password'}
endAction={
<FieldActionWrapper
onClick={e => {
e.stopPropagation();
setCurrentPasswordShown(prev => !prev);
}}
label={formatMessage(
currentPasswordShown
? {
id: 'Auth.form.password.show-password',
defaultMessage: 'Show password',
}
: {
id: 'Auth.form.password.hide-password',
defaultMessage: 'Hide password',
}
)}
>
{currentPasswordShown ? <Show /> : <Hide />}
</FieldActionWrapper>
}
/>
</GridItem>
</Grid>
<Grid gap={5}>
<GridItem s={12} col={6}>
<TextInput
@ -257,7 +313,7 @@ const ProfilePage = () => {
errors.password
? formatMessage({
id: errors.password,
defaultMessage: 'This value is required.',
defaultMessage: errors.password,
})
: ''
}
@ -295,10 +351,10 @@ const ProfilePage = () => {
<GridItem s={12} col={6}>
<TextInput
error={
errors.password
errors.confirmPassword
? formatMessage({
id: errors.password,
defaultMessage: 'This value is required.',
id: errors.confirmPassword,
defaultMessage: errors.confirmPassword,
})
: ''
}
@ -369,15 +425,21 @@ const ProfilePage = () => {
defaultMessage:
'This will only display your own interface in the chosen language.',
})}
onClear={() =>
handleChange({ target: { name: 'preferedLanguage', value: null } })}
onClear={() => {
handleChange({
target: { name: 'preferedLanguage', value: null },
});
}}
clearLabel={formatMessage({
id: 'Settings.profile.form.section.experience.clear.select',
defaultMessage: 'Clear the interface language selected',
})}
value={values.preferedLanguage}
onChange={e =>
handleChange({ target: { name: 'preferedLanguage', value: e } })}
onChange={e => {
handleChange({
target: { name: 'preferedLanguage', value: e },
});
}}
>
{Object.keys(localeNames).map(language => {
const langName = localeNames[language];

View File

@ -23,6 +23,13 @@ const schema = {
.when('password', (password, passSchema) => {
return password ? passSchema.required(translatedErrors.required) : passSchema;
}),
currentPassword: yup
.string()
.when(['password', 'confirmPassword'], (password, confirmPassword, passSchema) => {
return password || confirmPassword
? passSchema.required(translatedErrors.required)
: passSchema;
}),
preferedLanguage: yup.string().nullable(),
};

View File

@ -40,6 +40,7 @@
"Auth.form.password.hint": "Password must contain at least 8 characters, 1 uppercase, 1 lowercase and 1 number",
"Auth.form.password.label": "Password",
"Auth.form.password.show-password": "Show password",
"Auth.form.currentPassword.label": "Current Password",
"Auth.form.register.news.label": "Keep me updated about the new features and upcoming improvements (by doing this you accept the {terms} and the {policy}).",
"Auth.form.register.subtitle": "Your credentials are only used to authenticate yourself on the admin panel. All saved data will be stored in your own database.",
"Auth.form.rememberMe.label": "Remember me",

View File

@ -28,7 +28,8 @@
"Auth.form.forgot-password.email.label.success": "E-mail envoyé avec succès à l'adresse suivante",
"Auth.form.lastname.label": "Nom",
"Auth.form.lastname.placeholder": "Doe",
"Auth.form.password.label": "Mot de Passe",
"Auth.form.password.label": "Mot de passe",
"Auth.form.currentPassword.label": "Mot de passe actuel",
"Auth.form.register.news.label": "Me tenir au courant des nouvelles fonctionnalités et améliorations à venir (en faisant cela vous acceptez les {terms} et {policy}).",
"Auth.form.rememberMe.label": "Se souvenir de moi",
"Auth.form.username.label": "Nom d'utilisateur",

View File

@ -22,8 +22,21 @@ module.exports = {
}
const userService = getService('user');
const authServer = getService('auth');
const updatedUser = await userService.updateById(ctx.state.user.id, input);
const { currentPassword, ...userInfo } = ctx.request.body;
if (currentPassword && userInfo.password) {
const isValid = await authServer.validatePassword(currentPassword, ctx.state.user.password);
if (!isValid) {
return ctx.badRequest('ValidationError', {
currentPassword: ['Invalid credentials'],
});
}
}
const updatedUser = await userService.updateById(ctx.state.user.id, userInfo);
ctx.body = {
data: userService.sanitizeUser(updatedUser),

View File

@ -16,7 +16,7 @@ const hashPassword = password => bcrypt.hash(password, 10);
* Validate a password
* @param {string} password
* @param {string} hash
* @returns {boolean} is the password valid
* @returns {Promise<boolean>} is the password valid
*/
const validatePassword = (password, hash) => bcrypt.compare(password, hash);

View File

@ -133,5 +133,50 @@ describe('Authenticated User', () => {
isActive: expect.any(Boolean),
});
});
test('Updating password requires currentPassword', async () => {
const input = {
password: 'newPassword1234',
};
const res = await rq({
url: '/admin/users/me',
method: 'PUT',
body: input,
});
expect(res.statusCode).toBe(400);
expect(res.body).toMatchObject({
statusCode: 400,
error: 'Bad Request',
message: 'ValidationError',
data: {
currentPassword: expect.anything(),
},
});
});
test('Updating password requires currentPassword to be valid', async () => {
const input = {
password: 'newPassword1234',
currentPassword: 'wrongPass',
};
const res = await rq({
url: '/admin/users/me',
method: 'PUT',
body: input,
});
expect(res.statusCode).toBe(400);
expect(res.body).toMatchObject({
statusCode: 400,
error: 'Bad Request',
message: 'ValidationError',
data: {
currentPassword: expect.anything(),
},
});
});
});
});

View File

@ -1,5 +1,6 @@
'use strict';
const { isUndefined } = require('lodash/fp');
const { yup, formatYupErrors } = require('@strapi/utils');
const validators = require('./common-validators');
@ -28,6 +29,10 @@ const profileUpdateSchema = yup
lastname: validators.lastname.notNull(),
username: validators.username.nullable(),
password: validators.password.notNull(),
currentPassword: yup
.string()
.when('password', (password, schema) => (!isUndefined(password) ? schema.required() : schema))
.notNull(),
preferedLanguage: yup.string().nullable(),
})
.noUnknown();