diff --git a/common/models/user.js b/common/models/user.js index c51709a406..36476e5405 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -83,6 +83,39 @@ module.exports = function(User) { next(); }); + User.on('resetPasswordRequest', function(info) { + const host = User.app.get('host'); + // TODO(berks) get protocol as well + const url = `http://${host}/reset-password?access_token=` + + info.accessToken.id; + + // the email of the requested user + debug(info.email); + // the temp access token to allow password reset + debug(info.accessToken.id); + // requires AccessToken.belongsTo(User) + var mailOptions = { + to: info.email, + from: 'Team@freecodecamp.com', + subject: 'Password Reset Request', + text: ` + Hello,\n\n + This email is confirming that you requested to + reset your password for your Free Code Camp account. + This is your email: ${ info.email }. + Go to ${ url } to reset your password. + \n + Happy Coding! + \n + ` + }; + + User.app.models.Email.send(mailOptions, function(err) { + if (err) { console.error(err); } + debug('email reset sent'); + }); + }); + User.afterRemote('login', function(ctx, user, next) { var res = ctx.res; var req = ctx.req; diff --git a/server/boot/user.js b/server/boot/user.js index 3751bc837b..1fe0df0584 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -1,11 +1,8 @@ var _ = require('lodash'), async = require('async'), - crypto = require('crypto'), - nodemailer = require('nodemailer'), moment = require('moment'), - // debug = require('debug')('freecc:cntr:userController'), + debug = require('debug')('freecc:cntr:userController'); - secrets = require('../../config/secrets'); function calcCurrentStreak(cals) { const revCals = cals.slice().reverse(); @@ -40,8 +37,8 @@ module.exports = function(app) { router.get('/signout', signout); router.get('/forgot', getForgot); router.post('/forgot', postForgot); - router.get('/reset/:token', getReset); - router.post('/reset/:token', postReset); + router.get('/reset-password', getReset); + router.post('/reset-password', postReset); router.get('/email-signup', getEmailSignup); router.get('/email-signin', getEmailSignin); router.get('/account/api', getAccountAngular); @@ -182,11 +179,6 @@ module.exports = function(app) { ); } - /** - * POST /account/password - * Update current password. - */ - function postUpdatePassword(req, res, next) { req.assert('password', 'Password must be at least 4 characters long') .len(4); @@ -215,11 +207,6 @@ module.exports = function(app) { }); } - /** - * POST /account/delete - * Delete user account. - */ - function postDeleteAccount(req, res, next) { User.destroyById(req.user.id, function(err) { if (err) { return next(err); } @@ -229,11 +216,6 @@ module.exports = function(app) { }); } - /** - * GET /account/unlink/:provider - * Unlink OAuth provider. - */ - function getOauthUnlink(req, res, next) { var provider = req.params.provider; User.findById(req.user.id, function(err, user) { @@ -253,120 +235,38 @@ module.exports = function(app) { }); } - /** - * GET /reset/:token - * Reset Password page. - */ - - function getReset(req, res, next) { - if (req.isAuthenticated()) { - return res.redirect('/'); + function getReset(req, res) { + if (!req.accessToken) { + req.flash('errors', { msg: 'access token invalid' }); + return res.render('account/forgot'); } - User.findOne( - { - where: { - resetPasswordToken: req.params.token, - resetPasswordExpires: { gte: Date.now() } - } - }, - function(err, user) { - if (err) { return next(err); } - if (!user) { - req.flash('errors', { - msg: 'Password reset token is invalid or has expired.' - }); - return res.redirect('/forgot'); - } - res.render('account/reset', { - title: 'Password Reset', - token: req.params.token - }); - }); + res.render('account/reset', { + title: 'Password Reset', + accessToken: req.accessToken.id + }); } - /** - * POST /reset/:token - * Process the reset password request. - */ - function postReset(req, res, next) { - var errors = req.validationErrors(); + const errors = req.validationErrors(); + const { password } = req.body; if (errors) { req.flash('errors', errors); return res.redirect('back'); } - async.waterfall([ - function(done) { - User.findOne( - { - where: { - resetPasswordToken: req.params.token, - resetPasswordExpires: { gte: Date.now() } - } - }, - function(err, user) { - if (err) { return next(err); } - if (!user) { - req.flash('errors', { - msg: 'Password reset token is invalid or has expired.' - }); - return res.redirect('back'); - } - - user.password = req.body.password; - user.resetPasswordToken = null; - user.resetPasswordExpires = null; - - user.save(function(err) { - if (err) { return done(err); } - req.logIn(user, function(err) { - done(err, user); - }); - }); - }); - }, - function(user, done) { - var transporter = nodemailer.createTransport({ - service: 'Mandrill', - auth: { - user: secrets.mandrill.user, - pass: secrets.mandrill.password - } - }); - var mailOptions = { - to: user.email, - from: 'Team@freecodecamp.com', - subject: 'Your Free Code Camp password has been changed', - text: [ - 'Hello,\n\n', - 'This email is confirming that you requested to', - 'reset your password for your Free Code Camp account.', - 'This is your email:', - user.email, - '\n' - ].join(' ') - }; - transporter.sendMail(mailOptions, function(err) { - if (err) { return done(err); } - req.flash('success', { - msg: 'Success! Your password has been changed.' - }); - done(); - }); - } - ], function(err) { + User.findById(req.accessToken.userId, function(err, user) { if (err) { return next(err); } - res.redirect('/'); + user.updateAttribute('password', password, function(err) { + if (err) { return next(err); } + + debug('password reset processed successfully'); + req.flash('info', { msg: 'password reset processed successfully' }); + res.redirect('/'); + }); }); } - /** - * GET /forgot - * Forgot Password page. - */ - function getForgot(req, res) { if (req.isAuthenticated()) { return res.redirect('/'); @@ -381,83 +281,29 @@ module.exports = function(app) { * Create a random token, then the send user an email with a reset link. */ - function postForgot(req, res, next) { - var errors = req.validationErrors(); + function postForgot(req, res) { + const errors = req.validationErrors(); + const email = req.body.email.toLowerCase(); if (errors) { req.flash('errors', errors); return res.redirect('/forgot'); } - async.waterfall([ - function(done) { - crypto.randomBytes(16, function(err, buf) { - if (err) { return done(err); } - var token = buf.toString('hex'); - done(null, token); - }); - }, - function(token, done) { - User.findOne({ - where: { email: req.body.email.toLowerCase() } - }, function(err, user) { - if (err) { return done(err); } - if (!user) { - req.flash('errors', { - msg: 'No account with that email address exists.' - }); - return res.redirect('/forgot'); - } - - user.resetPasswordToken = token; - // 3600000 = 1 hour - user.resetPasswordExpires = Date.now() + 3600000; - - user.save(function(err) { - if (err) { return done(err); } - done(null, token, user); - }); - }); - }, - function(token, user, done) { - var transporter = nodemailer.createTransport({ - service: 'Mandrill', - auth: { - user: secrets.mandrill.user, - pass: secrets.mandrill.password - } - }); - var mailOptions = { - to: user.email, - from: 'Team@freecodecamp.com', - subject: 'Reset your Free Code Camp password', - text: [ - 'You are receiving this email because you (or someone else)\n', - 'requested we reset your Free Code Camp account\'s password.\n\n', - 'Please click on the following link, or paste this into your\n', - 'browser to complete the process:\n\n', - 'http://', - req.headers.host, - '/reset/', - token, - '\n\n', - 'If you did not request this, please ignore this email and\n', - 'your password will remain unchanged.\n' - ].join('') - }; - transporter.sendMail(mailOptions, function(err) { - if (err) { return done(err); } - req.flash('info', { - msg: 'An e-mail has been sent to ' + - user.email + - ' with further instructions.' - }); - done(null, 'done'); - }); + User.resetPassword({ + email: email + }, function(err) { + if (err) { + req.flash('errors', err); + return res.redirect('/forgot'); } - ], function(err) { - if (err) { return next(err); } - res.redirect('/forgot'); + + req.flash('info', { + msg: 'An e-mail has been sent to ' + + email + + ' with further instructions.' + }); + res.render('account/forgot'); }); } diff --git a/server/server.js b/server/server.js index 6c16c224ab..49b5825522 100755 --- a/server/server.js +++ b/server/server.js @@ -27,6 +27,7 @@ var passportConfigurator = new PassportConfigurator(app); app.set('port', process.env.PORT || 3000); app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); +app.use(loopback.token()); app.disable('x-powered-by'); // adds passport initialization after session middleware phase is complete diff --git a/server/views/account/reset.jade b/server/views/account/reset.jade index cfea79e93f..c82eeb1cca 100644 --- a/server/views/account/reset.jade +++ b/server/views/account/reset.jade @@ -2,7 +2,7 @@ extends ../layout block content .col-sm-8.col-sm-offset-2.jumbotron - form(action='/reset/#{token}', method='POST') + form(action='/reset-password?access_token=#{accessToken}', method='POST') h1 Reset Password input(type='hidden', name='_csrf', value=_csrf) .form-group