Skip to content

Commit e92c46a

Browse files
committed
Add password reset
1 parent 2090959 commit e92c46a

File tree

5 files changed

+131
-23
lines changed

5 files changed

+131
-23
lines changed

docs/api.md

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
## Node.js API
22

3-
* [App](#app-object)
4-
* [DataSource](#data-source-object)
5-
* [GeoPoint](#geopoint-object)
6-
* [Model](#model-object)
7-
* [Remote methods and hooks](#remote-methods-and-hooks)
8-
* [REST API](#rest-api)
3+
* [App](api-app.md)
4+
* [Model](api-model.md)
5+
* [Remote methods and hooks](api-model-remote.md)
6+
* [DataSource](api-datasource.md)
7+
* [GeoPoint](api-geopoint.md)
8+
* [REST API](rest.md)

docs/bundled-models.md

+47-16
Original file line numberDiff line numberDiff line change
@@ -147,31 +147,62 @@ User.afterRemote('create', function(ctx, user, next) {
147147
});
148148
```
149149

150-
#### Send Reset Password Email
150+
### Reset Password
151151

152-
Send an email to the user's supplied email address containing a link to reset their password.
152+
You can implement password reset using the `User.resetPassword` method.
153153

154-
```js
155-
User.reset(email, function(err) {
156-
console.log('email sent');
154+
Request a password reset access token.
155+
156+
**Node.js**
157+
158+
```js
159+
User.resetPassword({
160+
161+
}, function () {
162+
console.log('ready to change password');
157163
});
158164
```
159-
160-
#### Remote Password Reset
161165

162-
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.
166+
**REST**
163167

164-
```js
165-
User.settings.resetTemplate = 'reset.ejs';
166168
```
167-
168-
#### Remote Password Reset Confirmation
169+
POST
169170
170-
Confirm the password reset.
171+
/users/reset-password
172+
...
173+
{
174+
"email": "[email protected]"
175+
}
176+
...
177+
200 OK
178+
```
171179

172-
```js
173-
User.confirmReset(token, function(err) {
174-
console.log(err || 'your password was reset');
180+
You must the handle the `resetPasswordRequest` event this on the server to
181+
send a reset email containing an access token to the correct user. The
182+
example below shows a basic setup for sending the reset email.
183+
184+
```
185+
User.on('resetPasswordRequest', function (info) {
186+
console.log(info.email); // the email of the requested user
187+
console.log(info.accessToken.id); // the temp access token to allow password reset
188+
189+
// requires AccessToken.belongsTo(User)
190+
info.accessToken.user(function (err, user) {
191+
console.log(user); // the actual user
192+
var emailData = {
193+
user: user,
194+
accessToken: accessToken
195+
};
196+
197+
// this email should include a link to a page with a form to
198+
// change the password using the access token in the email
199+
Email.send({
200+
to: user.email,
201+
subject: 'Reset Your Password',
202+
text: loopback.template('reset-template.txt.ejs')(emailData),
203+
html: loopback.template('reset-template.html.ejs')(emailData)
204+
});
205+
});
175206
});
176207
```
177208

lib/models/user.js

+47
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var Model = require('../loopback').Model
1212
, LocalStrategy = require('passport-local').Strategy
1313
, BaseAccessToken = require('./access-token')
1414
, DEFAULT_TTL = 1209600 // 2 weeks in seconds
15+
, DEFAULT_RESET_PW_TTL = 15 * 60 // 15 mins in seconds
1516
, DEFAULT_MAX_TTL = 31556926; // 1 year in seconds
1617

1718
/**
@@ -235,6 +236,42 @@ User.confirm = function (uid, token, redirect, fn) {
235236
}
236237
});
237238
}
239+
240+
User.resetPassword = function(options, cb) {
241+
var UserModel = this;
242+
var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL;
243+
244+
options = options || {};
245+
if(typeof options.email === 'string') {
246+
UserModel.findOne({email: options.email}, function(err, user) {
247+
if(err) {
248+
cb(err);
249+
} else if(user) {
250+
// create a short lived access token for temp login to change password
251+
// TODO(ritch) - eventually this should only allow password change
252+
user.accessTokens.create({ttl: ttl}, function(err, accessToken) {
253+
if(err) {
254+
cb(err);
255+
} else {
256+
cb();
257+
UserModel.emit('resetPasswordRequest', {
258+
email: options.email,
259+
accessToken: accessToken
260+
});
261+
}
262+
})
263+
} else {
264+
cb();
265+
}
266+
});
267+
} else {
268+
var err = new Error('email is required');
269+
err.statusCode = 400;
270+
271+
cb(err);
272+
}
273+
}
274+
238275
/**
239276
* Setup an extended user model.
240277
*/
@@ -286,6 +323,16 @@ User.setup = function () {
286323
}
287324
);
288325

326+
loopback.remoteMethod(
327+
UserModel.resetPassword,
328+
{
329+
accepts: [
330+
{arg: 'options', type: 'object', required: true, http: {source: 'body'}}
331+
],
332+
http: {verb: 'post', path: '/reset'}
333+
}
334+
);
335+
289336
UserModel.on('attached', function () {
290337
UserModel.afterRemote('confirm', function (ctx, inst, next) {
291338
if(ctx.req) {

templates/reset-form.ejs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<form>
2+
3+
</form>

test/user.test.js

+28-1
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ describe('User', function(){
1212
User.setMaxListeners(0);
1313

1414
before(function () {
15-
debugger;
1615
User.hasMany(AccessToken, {as: 'accessTokens', foreignKey: 'userId'});
16+
AccessToken.belongsTo(User);
1717
});
1818

1919
beforeEach(function (done) {
@@ -313,4 +313,31 @@ describe('User', function(){
313313
});
314314
});
315315
});
316+
317+
describe('Password Reset', function () {
318+
describe('User.resetPassword(options, cb)', function () {
319+
it('Creates a temp accessToken to allow a user to change password', function (done) {
320+
var calledBack = false;
321+
var email = '[email protected]';
322+
323+
User.resetPassword({
324+
email: email
325+
}, function () {
326+
calledBack = true;
327+
});
328+
329+
User.once('resetPasswordRequest', function (info) {
330+
assert(info.email);
331+
assert(info.accessToken);
332+
assert(info.accessToken.id);
333+
assert.equal(info.accessToken.ttl / 60, 15);
334+
assert(calledBack);
335+
info.accessToken.user(function (err, user) {
336+
assert.equal(user.email, email);
337+
done();
338+
});
339+
});
340+
});
341+
});
342+
});
316343
});

0 commit comments

Comments
 (0)