Merge branch 'release/1.8.4' into production
This commit is contained in:
commit
be0ca7e1ce
|
@ -19,7 +19,7 @@ For more details, see http://loopback.io/.
|
||||||
In addition to the [main LoopBack module](https://github.com/strongloop/loopback), LoopBack consists of numerous other modules that implement specific functionality,
|
In addition to the [main LoopBack module](https://github.com/strongloop/loopback), LoopBack consists of numerous other modules that implement specific functionality,
|
||||||
as illustrated below:
|
as illustrated below:
|
||||||
|
|
||||||
![LoopBack modules](./docs/assets/lb-modules.png "LoopBack modules")
|
![LoopBack modules](https://github.com/strongloop/loopback/raw/master/docs/assets/lb-modules.png "LoopBack modules")
|
||||||
|
|
||||||
* Frameworks
|
* Frameworks
|
||||||
* [loopback](https://github.com/strongloop/loopback)
|
* [loopback](https://github.com/strongloop/loopback)
|
||||||
|
@ -30,6 +30,7 @@ as illustrated below:
|
||||||
* [loopback-connector-mongodb](https://github.com/strongloop/loopback-connector-mongodb)
|
* [loopback-connector-mongodb](https://github.com/strongloop/loopback-connector-mongodb)
|
||||||
* [loopback-connector-mysql](https://github.com/strongloop/loopback-connector-mysql)
|
* [loopback-connector-mysql](https://github.com/strongloop/loopback-connector-mysql)
|
||||||
* [loopback-connector-oracle](https://github.com/strongloop/loopback-connector-oracle)
|
* [loopback-connector-oracle](https://github.com/strongloop/loopback-connector-oracle)
|
||||||
|
* [loopback-connector-mssql](https://github.com/strongloop/loopback-connector-mssql)
|
||||||
* [loopback-connector-postgresql](https://github.com/strongloop/loopback-connector-postgresql)
|
* [loopback-connector-postgresql](https://github.com/strongloop/loopback-connector-postgresql)
|
||||||
* [loopback-connector-rest](https://github.com/strongloop/loopback-connector-rest)
|
* [loopback-connector-rest](https://github.com/strongloop/loopback-connector-rest)
|
||||||
* [loopback-connector-soap](https://github.com/strongloop/loopback-connector-soap)
|
* [loopback-connector-soap](https://github.com/strongloop/loopback-connector-soap)
|
||||||
|
@ -57,7 +58,7 @@ as illustrated below:
|
||||||
* [loopback-example-access-control](https://github.com/strongloop/loopback-example-access-control)
|
* [loopback-example-access-control](https://github.com/strongloop/loopback-example-access-control)
|
||||||
* [loopback-example-proxy](https://github.com/strongloop/loopback-example-proxy)
|
* [loopback-example-proxy](https://github.com/strongloop/loopback-example-proxy)
|
||||||
* [strongloop-community/loopback-example-datagraph](https://github.com/strongloop-community/loopback-example-datagraph)
|
* [strongloop-community/loopback-example-datagraph](https://github.com/strongloop-community/loopback-example-datagraph)
|
||||||
* [strongloop-community/loopback-mysql-example](https://github.com/strongloop-community/loopback-mysql-example)
|
* [strongloop-community/loopback-example-database](https://github.com/strongloop-community/loopback-example-database)
|
||||||
* [strongloop-community/loopback-examples-ios](https://github.com/strongloop-community/loopback-examples-ios)
|
* [strongloop-community/loopback-examples-ios](https://github.com/strongloop-community/loopback-examples-ios)
|
||||||
* [strongloop-community/loopback-example-ssl](https://github.com/strongloop-community/loopback-example-ssl)
|
* [strongloop-community/loopback-example-ssl](https://github.com/strongloop-community/loopback-example-ssl)
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 601 KiB |
|
@ -7,6 +7,7 @@ var DataSource = require('loopback-datasource-juggler').DataSource
|
||||||
, compat = require('./compat')
|
, compat = require('./compat')
|
||||||
, assert = require('assert')
|
, assert = require('assert')
|
||||||
, fs = require('fs')
|
, fs = require('fs')
|
||||||
|
, extend = require('util')._extend
|
||||||
, _ = require('underscore')
|
, _ = require('underscore')
|
||||||
, RemoteObjects = require('strong-remoting')
|
, RemoteObjects = require('strong-remoting')
|
||||||
, swagger = require('strong-remoting/ext/swagger')
|
, swagger = require('strong-remoting/ext/swagger')
|
||||||
|
@ -540,7 +541,10 @@ function dataSourcesFromConfig(config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelFromConfig(name, config, app) {
|
function modelFromConfig(name, config, app) {
|
||||||
var ModelCtor = require('./loopback').createModel(name, config.properties, config.options);
|
var options = buildModelOptionsFromConfig(config);
|
||||||
|
var properties = config.properties;
|
||||||
|
|
||||||
|
var ModelCtor = require('./loopback').createModel(name, properties, options);
|
||||||
var dataSource = config.dataSource;
|
var dataSource = config.dataSource;
|
||||||
|
|
||||||
if(typeof dataSource === 'string') {
|
if(typeof dataSource === 'string') {
|
||||||
|
@ -553,6 +557,26 @@ function modelFromConfig(name, config, app) {
|
||||||
return ModelCtor;
|
return ModelCtor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildModelOptionsFromConfig(config) {
|
||||||
|
var options = extend({}, config.options);
|
||||||
|
for (var key in config) {
|
||||||
|
if (['properties', 'options', 'dataSource'].indexOf(key) !== -1) {
|
||||||
|
// Skip items which have special meaning
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options[key] !== undefined) {
|
||||||
|
// When both `config.key` and `config.options.key` are set,
|
||||||
|
// use the latter one to preserve backwards compatibility
|
||||||
|
// with loopback 1.x
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
options[key] = config[key];
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
function requireDir(dir, basenames) {
|
function requireDir(dir, basenames) {
|
||||||
assert(dir, 'cannot require directory contents without directory name');
|
assert(dir, 'cannot require directory contents without directory name');
|
||||||
|
|
||||||
|
|
|
@ -15,17 +15,37 @@ module.exports = rest;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function rest() {
|
function rest() {
|
||||||
|
var tokenParser = null;
|
||||||
return function (req, res, next) {
|
return function (req, res, next) {
|
||||||
var app = req.app;
|
var app = req.app;
|
||||||
var handler = app.handler('rest');
|
var handler = app.handler('rest');
|
||||||
|
|
||||||
if(req.url === '/routes') {
|
if(req.url === '/routes') {
|
||||||
res.send(handler.adapter.allRoutes());
|
res.send(handler.adapter.allRoutes());
|
||||||
} else if(req.url === '/models') {
|
} else if(req.url === '/models') {
|
||||||
return res.send(app.remotes().toJSON());
|
return res.send(app.remotes().toJSON());
|
||||||
|
} else if (app.isAuthEnabled) {
|
||||||
|
if (!tokenParser) {
|
||||||
|
// NOTE(bajtos) It would be better to search app.models for a model
|
||||||
|
// of type AccessToken instead of searching all loopback models.
|
||||||
|
// Unfortunately that's not supported now.
|
||||||
|
// Related discussions:
|
||||||
|
// https://github.com/strongloop/loopback/pull/167
|
||||||
|
// https://github.com/strongloop/loopback/commit/f07446a
|
||||||
|
var AccessToken = loopback.getModelByType(loopback.AccessToken);
|
||||||
|
tokenParser = loopback.token({ model: AccessToken });
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenParser(req, res, function(err) {
|
||||||
|
if (err) {
|
||||||
|
next(err);
|
||||||
|
} else {
|
||||||
|
handler(req, res, next);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
handler(req, res, next);
|
handler(req, res, next);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,12 +53,14 @@ function token(options) {
|
||||||
assert(TokenModel, 'loopback.token() middleware requires a AccessToken model');
|
assert(TokenModel, 'loopback.token() middleware requires a AccessToken model');
|
||||||
|
|
||||||
return function (req, res, next) {
|
return function (req, res, next) {
|
||||||
|
if (req.accessToken !== undefined) return next();
|
||||||
TokenModel.findForRequest(req, options, function(err, token) {
|
TokenModel.findForRequest(req, options, function(err, token) {
|
||||||
if(err) return next(err);
|
if(err) return next(err);
|
||||||
if(token) {
|
if(token) {
|
||||||
req.accessToken = token;
|
req.accessToken = token;
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
|
req.accessToken = null;
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
77
package.json
77
package.json
|
@ -3,57 +3,80 @@
|
||||||
"description": "LoopBack: Open Mobile Platform for Node.js",
|
"description": "LoopBack: Open Mobile Platform for Node.js",
|
||||||
"homepage": "http://loopback.io",
|
"homepage": "http://loopback.io",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
"framework",
|
||||||
|
"web",
|
||||||
|
"API framework",
|
||||||
|
"REST API",
|
||||||
"StrongLoop",
|
"StrongLoop",
|
||||||
"LoopBack",
|
"LoopBack",
|
||||||
"Mobile",
|
"Mobile",
|
||||||
"Backend",
|
"mBaaS",
|
||||||
"Platform",
|
"restful",
|
||||||
"mBaaS"
|
"rest",
|
||||||
|
"api",
|
||||||
|
"app",
|
||||||
|
"auth",
|
||||||
|
"security",
|
||||||
|
"orm",
|
||||||
|
"database",
|
||||||
|
"oracle",
|
||||||
|
"mysql",
|
||||||
|
"nosql",
|
||||||
|
"mongo",
|
||||||
|
"mongodb",
|
||||||
|
"sqlserver",
|
||||||
|
"mssql",
|
||||||
|
"express",
|
||||||
|
"restify",
|
||||||
|
"koa",
|
||||||
|
"postgres",
|
||||||
|
"postgresql",
|
||||||
|
"soap"
|
||||||
],
|
],
|
||||||
"version": "1.8.3",
|
"version": "1.8.4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha -R spec"
|
"test": "mocha -R spec"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "~0.7.4",
|
"debug": "~0.8.1",
|
||||||
"express": "~3.4.8",
|
"express": "~3.5.0",
|
||||||
"strong-remoting": "~1.3.1",
|
"strong-remoting": "~1.4.0",
|
||||||
"inflection": "~1.3.5",
|
"inflection": "~1.3.5",
|
||||||
"passport": "~0.2.0",
|
"passport": "~0.2.0",
|
||||||
"passport-local": "~0.1.6",
|
"passport-local": "~1.0.0",
|
||||||
"nodemailer": "~0.6.0",
|
"nodemailer": "~0.6.5",
|
||||||
"ejs": "~0.8.5",
|
"ejs": "~1.0.0",
|
||||||
"bcryptjs": "~0.7.12",
|
"bcryptjs": "~0.7.12",
|
||||||
"underscore.string": "~2.3.3",
|
"underscore.string": "~2.3.3",
|
||||||
"underscore": "~1.6.0",
|
"underscore": "~1.6.0",
|
||||||
"uid2": "0.0.3",
|
"uid2": "0.0.3",
|
||||||
"async": "~0.2.10"
|
"async": "~0.9.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"loopback-datasource-juggler": "^1.4.0 < 1.6.0"
|
"loopback-datasource-juggler": ">=1.4.0 <1.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"loopback-datasource-juggler": "^1.4.0 < 1.6.0",
|
"loopback-datasource-juggler": ">=1.4.0 <1.6.0",
|
||||||
"mocha": "~1.17.1",
|
"mocha": "~1.18.0",
|
||||||
"strong-task-emitter": "0.0.x",
|
"strong-task-emitter": "0.0.x",
|
||||||
"supertest": "~0.9.0",
|
"supertest": "~0.12.1",
|
||||||
"chai": "~1.9.0",
|
"chai": "~1.9.1",
|
||||||
"loopback-testing": "~0.1.2",
|
"loopback-testing": "~0.1.2",
|
||||||
"browserify": "~3.41.0",
|
"browserify": "~4.1.5",
|
||||||
"grunt": "~0.4.2",
|
"grunt": "~0.4.5",
|
||||||
"grunt-browserify": "~1.3.1",
|
"grunt-browserify": "~2.1.0",
|
||||||
"grunt-contrib-uglify": "~0.3.2",
|
"grunt-contrib-uglify": "~0.4.0",
|
||||||
"grunt-contrib-jshint": "~0.8.0",
|
"grunt-contrib-jshint": "~0.10.0",
|
||||||
"grunt-contrib-watch": "~0.5.3",
|
"grunt-contrib-watch": "~0.6.1",
|
||||||
"karma-script-launcher": "~0.1.0",
|
"karma-script-launcher": "~0.1.0",
|
||||||
"karma-chrome-launcher": "~0.1.2",
|
"karma-chrome-launcher": "~0.1.3",
|
||||||
"karma-firefox-launcher": "~0.1.3",
|
"karma-firefox-launcher": "~0.1.3",
|
||||||
"karma-html2js-preprocessor": "~0.1.0",
|
"karma-html2js-preprocessor": "~0.1.0",
|
||||||
"karma-phantomjs-launcher": "~0.1.2",
|
"karma-phantomjs-launcher": "~0.1.4",
|
||||||
"karma": "~0.10.9",
|
"karma": "~0.12.16",
|
||||||
"karma-browserify": "~0.2.0",
|
"karma-browserify": "~0.2.0",
|
||||||
"karma-mocha": "~0.1.1",
|
"karma-mocha": "~0.1.3",
|
||||||
"grunt-karma": "~0.6.2"
|
"grunt-karma": "~0.8.3"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -32,6 +32,27 @@ describe('loopback.token(options)', function() {
|
||||||
.end(done);
|
.end(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should skip when req.token is already present', function(done) {
|
||||||
|
var tokenStub = { id: 'stub id' };
|
||||||
|
app.use(function(req, res, next) {
|
||||||
|
req.accessToken = tokenStub;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
app.use(loopback.token({ model: Token }));
|
||||||
|
app.get('/', function(req, res, next) {
|
||||||
|
res.send(req.accessToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
request(app).get('/')
|
||||||
|
.set('Authorization', this.token.id)
|
||||||
|
.expect(200)
|
||||||
|
.end(function(err, res) {
|
||||||
|
if (err) return done(err);
|
||||||
|
expect(res.body).to.eql(tokenStub);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('AccessToken', function () {
|
describe('AccessToken', function () {
|
||||||
|
|
|
@ -14,10 +14,13 @@ function checkResult(err, result) {
|
||||||
assert(!err);
|
assert(!err);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('security scopes', function () {
|
var ds = null;
|
||||||
|
before(function() {
|
||||||
|
ds = loopback.createDataSource({connector: loopback.Memory});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('security scopes', function () {
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
var ds = this.ds = loopback.createDataSource({connector: loopback.Memory});
|
|
||||||
testModel = loopback.Model.extend('testModel');
|
testModel = loopback.Model.extend('testModel');
|
||||||
ACL.attachTo(ds);
|
ACL.attachTo(ds);
|
||||||
Role.attachTo(ds);
|
Role.attachTo(ds);
|
||||||
|
@ -156,7 +159,6 @@ describe('security ACLs', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should honor defaultPermission from the model", function () {
|
it("should honor defaultPermission from the model", function () {
|
||||||
var ds = this.ds;
|
|
||||||
var Customer = ds.createModel('Customer', {
|
var Customer = ds.createModel('Customer', {
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -188,7 +190,6 @@ describe('security ACLs', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should honor static ACLs from the model", function () {
|
it("should honor static ACLs from the model", function () {
|
||||||
var ds = this.ds;
|
|
||||||
var Customer = ds.createModel('Customer', {
|
var Customer = ds.createModel('Customer', {
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -226,7 +227,6 @@ describe('security ACLs', function () {
|
||||||
it("should check access against LDL, ACL, and Role", function () {
|
it("should check access against LDL, ACL, and Role", function () {
|
||||||
// var log = console.log;
|
// var log = console.log;
|
||||||
var log = function() {};
|
var log = function() {};
|
||||||
var ds = this.ds;
|
|
||||||
|
|
||||||
// Create
|
// Create
|
||||||
User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) {
|
User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) {
|
||||||
|
|
|
@ -61,9 +61,11 @@ describe('app', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('app.model(name, properties, options)', function () {
|
describe('app.model(name, config)', function () {
|
||||||
it('Sugar for defining a fully built model', function () {
|
var app;
|
||||||
var app = loopback();
|
|
||||||
|
beforeEach(function() {
|
||||||
|
app = loopback();
|
||||||
app.boot({
|
app.boot({
|
||||||
app: {port: 3000, host: '127.0.0.1'},
|
app: {port: 3000, host: '127.0.0.1'},
|
||||||
dataSources: {
|
dataSources: {
|
||||||
|
@ -72,16 +74,39 @@ describe('app', function() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Sugar for defining a fully built model', function () {
|
||||||
app.model('foo', {
|
app.model('foo', {
|
||||||
dataSource: 'db'
|
dataSource: 'db'
|
||||||
});
|
});
|
||||||
|
|
||||||
var Foo = app.models.foo;
|
var Foo = app.models.foo;
|
||||||
var f = new Foo;
|
var f = new Foo();
|
||||||
|
|
||||||
assert(f instanceof loopback.Model);
|
assert(f instanceof loopback.Model);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('interprets extra first-level keys as options', function() {
|
||||||
|
app.model('foo', {
|
||||||
|
dataSource: 'db',
|
||||||
|
base: 'User'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(app.models.foo.definition.settings.base).to.equal('User');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers config.options.key over config.key', function() {
|
||||||
|
app.model('foo', {
|
||||||
|
dataSource: 'db',
|
||||||
|
base: 'User',
|
||||||
|
options: {
|
||||||
|
base: 'Application'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(app.models.foo.definition.settings.base).to.equal('Application');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('app.models', function() {
|
describe('app.models', function() {
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
describe('loopback.rest', function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
app.dataSource('db', { connector: loopback.Memory });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works out-of-the-box', function(done) {
|
||||||
|
app.model('MyModel', { dataSource: 'db' });
|
||||||
|
app.use(loopback.rest());
|
||||||
|
request(app).get('/mymodels')
|
||||||
|
.expect(200)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes loopback.token when necessary', function(done) {
|
||||||
|
givenUserModelWithAuth();
|
||||||
|
app.enableAuth();
|
||||||
|
app.use(loopback.rest());
|
||||||
|
|
||||||
|
givenLoggedInUser(function(err, token) {
|
||||||
|
if (err) return done(err);
|
||||||
|
expect(token).instanceOf(app.models.accessToken);
|
||||||
|
request(app).get('/users/' + token.userId)
|
||||||
|
.set('Authorization', token.id)
|
||||||
|
.expect(200)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not include loopback.token when auth not enabled', function(done) {
|
||||||
|
var User = givenUserModelWithAuth();
|
||||||
|
User.getToken = function(req, cb) {
|
||||||
|
cb(null, req.accessToken ? req.accessToken.id : null);
|
||||||
|
};
|
||||||
|
loopback.remoteMethod(User.getToken, {
|
||||||
|
accepts: [{ type: 'object', http: { source: 'req' } }],
|
||||||
|
returns: [{ type: 'object', name: 'id' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(loopback.rest());
|
||||||
|
givenLoggedInUser(function(err, token) {
|
||||||
|
if (err) return done(err);
|
||||||
|
request(app).get('/users/getToken')
|
||||||
|
.set('Authorization', token.id)
|
||||||
|
.expect(200)
|
||||||
|
.end(function(err, res) {
|
||||||
|
if (err) return done(err);
|
||||||
|
expect(res.body.id).to.equal(null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function givenUserModelWithAuth() {
|
||||||
|
// NOTE(bajtos) It is important to create a custom AccessToken model here,
|
||||||
|
// in order to overwrite the entry created by previous tests in
|
||||||
|
// the global model registry
|
||||||
|
app.model('accessToken', {
|
||||||
|
options: {
|
||||||
|
base: 'AccessToken'
|
||||||
|
},
|
||||||
|
dataSource: 'db'
|
||||||
|
});
|
||||||
|
return app.model('user', {
|
||||||
|
options: {
|
||||||
|
base: 'User',
|
||||||
|
relations: {
|
||||||
|
accessTokens: {
|
||||||
|
model: 'accessToken',
|
||||||
|
type: 'hasMany',
|
||||||
|
foreignKey: 'userId'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataSource: 'db'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function givenLoggedInUser(cb) {
|
||||||
|
var credentials = { email: 'user@example.com', password: 'pwd' };
|
||||||
|
var User = app.models.user;
|
||||||
|
User.create(credentials,
|
||||||
|
function(err, user) {
|
||||||
|
if (err) return done(err);
|
||||||
|
User.login(credentials, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
Loading…
Reference in New Issue