mirror of
https://github.com/strapi/strapi.git
synced 2025-12-16 09:45:08 +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 ProfilePage = () => {
|
||||||
const [passwordShown, setPasswordShown] = useState(false);
|
const [passwordShown, setPasswordShown] = useState(false);
|
||||||
const [passwordConfirmShown, setPasswordConfirmShown] = useState(false);
|
const [passwordConfirmShown, setPasswordConfirmShown] = useState(false);
|
||||||
|
const [currentPasswordShown, setCurrentPasswordShown] = useState(false);
|
||||||
const { changeLocale, localeNames } = useLocalesProvider();
|
const { changeLocale, localeNames } = useLocalesProvider();
|
||||||
const { setUserDisplayName } = useAppInfos();
|
const { setUserDisplayName } = useAppInfos();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -88,14 +89,8 @@ const ProfilePage = () => {
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
message: { id: 'notification.success.saved', defaultMessage: 'Saved' },
|
message: { id: 'notification.success.saved', defaultMessage: 'Saved' },
|
||||||
});
|
});
|
||||||
|
|
||||||
unlockApp();
|
|
||||||
},
|
},
|
||||||
onError: () => {
|
onSettled: () => {
|
||||||
toggleNotification({
|
|
||||||
type: 'warning',
|
|
||||||
message: { id: 'notification.error', defaultMessage: 'An error occured' },
|
|
||||||
});
|
|
||||||
unlockApp();
|
unlockApp();
|
||||||
},
|
},
|
||||||
refetchActive: true,
|
refetchActive: true,
|
||||||
@ -103,11 +98,27 @@ const ProfilePage = () => {
|
|||||||
|
|
||||||
const { isLoading: isSubmittingForm } = submitMutation;
|
const { isLoading: isSubmittingForm } = submitMutation;
|
||||||
|
|
||||||
const handleSubmit = async body => {
|
const handleSubmit = async (body, { setErrors }) => {
|
||||||
lockApp();
|
lockApp();
|
||||||
|
|
||||||
const username = body.username || null;
|
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'];
|
const fieldsToPick = ['email', 'firstname', 'lastname', 'username', 'preferedLanguage'];
|
||||||
@ -250,6 +261,51 @@ const ProfilePage = () => {
|
|||||||
defaultMessage: 'Change password',
|
defaultMessage: 'Change password',
|
||||||
})}
|
})}
|
||||||
</H3>
|
</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}>
|
<Grid gap={5}>
|
||||||
<GridItem s={12} col={6}>
|
<GridItem s={12} col={6}>
|
||||||
<TextInput
|
<TextInput
|
||||||
@ -257,7 +313,7 @@ const ProfilePage = () => {
|
|||||||
errors.password
|
errors.password
|
||||||
? formatMessage({
|
? formatMessage({
|
||||||
id: errors.password,
|
id: errors.password,
|
||||||
defaultMessage: 'This value is required.',
|
defaultMessage: errors.password,
|
||||||
})
|
})
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
@ -295,10 +351,10 @@ const ProfilePage = () => {
|
|||||||
<GridItem s={12} col={6}>
|
<GridItem s={12} col={6}>
|
||||||
<TextInput
|
<TextInput
|
||||||
error={
|
error={
|
||||||
errors.password
|
errors.confirmPassword
|
||||||
? formatMessage({
|
? formatMessage({
|
||||||
id: errors.password,
|
id: errors.confirmPassword,
|
||||||
defaultMessage: 'This value is required.',
|
defaultMessage: errors.confirmPassword,
|
||||||
})
|
})
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
@ -369,15 +425,21 @@ const ProfilePage = () => {
|
|||||||
defaultMessage:
|
defaultMessage:
|
||||||
'This will only display your own interface in the chosen language.',
|
'This will only display your own interface in the chosen language.',
|
||||||
})}
|
})}
|
||||||
onClear={() =>
|
onClear={() => {
|
||||||
handleChange({ target: { name: 'preferedLanguage', value: null } })}
|
handleChange({
|
||||||
|
target: { name: 'preferedLanguage', value: null },
|
||||||
|
});
|
||||||
|
}}
|
||||||
clearLabel={formatMessage({
|
clearLabel={formatMessage({
|
||||||
id: 'Settings.profile.form.section.experience.clear.select',
|
id: 'Settings.profile.form.section.experience.clear.select',
|
||||||
defaultMessage: 'Clear the interface language selected',
|
defaultMessage: 'Clear the interface language selected',
|
||||||
})}
|
})}
|
||||||
value={values.preferedLanguage}
|
value={values.preferedLanguage}
|
||||||
onChange={e =>
|
onChange={e => {
|
||||||
handleChange({ target: { name: 'preferedLanguage', value: e } })}
|
handleChange({
|
||||||
|
target: { name: 'preferedLanguage', value: e },
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{Object.keys(localeNames).map(language => {
|
{Object.keys(localeNames).map(language => {
|
||||||
const langName = localeNames[language];
|
const langName = localeNames[language];
|
||||||
|
|||||||
@ -23,6 +23,13 @@ const schema = {
|
|||||||
.when('password', (password, passSchema) => {
|
.when('password', (password, passSchema) => {
|
||||||
return password ? passSchema.required(translatedErrors.required) : 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(),
|
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.hint": "Password must contain at least 8 characters, 1 uppercase, 1 lowercase and 1 number",
|
||||||
"Auth.form.password.label": "Password",
|
"Auth.form.password.label": "Password",
|
||||||
"Auth.form.password.show-password": "Show 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.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.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",
|
"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.forgot-password.email.label.success": "E-mail envoyé avec succès à l'adresse suivante",
|
||||||
"Auth.form.lastname.label": "Nom",
|
"Auth.form.lastname.label": "Nom",
|
||||||
"Auth.form.lastname.placeholder": "Doe",
|
"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.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.rememberMe.label": "Se souvenir de moi",
|
||||||
"Auth.form.username.label": "Nom d'utilisateur",
|
"Auth.form.username.label": "Nom d'utilisateur",
|
||||||
|
|||||||
@ -22,8 +22,21 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userService = getService('user');
|
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 = {
|
ctx.body = {
|
||||||
data: userService.sanitizeUser(updatedUser),
|
data: userService.sanitizeUser(updatedUser),
|
||||||
|
|||||||
@ -16,7 +16,7 @@ const hashPassword = password => bcrypt.hash(password, 10);
|
|||||||
* Validate a password
|
* Validate a password
|
||||||
* @param {string} password
|
* @param {string} password
|
||||||
* @param {string} hash
|
* @param {string} hash
|
||||||
* @returns {boolean} is the password valid
|
* @returns {Promise<boolean>} is the password valid
|
||||||
*/
|
*/
|
||||||
const validatePassword = (password, hash) => bcrypt.compare(password, hash);
|
const validatePassword = (password, hash) => bcrypt.compare(password, hash);
|
||||||
|
|
||||||
|
|||||||
@ -133,5 +133,50 @@ describe('Authenticated User', () => {
|
|||||||
isActive: expect.any(Boolean),
|
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';
|
'use strict';
|
||||||
|
|
||||||
|
const { isUndefined } = require('lodash/fp');
|
||||||
const { yup, formatYupErrors } = require('@strapi/utils');
|
const { yup, formatYupErrors } = require('@strapi/utils');
|
||||||
const validators = require('./common-validators');
|
const validators = require('./common-validators');
|
||||||
|
|
||||||
@ -28,6 +29,10 @@ const profileUpdateSchema = yup
|
|||||||
lastname: validators.lastname.notNull(),
|
lastname: validators.lastname.notNull(),
|
||||||
username: validators.username.nullable(),
|
username: validators.username.nullable(),
|
||||||
password: validators.password.notNull(),
|
password: validators.password.notNull(),
|
||||||
|
currentPassword: yup
|
||||||
|
.string()
|
||||||
|
.when('password', (password, schema) => (!isUndefined(password) ? schema.required() : schema))
|
||||||
|
.notNull(),
|
||||||
preferedLanguage: yup.string().nullable(),
|
preferedLanguage: yup.string().nullable(),
|
||||||
})
|
})
|
||||||
.noUnknown();
|
.noUnknown();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user