I’m sharing my working notes on here to force myself to articulate my understanding as I build a GraphQL project. This isn’t a tutorial, sorry if you came here looking for one.
How it works: 2 key resolvers
requestReset
Arguments
email
Finds user with that
email
in dbCreates random bytes as
resetToken
Creates a date as
resetTokenExpiry
Adds
resetToken
andresetTokenExpiry
to the user in the dbSends an email to the user that has the token
resetPassword
Arguments:
email
password
confirmPassword
resetToken
Find the the user in the db with that
resetToken
Make sure
resetTokenExpiry
date hasn’t expiredHash the new
password
Update the user in the db
password
: The new hashed passwordresetToken
: nullresetTokenExpiry
: null
Return the user
Here’s a look at the code for each:
requestReset: async (parent, { email }, { models }) => { | |
email = email.toLowerCase(); | |
// Check that user exists. | |
const user = await models.User.findAll({ | |
where: { | |
email: email | |
}, | |
plain: true | |
}); | |
if (!user) throw new Error("No user found with that email."); | |
// Create randomBytes that will be used as a token | |
const randomBytesPromisified = promisify(randomBytes); | |
const resetToken = (await randomBytesPromisified(20)).toString("hex"); | |
const resetTokenExpiry = Date.now() + 3600000; // 1 hour from now | |
// Add token and tokenExpiry to the db user | |
const result = await models.User.update( | |
{ resetToken, resetTokenExpiry }, | |
{ | |
where: { email }, | |
returning: true, | |
plain: true | |
} | |
); | |
// Email them the token | |
const mailRes = await transport.sendMail({ | |
from: process.env.MAIL_SENDER, | |
to: user.email, | |
subject: "Your Password Reset Token", | |
html: makeResetEmail(resetToken) | |
}); | |
return true; | |
}, |
resetPassword: async ( | |
parent, | |
{ email, password, confirmPassword, resetToken }, | |
{ models, res } | |
) => { | |
email = email.toLowerCase(); | |
// check if passwords match | |
if (password !== confirmPassword) { | |
throw new Error(`Your passwords don't match`); | |
} | |
// find the user with that resetToken | |
// make sure it's not expired | |
const user = await models.User.findAll({ | |
where: { | |
resetToken, | |
resetTokenExpiry: { | |
[Op.gte]: Date.now() - 3600000 | |
} | |
}, | |
plain: true | |
}); | |
// throw error if user doesn't exist | |
if (!user) | |
throw new Error( | |
"Your password reset token is either invalid or expired." | |
); | |
const saltRounds = 12; | |
const hash = await bcrypt.hash(password, saltRounds); | |
const result = await models.User.update( | |
{ | |
password: hash, | |
resetToken: null, | |
resetTokenExpiry: null | |
}, | |
{ | |
where: { id: user.id }, | |
returning: true, | |
plain: true | |
} | |
); | |
// sequelize puts our result in an array, in the 2nd slot (first slot give affected rows) | |
const updatedUser = result[1]; | |
// jwt | |
const token = await createToken(updatedUser, process.env.APP_SECRET); | |
// cookie with jwt | |
res.cookie("token", token, { | |
httpOnly: true, | |
maxAge: 1000 * 60 * 60 * 24 * 365 // 1 year cookie | |
}); | |
return updatedUser; | |
} |
How to test the Password Reset flow in GraphQL Playground
1. Run the requestReset
mutation:
mutation { requestReset(email: "jacob@gmail.com") }
2. Check MailTrap (my dev test email inbox) or the database (I’m using postgres) to get the resetToken
:
Example commands for postgres in Terminal:
psql \c graindev SELECT * FROM users;
3. Run the resetPassword
mutation in GraphQL Playground:
mutation { resetPassword( email: "user@test.com" password: "password" confirmPassword: "password" resetToken: "0ad35f89a0173f474dc87d6b1323e48fadecb1c4" ) { id email username } }
If all went well, it should return the user.