Hey,
I'm currently trying to implement the typical settings page, where you allow the user to change the password. Now, I'm having an issue, because I have a beforeValidateHook function on the Users collection - in there I do something like this:
export const usersBeforeValidateHook: CollectionBeforeValidateHook<User> = async ({
data,
req: { user, payload },
originalDoc,
}) => {
if (user && originalDoc && !user.roles.includes('admin') && !user.roles.includes('moderator')) {
throw new Error('Only an admin can edit another admin');
}
if (user && user.id === originalDoc.id) {
const newData = data as User & {
currentPassword?: string;
newPassword?: string;
};
if (typeof newData.newPassword !== 'undefined') {
const providedCurrentPasswordHash = payload.encrypt(newData.currentPassword ?? '');
if (user.password !== providedCurrentPasswordHash) {
throw new Error('The provided password does not match the current password.');
}
if (newData.newPassword.length < 8) {
throw new Error('Your password must be longer than 8 characters.');
}
data.password = payload.decrypt(newData.newPassword);
delete (data as any).currentPassword;
delete (data as any).newPassword;
}
}
return data;
};
Now, as you can see - the issue is, that the payload.encrypt() function does not work, because I'm sure that this is just a generic encrypt function and not the same one that is used when registering or logging the user in, as that one would need to also need to take in the salt. I have gone through the payload API, but can not really find a way on how to implement that properly.
So I'm wondering how would I implement this functionality of changing the password in payload?
Hi @hellboy124124! A couple questions:
1. Is this a settings page on your front end? Or a custom page in your Payload project?
2. Is the purpose of this to throw a more specific error? Are you also using the access property to set permissions on your users collection?
if (user && originalDoc && !user.roles.includes('admin') && !user.roles.includes('moderator')) {
throw new Error('Only an admin can edit another admin');
}
To simplify this and avoid the need for encrypting/decrypting, I would add to the function above
|| user.id !== originalDoc.id
, then remove the rest and take care of it on the front end.
Now on the front end, you'd make an onSubmit function to take their old password and send a login request to the REST API. If the login is successful, make another request to update their password.
You can take a look at how we do this for the Cloud settings page here:
https://github.com/payloadcms/website/blob/main/src/app/cloud/settings/client_page.tsxThank you.
1. Yes, this is on my frontend, which is actually a nextjs project - I have a /settings endpoint there, which basically just proxies the call to payload (PATCH /api/users/{id})
2. This is not really an issue, this all works as expected.
It's the bit after that, which does not work. At this point, the user is actually logged in, but from a security standpoint I think it's better for the user, when they are changing their password, to also provide the current password. At least that is the practice for the majority of platforms nowadays I think. So would there be any way to validate the provided current password somehow on the payload side - specifically on the User collection's beforeValidate hook?
You should also be able to do this with the
beforeValidate
hook , there is some useful information from Dan about how to use encrypt / decrypt functions from Payload here
https://github.com/payloadcms/payload/discussions/1435#discussioncomment-4173265Sadly that is not the same thing. Like mentioned in the original message - those do not work as those are general encryption functions, not the one that are used for the authentication process, which is the whole issue.
After reading your beforeValidate hook again, isn't
user.password
always returning as
undefined
?
At this point, the user is actually logged in, but from a security standpoint I think it's better for the user, when they are changing their password, to also provide the current password.
You can still have them provide the current password again, then make a payload.login request within your beforeValidate hook and continue that way?
Ok, I did try now with the method suggested, but it's rather really hack-y:
export const usersBeforeValidateHook: CollectionBeforeValidateHook<User> = async ({
data,
req: { user, payload },
originalDoc,
}) => {
if (user && originalDoc && !user.roles.includes('admin') && !user.roles.includes('moderator')) {
throw new Error('Only an admin can edit another admin');
}
if (user && user.id === originalDoc.id) {
const newData = data as User & {
currentPassword?: string;
newPassword?: string;
};
if (
typeof newData.currentPassword !== 'undefined' &&
typeof newData.newPassword === 'undefined'
) {
throw new Error('Please provide the new password');
} else if (typeof newData.newPassword !== 'undefined') {
try {
await payload.unlock({
collection: 'users',
data: {
email: user.email,
},
overrideAccess: true,
});
await payload.login({
collection: 'users',
data: {
email: user.email,
password: newData.currentPassword,
},
overrideAccess: true, // Not really working, as it will still block the user, without the .unlock() call above
});
} catch (err) {
throw new Error('The current password is incorrect');
}
if (newData.newPassword.length < 8) {
throw new Error('Your password must be longer than 8 characters.');
}
data.password = newData.newPassword; // TODO: This part is not working
delete (data as any).currentPassword;
delete (data as any).newPassword;
}
}
return data;
};
Still not full working, because when I provide the new password in "data.password = newData.password", it is not saving the new password. Is there any reason for that? And yes, I can 100% confirm that if I console.log() right before the return data;, I do get the correct (raw) password there.
Star
Discord
online
Get dedicated engineering support directly from the Payload team.