From fc874be72a8a718ae44cc1c2c8371a106cc97949 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 29 Mar 2020 19:58:52 -0400 Subject: [PATCH] revises and simplifies forgot password routes --- src/auth/passwordResets/routes.js | 270 +++++++++--------- src/auth/requestHandlers.js | 4 +- src/auth/routes.js | 48 ++-- .../components/views/CreateFirstUser/index.js | 4 +- 4 files changed, 158 insertions(+), 168 deletions(-) diff --git a/src/auth/passwordResets/routes.js b/src/auth/passwordResets/routes.js index 315456c8e1..3c18de73d8 100644 --- a/src/auth/passwordResets/routes.js +++ b/src/auth/passwordResets/routes.js @@ -4,159 +4,153 @@ const nodemailer = require('nodemailer'); const crypto = require('crypto'); const router = express.Router(); + const passwordResetRoutes = (emailConfig, User) => { + const mockEmailHandler = async () => { + const testAccount = await nodemailer.createTestAccount(); + + const smtpOptions = { + ...emailConfig, + host: 'smtp.ethereal.email', + port: 587, + secure: false, + fromName: 'John Doe', + fromAddress: 'john.doe@payloadcms.com', + auth: { + user: testAccount.user, + pass: testAccount.pass, + }, + }; + + return nodemailer.createTransport(smtpOptions); + }; + + + const sendResetEmail = async (req, res, next) => { + let emailHandler; + + try { + switch (emailConfig.provider) { + case 'mock': + emailHandler = await mockEmailHandler(); + break; + case 'smtp': + emailHandler = nodemailer.createTransport(emailConfig); + break; + default: + emailHandler = await mockEmailHandler(req, res, next, emailConfig); + } + + const generateToken = () => new Promise(resolve => crypto.randomBytes(20, (err, buffer) => resolve(buffer.toString('hex')))); + + const token = await generateToken(); + + User.findOne({ email: req.body.userEmail }, (err, user) => { + if (!user) { + const message = `No account with email ${req.body.userEmail} address exists.`; + return res.status(400).json({ message }); + } + + user.resetPasswordToken = token; + user.resetPasswordExpiration = Date.now() + 3600000; // 1 hour + + user.save(async (saveError) => { + if (saveError) { + const message = 'Error saving temporary reset token'; + console.error(message, saveError); + res.status(500).json({ message }); + } + + console.log('Temporary reset token created and saved to DB'); + + const emailText = `You are receiving this because you (or someone else) have requested the reset of the password for your account. + Please click on the following link, or paste this into your browser to complete the process: + ${req.protocol}://${req.headers.host}/reset/${token} + If you did not request this, please ignore this email and your password will remain unchanged.`; + + await emailHandler.sendMail({ + from: `"${emailConfig.fromName}" <${emailConfig.fromAddress}>`, + to: req.body.userEmail, + subject: req.body.subject || 'Password Reset', + text: emailText, + }); + res.sendStatus(200); + }); + }); + } catch (e) { + console.error(e); + res.status(500).json({ success: false, error: 'Unable to send e-mail' }); + } + }; + + const validateForgotRequestBody = (req, res, next) => { + if (Object.prototype.hasOwnProperty.call(req.body, 'userEmail')) { + next(); + } else { + return res.status(400).json({ + message: 'Missing userEmail in request body', + }); + } + }; + + const validateResetPasswordBody = (req, res, next) => { + if (Object.prototype.hasOwnProperty.call(req.body, 'token') && Object.prototype.hasOwnProperty.call(req.body, 'password')) { + next(); + } else { + return res.status(400).json({ + message: 'Invalid request body', + }); + } + }; + + const resetPassword = (req, res, User) => { + User.findOne( + { + resetPasswordToken: req.body.token, + resetPasswordExpiration: { $gt: Date.now() }, + }, + async (err, user) => { + if (!user) { + const message = 'Password reset token is invalid or has expired.'; + console.error(message); + return res.status(400).json({ message }); + } + + const errorMessage = 'Error setting new user password'; + try { + await user.setPassword(req.body.password); + user.resetPasswordExpiration = Date.now(); + await user.save((saveError) => { + if (saveError) { + console.error(errorMessage); + return res.status(500).json({ message: errorMessage }); + } + + return res.status(200).json({ message: 'Password successfully reset' }); + }); + } catch (e) { + return res.status(500).json({ message: errorMessage }); + } + }, + ); + }; router .route('/forgot') .post( passport.authenticate('jwt', { session: false }), - (req, res, next) => validateForgotRequestBody(req, res, next), - (req, res, next) => sendResetEmail(req, res, next, emailConfig, User) + validateForgotRequestBody, + sendResetEmail, ); router .route('/reset') .post( (req, res, next) => validateResetPasswordBody(req, res, next), - (req, res) => resetPassword(req, res, User) + (req, res) => resetPassword(req, res, User), ); return router; }; -const sendResetEmail = async (req, res, next, emailConfig, User) => { - let emailHandler; - - try { - switch (emailConfig.provider) { - case 'mock': - emailHandler = await mockEmailHandler(req, res, next, emailConfig); - break; - case 'smtp': - emailHandler = smtpEmailHandler(req, res, next, emailConfig); - break; - default: - emailHandler = await mockEmailHandler(req, res, next, emailConfig); - } - - const generateToken = () => new Promise(resolve => - crypto.randomBytes(20, (err, buffer) => - resolve(buffer.toString('hex')) - )); - - let token = await generateToken(); - - User.findOne({ email: req.body.userEmail }, (err, user) => { - if (!user) { - const message = `No account with email ${req.body.userEmail} address exists.`; - return res.status(400).json({ message: message }) - } - - user.resetPasswordToken = token; - user.resetPasswordExpiration = Date.now() + 3600000; // 1 hour - user.save(async (saveError) => { - if (saveError) { - const message = 'Error saving temporary reset token'; - console.error(message, saveError); - res.status(500).json({ message }); - } - console.log('Temporary reset token created and saved to DB'); - - - const emailText = `You are receiving this because you (or someone else) have requested the reset of the password for your account. - Please click on the following link, or paste this into your browser to complete the process: - ${req.protocol}://${req.headers.host}/reset/${token} - If you did not request this, please ignore this email and your password will remain unchanged.`; - - await emailHandler.sendMail({ - from: `"${emailConfig.fromName}" <${emailConfig.fromAddress}>`, - to: req.body.userEmail, - subject: req.body.subject || 'Password Reset', - text: emailText, - }); - res.sendStatus(200); - - }); - }); - } catch (e) { - console.error(e); - res.status(500).json({ success: false, error: 'Unable to send e-mail' }); - } -}; - -const validateForgotRequestBody = (req, res, next) => { - if (req.body.hasOwnProperty('userEmail')) { - next(); - } else { - return res.status(400).json({ - message: 'Missing userEmail in request body' - }) - } -}; - -const validateResetPasswordBody = (req, res, next) => { - if (req.body.hasOwnProperty('token') && req.body.hasOwnProperty('password')) { - next(); - } else { - return res.status(400).json({ - message: 'Invalid request body' - }) - } -}; - -const resetPassword = (req, res, User) => { - User.findOne( - { - resetPasswordToken: req.body.token, - resetPasswordExpiration: { $gt: Date.now() } - }, - async (err, user) => { - if (!user) { - const message = 'Password reset token is invalid or has expired.'; - console.error(message); - return res.status(400).json({ message }); - } - - const errorMessage = 'Error setting new user password'; - try { - await user.setPassword(req.body.password); - user.resetPasswordExpiration = Date.now(); - await user.save(saveError => { - if (saveError) { - console.error(errorMessage); - return res.status(500).json({ message: errorMessage }); - } - - return res.status(200).json({ message: 'Password successfully reset' }); - }); - } catch (e) { - return res.status(500).json({ message: errorMessage }); - } - }) -}; - -const mockEmailHandler = async (req, res, next, emailConfig) => { - let testAccount = await nodemailer.createTestAccount(); - process.env.EMAIL_USER = testAccount.user; - process.env.EMAIL_PASS = testAccount.pass; - emailConfig.host = 'smtp.ethereal.email'; - emailConfig.port = 587; - emailConfig.secure = false; - emailConfig.fromName = 'John Doe'; - emailConfig.fromAddress = 'john.doe@payloadcms.com'; - return smtpEmailHandler(req, res, next, emailConfig); -}; - -const smtpEmailHandler = (req, res, next, emailConfig) => { - return nodemailer.createTransport({ - host: emailConfig.host, - port: emailConfig.port, - secure: emailConfig.secure, // true for 465, false for other ports - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASS - } - }); -}; - module.exports = passwordResetRoutes; diff --git a/src/auth/requestHandlers.js b/src/auth/requestHandlers.js index 32713948c2..93d339305d 100644 --- a/src/auth/requestHandlers.js +++ b/src/auth/requestHandlers.js @@ -12,7 +12,7 @@ module.exports = (config, User) => ({ * @returns {*} */ register: (req, res, next) => { - const usernameField = config.user.useAsUsername || 'email'; + const usernameField = config.user.auth.useAsUsername; User.register(new User({ usernameField: req.body[usernameField] }), req.body.password, (err, user) => { if (err) { @@ -36,7 +36,7 @@ module.exports = (config, User) => ({ * @returns {*} */ login: (req, res) => { - const usernameField = config.user.useAsUsername || 'email'; + const usernameField = config.user.auth.useAsUsername; const username = req.body[usernameField]; const { password } = req.body; diff --git a/src/auth/routes.js b/src/auth/routes.js index 38a4fb4171..73a4928ea6 100644 --- a/src/auth/routes.js +++ b/src/auth/routes.js @@ -19,36 +19,32 @@ const authRoutes = (config, User) => { router .route('/me') - .post(passport.authenticate(config.user.auth.strategy, { session: false }), auth.me); + .post(passport.authenticate('jwt', { session: false }), auth.me); - if (config.user.auth.passwordResets) { - router.use('', passwordResetRoutes(config.user.email, User)); - } + router.use('', passwordResetRoutes(config.email, User)); - if (config.user.auth.registration) { - router - .route(`${config.user.slug}/register`) // TODO: not sure how to incorporate url params like `:pageId` - .post(auth.register); + router + .route(`${config.user.slug}/register`) + .post(auth.register); - router - .route('/first-register') - .post((req, res, next) => { - User.countDocuments({}, (err, count) => { - if (err) res.status(500).json({ error: err }); - if (count >= 1) return res.status(403).json({ initialized: true }); - return next(); - }); - }, (req, res, next) => { - User.register(new User(req.body), req.body.password, (err) => { - if (err) { - const error = new APIError('Authentication error', httpStatus.UNAUTHORIZED); - return res.status(httpStatus.UNAUTHORIZED).json(error); - } + router + .route('/first-register') + .post((req, res, next) => { + User.countDocuments({}, (err, count) => { + if (err) res.status(500).json({ error: err }); + if (count >= 1) return res.status(403).json({ initialized: true }); + return next(); + }); + }, (req, res, next) => { + User.register(new User(req.body), req.body.password, (err) => { + if (err) { + const error = new APIError('Authentication error', httpStatus.UNAUTHORIZED); + return res.status(httpStatus.UNAUTHORIZED).json(error); + } - return next(); - }); - }, auth.login); - } + return next(); + }); + }, auth.login); return router; }; diff --git a/src/client/components/views/CreateFirstUser/index.js b/src/client/components/views/CreateFirstUser/index.js index 76d209a1b0..dab9517925 100644 --- a/src/client/components/views/CreateFirstUser/index.js +++ b/src/client/components/views/CreateFirstUser/index.js @@ -49,8 +49,8 @@ const CreateFirstUser = (props) => { const fields = [...config.user.fields]; - if (config.user.passwordIndex) { - fields.splice(config.user.passwordIndex, 0, passwordField); + if (config.user.auth.passwordIndex) { + fields.splice(config.user.auth.passwordIndex, 0, passwordField); } else { fields.push(passwordField); }