From e92c46a4e4864860dce0dbf40cfb23abbc2c9c7e Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Wed, 20 Nov 2013 10:59:29 -0800 Subject: [PATCH] Add password reset --- docs/api.md | 12 ++++---- docs/bundled-models.md | 63 ++++++++++++++++++++++++++++++---------- lib/models/user.js | 47 ++++++++++++++++++++++++++++++ templates/reset-form.ejs | 3 ++ test/user.test.js | 29 +++++++++++++++++- 5 files changed, 131 insertions(+), 23 deletions(-) create mode 100644 templates/reset-form.ejs diff --git a/docs/api.md b/docs/api.md index 745e9093..6d55b041 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,8 +1,8 @@ ## Node.js API - * [App](#app-object) - * [DataSource](#data-source-object) - * [GeoPoint](#geopoint-object) - * [Model](#model-object) - * [Remote methods and hooks](#remote-methods-and-hooks) - * [REST API](#rest-api) + * [App](api-app.md) + * [Model](api-model.md) + * [Remote methods and hooks](api-model-remote.md) + * [DataSource](api-datasource.md) + * [GeoPoint](api-geopoint.md) + * [REST API](rest.md) diff --git a/docs/bundled-models.md b/docs/bundled-models.md index 48d12d1f..59521cd7 100644 --- a/docs/bundled-models.md +++ b/docs/bundled-models.md @@ -147,31 +147,62 @@ User.afterRemote('create', function(ctx, user, next) { }); ``` -#### Send Reset Password Email +### Reset Password -Send an email to the user's supplied email address containing a link to reset their password. +You can implement password reset using the `User.resetPassword` method. -```js -User.reset(email, function(err) { - console.log('email sent'); +Request a password reset access token. + +**Node.js** + +```js +User.resetPassword({ + email: 'foo@bar.com' +}, function () { + console.log('ready to change password'); }); ``` - -#### Remote Password Reset -The password reset email will send users to a page rendered by loopback with fields required to reset the user's password. You may customize this template by defining a `resetTemplate` setting. +**REST** -```js -User.settings.resetTemplate = 'reset.ejs'; ``` - -#### Remote Password Reset Confirmation +POST -Confirm the password reset. + /users/reset-password + ... + { + "email": "foo@bar.com" + } + ... + 200 OK +``` -```js -User.confirmReset(token, function(err) { - console.log(err || 'your password was reset'); +You must the handle the `resetPasswordRequest` event this on the server to +send a reset email containing an access token to the correct user. The +example below shows a basic setup for sending the reset email. + +``` +User.on('resetPasswordRequest', function (info) { + console.log(info.email); // the email of the requested user + console.log(info.accessToken.id); // the temp access token to allow password reset + + // requires AccessToken.belongsTo(User) + info.accessToken.user(function (err, user) { + console.log(user); // the actual user + var emailData = { + user: user, + accessToken: accessToken + }; + + // this email should include a link to a page with a form to + // change the password using the access token in the email + Email.send({ + to: user.email, + subject: 'Reset Your Password', + text: loopback.template('reset-template.txt.ejs')(emailData), + html: loopback.template('reset-template.html.ejs')(emailData) + }); + }); }); ``` diff --git a/lib/models/user.js b/lib/models/user.js index ace3ef5a..86ad3d50 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -12,6 +12,7 @@ var Model = require('../loopback').Model , LocalStrategy = require('passport-local').Strategy , BaseAccessToken = require('./access-token') , DEFAULT_TTL = 1209600 // 2 weeks in seconds + , DEFAULT_RESET_PW_TTL = 15 * 60 // 15 mins in seconds , DEFAULT_MAX_TTL = 31556926; // 1 year in seconds /** @@ -235,6 +236,42 @@ User.confirm = function (uid, token, redirect, fn) { } }); } + +User.resetPassword = function(options, cb) { + var UserModel = this; + var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL; + + options = options || {}; + if(typeof options.email === 'string') { + UserModel.findOne({email: options.email}, function(err, user) { + if(err) { + cb(err); + } else if(user) { + // create a short lived access token for temp login to change password + // TODO(ritch) - eventually this should only allow password change + user.accessTokens.create({ttl: ttl}, function(err, accessToken) { + if(err) { + cb(err); + } else { + cb(); + UserModel.emit('resetPasswordRequest', { + email: options.email, + accessToken: accessToken + }); + } + }) + } else { + cb(); + } + }); + } else { + var err = new Error('email is required'); + err.statusCode = 400; + + cb(err); + } +} + /** * Setup an extended user model. */ @@ -286,6 +323,16 @@ User.setup = function () { } ); + loopback.remoteMethod( + UserModel.resetPassword, + { + accepts: [ + {arg: 'options', type: 'object', required: true, http: {source: 'body'}} + ], + http: {verb: 'post', path: '/reset'} + } + ); + UserModel.on('attached', function () { UserModel.afterRemote('confirm', function (ctx, inst, next) { if(ctx.req) { diff --git a/templates/reset-form.ejs b/templates/reset-form.ejs new file mode 100644 index 00000000..2e0999f9 --- /dev/null +++ b/templates/reset-form.ejs @@ -0,0 +1,3 @@ +
+ +
diff --git a/test/user.test.js b/test/user.test.js index 26b3ef90..e8a687b2 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -12,8 +12,8 @@ describe('User', function(){ User.setMaxListeners(0); before(function () { - debugger; User.hasMany(AccessToken, {as: 'accessTokens', foreignKey: 'userId'}); + AccessToken.belongsTo(User); }); beforeEach(function (done) { @@ -313,4 +313,31 @@ describe('User', function(){ }); }); }); + + describe('Password Reset', function () { + describe('User.resetPassword(options, cb)', function () { + it('Creates a temp accessToken to allow a user to change password', function (done) { + var calledBack = false; + var email = 'foo@bar.com'; + + User.resetPassword({ + email: email + }, function () { + calledBack = true; + }); + + User.once('resetPasswordRequest', function (info) { + assert(info.email); + assert(info.accessToken); + assert(info.accessToken.id); + assert.equal(info.accessToken.ttl / 60, 15); + assert(calledBack); + info.accessToken.user(function (err, user) { + assert.equal(user.email, email); + done(); + }); + }); + }); + }); + }); });