revises and simplifies forgot password routes

This commit is contained in:
James
2020-03-29 19:58:52 -04:00
parent bead4d51bc
commit fc874be72a
4 changed files with 158 additions and 168 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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);
}