mirror of
https://github.com/strapi/strapi.git
synced 2025-12-13 07:55:33 +00:00
Add current password requirement to edit own profile password
This commit is contained in:
parent
a639e92022
commit
15e18be98c
@ -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];
|
||||
|
||||
@ -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(),
|
||||
};
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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(),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user