diff --git a/packages/core/admin/admin/src/pages/ProfilePage/index.js b/packages/core/admin/admin/src/pages/ProfilePage/index.js
index 02be23f755..62e23f04f2 100644
--- a/packages/core/admin/admin/src/pages/ProfilePage/index.js
+++ b/packages/core/admin/admin/src/pages/ProfilePage/index.js
@@ -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',
})}
+
+
+
+ {
+ 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 ? : }
+
+ }
+ />
+
+
+
{
errors.password
? formatMessage({
id: errors.password,
- defaultMessage: 'This value is required.',
+ defaultMessage: errors.password,
})
: ''
}
@@ -295,10 +351,10 @@ 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];
diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/utils/validations/users/profile.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/utils/validations/users/profile.js
index f4f255bf2f..d2e45c1436 100644
--- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/utils/validations/users/profile.js
+++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/utils/validations/users/profile.js
@@ -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(),
};
diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json
index 5500f2703b..7fd99d1d54 100644
--- a/packages/core/admin/admin/src/translations/en.json
+++ b/packages/core/admin/admin/src/translations/en.json
@@ -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",
diff --git a/packages/core/admin/admin/src/translations/fr.json b/packages/core/admin/admin/src/translations/fr.json
index 5008adff19..ff9a6d9d1a 100644
--- a/packages/core/admin/admin/src/translations/fr.json
+++ b/packages/core/admin/admin/src/translations/fr.json
@@ -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",
diff --git a/packages/core/admin/server/controllers/authenticated-user.js b/packages/core/admin/server/controllers/authenticated-user.js
index 35fb8bf67e..6f9019bfff 100644
--- a/packages/core/admin/server/controllers/authenticated-user.js
+++ b/packages/core/admin/server/controllers/authenticated-user.js
@@ -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),
diff --git a/packages/core/admin/server/services/auth.js b/packages/core/admin/server/services/auth.js
index bc57e07e18..9ec6819602 100644
--- a/packages/core/admin/server/services/auth.js
+++ b/packages/core/admin/server/services/auth.js
@@ -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} is the password valid
*/
const validatePassword = (password, hash) => bcrypt.compare(password, hash);
diff --git a/packages/core/admin/server/tests/admin-authenticated-user.test.e2e.js b/packages/core/admin/server/tests/admin-authenticated-user.test.e2e.js
index bf574b380a..4c2119349b 100644
--- a/packages/core/admin/server/tests/admin-authenticated-user.test.e2e.js
+++ b/packages/core/admin/server/tests/admin-authenticated-user.test.e2e.js
@@ -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(),
+ },
+ });
+ });
});
});
diff --git a/packages/core/admin/server/validation/user.js b/packages/core/admin/server/validation/user.js
index 842fa6d551..cea2da2603 100644
--- a/packages/core/admin/server/validation/user.js
+++ b/packages/core/admin/server/validation/user.js
@@ -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();