diff --git a/lib/models/access-token.js b/lib/models/access-token.js index acf8484e..e920cde9 100644 --- a/lib/models/access-token.js +++ b/lib/models/access-token.js @@ -17,7 +17,7 @@ var Model = require('../loopback').Model */ var properties = { - id: {type: String, generated: true, id: 1}, + id: {type: String, id: true}, ttl: {type: Number, ttl: true, default: DEFAULT_TTL}, // time to live in seconds created: {type: Date, default: function() { return new Date(); diff --git a/lib/models/user.js b/lib/models/user.js index 4ad2fde7..b6ed1273 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -18,6 +18,7 @@ var Model = require('../loopback').Model , ACL = require('./acl').ACL , assert = require('assert'); +var debug = require('debug')('loopback:user'); /** * Default User properties. */ @@ -127,10 +128,15 @@ var User = module.exports = Model.extend('User', properties, options); * @param {AccessToken} token */ -User.login = function (credentials, fn) { - var UserCtor = this; +User.login = function (credentials, include, fn) { + if (typeof include === 'function') { + fn = include; + include = undefined; + } + + include = (include || '').toLowerCase(); + var query = {}; - if(credentials.email) { query.email = credentials.email; } else if(credentials.username) { @@ -143,20 +149,36 @@ User.login = function (credentials, fn) { var defaultError = new Error('login failed'); if(err) { + debug('An error is reported from User.findOne: %j', err); fn(defaultError); } else if(user) { user.hasPassword(credentials.password, function(err, isMatch) { if(err) { + debug('An error is reported from User.hasPassword: %j', err); fn(defaultError); } else if(isMatch) { user.accessTokens.create({ ttl: Math.min(credentials.ttl || User.settings.ttl, User.settings.maxTTL) - }, fn); + }, function(err, token) { + if (err) return fn(err); + if (include === 'user') { + // NOTE(bajtos) We can't set token.user here: + // 1. token.user already exists, it's a function injected by + // "AccessToken belongsTo User" relation + // 2. ModelBaseClass.toJSON() ignores own properties, thus + // the value won't be included in the HTTP response + // See also loopback#161 and loopback#162 + token.__data.user = user; + } + fn(err, token); + }); } else { + debug('The password is invalid for user %s', query.email || query.username); fn(defaultError); } }); } else { + debug('No matching record is found for user %s', query.email || query.username); fn(defaultError); } }); @@ -386,9 +408,18 @@ User.setup = function () { UserModel.login, { accepts: [ - {arg: 'credentials', type: 'object', required: true, http: {source: 'body'}} + {arg: 'credentials', type: 'object', required: true, http: {source: 'body'}}, + {arg: 'include', type: 'string', http: {source: 'query' }, description: + 'Related objects to include in the response. ' + + 'See the description of return value for more details.'} ], - returns: {arg: 'accessToken', type: 'object', root: true}, + returns: { + arg: 'accessToken', type: 'object', root: true, description: + 'The response body contains properties of the AccessToken created on login.\n' + + 'Depending on the value of `include` parameter, the body may contain ' + + 'additional properties:\n\n' + + ' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n' + }, http: {verb: 'post'} } ); @@ -403,7 +434,10 @@ User.setup = function () { var tokenID = accessToken && accessToken.id; return tokenID; - }} + }, description: + 'Do not supply this argument, it is automatically extracted ' + + 'from request headers.' + } ], http: {verb: 'all'} } diff --git a/package.json b/package.json index cd766b48..d504b4f6 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "Platform", "mBaaS" ], - "version": "1.6.0", + "version": "1.6.1", "scripts": { "test": "mocha -R spec" }, diff --git a/test/user.test.js b/test/user.test.js index 20271335..9a056180 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -8,6 +8,7 @@ var userMemory = loopback.createDataSource({ }); describe('User', function(){ + var validCredentials = {email: 'foo@bar.com', password: 'bar'}; beforeEach(function() { User = loopback.User.extend('user'); User.email = loopback.Email.extend('email'); @@ -25,7 +26,7 @@ describe('User', function(){ app.use(loopback.rest()); app.model(User); - User.create({email: 'foo@bar.com', password: 'bar'}, done); + User.create(validCredentials, done); }); afterEach(function (done) { @@ -105,7 +106,7 @@ describe('User', function(){ describe('User.login', function() { it('Login a user by providing credentials', function(done) { - User.login({email: 'foo@bar.com', password: 'bar'}, function (err, accessToken) { + User.login(validCredentials, function (err, accessToken) { assert(accessToken.userId); assert(accessToken.id); assert.equal(accessToken.id.length, 64); @@ -119,7 +120,7 @@ describe('User', function(){ .post('/users/login') .expect('Content-Type', /json/) .expect(200) - .send({email: 'foo@bar.com', password: 'bar'}) + .send(validCredentials) .end(function(err, res){ if(err) return done(err); var accessToken = res.body; @@ -127,11 +128,28 @@ describe('User', function(){ assert(accessToken.userId); assert(accessToken.id); assert.equal(accessToken.id.length, 64); + assert(accessToken.user === undefined); done(); }); }); - + + it('Returns current user when `include` is `USER`', function(done) { + request(app) + .post('/users/login?include=USER') + .send(validCredentials) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if (err) return done(err); + var token = res.body; + expect(token.user, 'body.user').to.not.equal(undefined); + expect(token.user, 'body.user') + .to.have.property('email', validCredentials.email); + done(); + }); + }); + it('Login should only allow correct credentials', function(done) { User.create({email: 'foo22@bar.com', password: 'bar'}, function(user, err) { User.login({email: 'foo44@bar.com', password: 'bar'}, function(err, accessToken) {