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();