Add password reset

This commit is contained in:
Ritchie Martori 2013-11-20 10:59:29 -08:00
parent 2090959bfc
commit e92c46a4e4
5 changed files with 131 additions and 23 deletions

View File

@ -1,8 +1,8 @@
## Node.js API ## Node.js API
* [App](#app-object) * [App](api-app.md)
* [DataSource](#data-source-object) * [Model](api-model.md)
* [GeoPoint](#geopoint-object) * [Remote methods and hooks](api-model-remote.md)
* [Model](#model-object) * [DataSource](api-datasource.md)
* [Remote methods and hooks](#remote-methods-and-hooks) * [GeoPoint](api-geopoint.md)
* [REST API](#rest-api) * [REST API](rest.md)

View File

@ -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.
Request a password reset access token.
**Node.js**
```js ```js
User.reset(email, function(err) { User.resetPassword({
console.log('email sent'); email: 'foo@bar.com'
}, function () {
console.log('ready to change password');
}); });
``` ```
#### Remote Password Reset **REST**
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. ```
POST
```js /users/reset-password
User.settings.resetTemplate = 'reset.ejs'; ...
{
"email": "foo@bar.com"
}
...
200 OK
``` ```
#### Remote Password Reset Confirmation 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.
Confirm the password reset. ```
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
```js // requires AccessToken.belongsTo(User)
User.confirmReset(token, function(err) { info.accessToken.user(function (err, user) {
console.log(err || 'your password was reset'); 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)
});
});
}); });
``` ```

View File

@ -12,6 +12,7 @@ var Model = require('../loopback').Model
, LocalStrategy = require('passport-local').Strategy , LocalStrategy = require('passport-local').Strategy
, BaseAccessToken = require('./access-token') , BaseAccessToken = require('./access-token')
, DEFAULT_TTL = 1209600 // 2 weeks in seconds , 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 , 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. * 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.on('attached', function () {
UserModel.afterRemote('confirm', function (ctx, inst, next) { UserModel.afterRemote('confirm', function (ctx, inst, next) {
if(ctx.req) { if(ctx.req) {

3
templates/reset-form.ejs Normal file
View File

@ -0,0 +1,3 @@
<form>
</form>

View File

@ -12,8 +12,8 @@ describe('User', function(){
User.setMaxListeners(0); User.setMaxListeners(0);
before(function () { before(function () {
debugger;
User.hasMany(AccessToken, {as: 'accessTokens', foreignKey: 'userId'}); User.hasMany(AccessToken, {as: 'accessTokens', foreignKey: 'userId'});
AccessToken.belongsTo(User);
}); });
beforeEach(function (done) { 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();
});
});
});
});
});
}); });