Merge branch 'release/1.8.6' into production

This commit is contained in:
Raymond Feng 2014-06-09 15:04:19 -07:00
commit 093f3ed412
12 changed files with 146 additions and 796 deletions

View File

@ -3,18 +3,21 @@
"content": [
"lib/application.js",
"lib/loopback.js",
"lib/middleware/token.js",
{ "title": "Base model", "depth": 2 },
"lib/models/model.js",
"lib/models/data-model.js",
{ "title": "Middleware", "depth": 2 },
"lib/middleware/rest.js",
"lib/middleware/status.js",
"lib/middleware/token.js",
"lib/middleware/urlNotFound.js",
{ "title": "Built-in models", "depth": 2 },
"lib/models/access-token.js",
"lib/models/access-context.js",
"lib/models/acl.js",
"lib/models/application.js",
"lib/models/email.js",
"lib/models/model.js",
"lib/models/data-model.js",
"lib/models/role.js",
"lib/models/user.js",
"docs/api-model.md",
"docs/api-model-remote.md"
"lib/models/user.js"
],
"assets": "/docs/assets"
}

View File

@ -1,212 +0,0 @@
## Data Source object
LoopBack models can manipulate data via the DataSource object. Attaching a `DataSource` to a `Model` adds instance methods and static methods to the `Model`; some of the added methods may be remote methods.
Define a data source for persisting models.
```js
var oracle = loopback.createDataSource({
connector: 'oracle',
host: '111.22.333.44',
database: 'MYDB',
username: 'username',
password: 'password'
});
```
### Methods
#### dataSource.createModel(name, properties, options)
Define a model and attach it to a `DataSource`.
```js
var Color = oracle.createModel('color', {name: String});
```
You can define an ACL when you create a new data source with the `DataSource.create()` method. For example:
```js
var Customer = ds.createModel('Customer', {
name: {
type: String,
acls: [
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.WRITE, permission: ACL.DENY},
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
]
}
}, {
acls: [
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
]
});
```
#### dataSource.discoverModelDefinitions([username], fn)
Discover a set of model definitions (table or collection names) based on tables or collections in a data source.
```js
oracle.discoverModelDefinitions(function (err, models) {
models.forEach(function (def) {
// def.name ~ the model name
oracle.discoverSchema(null, def.name, function (err, schema) {
console.log(schema);
});
});
});
```
#### dataSource.discoverSchema([owner], name, fn)
Discover the schema of a specific table or collection.
**Example schema from oracle connector:**
```js
{
"name": "Product",
"options": {
"idInjection": false,
"oracle": {
"schema": "BLACKPOOL",
"table": "PRODUCT"
}
},
"properties": {
"id": {
"type": "String",
"required": true,
"length": 20,
"id": 1,
"oracle": {
"columnName": "ID",
"dataType": "VARCHAR2",
"dataLength": 20,
"nullable": "N"
}
},
"name": {
"type": "String",
"required": false,
"length": 64,
"oracle": {
"columnName": "NAME",
"dataType": "VARCHAR2",
"dataLength": 64,
"nullable": "Y"
}
},
"audibleRange": {
"type": "Number",
"required": false,
"length": 22,
"oracle": {
"columnName": "AUDIBLE_RANGE",
"dataType": "NUMBER",
"dataLength": 22,
"nullable": "Y"
}
},
"effectiveRange": {
"type": "Number",
"required": false,
"length": 22,
"oracle": {
"columnName": "EFFECTIVE_RANGE",
"dataType": "NUMBER",
"dataLength": 22,
"nullable": "Y"
}
},
"rounds": {
"type": "Number",
"required": false,
"length": 22,
"oracle": {
"columnName": "ROUNDS",
"dataType": "NUMBER",
"dataLength": 22,
"nullable": "Y"
}
},
"extras": {
"type": "String",
"required": false,
"length": 64,
"oracle": {
"columnName": "EXTRAS",
"dataType": "VARCHAR2",
"dataLength": 64,
"nullable": "Y"
}
},
"fireModes": {
"type": "String",
"required": false,
"length": 64,
"oracle": {
"columnName": "FIRE_MODES",
"dataType": "VARCHAR2",
"dataLength": 64,
"nullable": "Y"
}
}
}
}
```
#### dataSource.enableRemote(operation)
Enable remote access to a data source operation. Each [connector](#connector) has its own set of set remotely enabled and disabled operations. You can always list these by calling `dataSource.operations()`.
#### dataSource.disableRemote(operation)
Disable remote access to a data source operation. Each [connector](#connector) has its own set of set enabled and disabled operations. You can always list these by calling `dataSource.operations()`.
```js
// all rest data source operations are
// disabled by default
var oracle = loopback.createDataSource({
connector: require('loopback-connector-oracle'),
host: '...',
...
});
// or only disable it as a remote method
oracle.disableRemote('destroyAll');
```
**Notes:**
- Disabled operations will not be added to attached models.
- Disabling the remoting for a method only affects client access (it will still be available from server models).
- Data sources must enable / disable operations before attaching or creating models.
#### dataSource.operations()
List the enabled and disabled operations.
console.log(oracle.operations());
Output:
```js
{
find: {
remoteEnabled: true,
accepts: [...],
returns: [...]
enabled: true
},
save: {
remoteEnabled: true,
prototype: true,
accepts: [...],
returns: [...],
enabled: true
},
...
}
```

View File

@ -1,69 +0,0 @@
## GeoPoint object
The GeoPoint object represents a physical location.
Use the `GeoPoint` class.
```js
var GeoPoint = require('loopback').GeoPoint;
```
Embed a latitude / longitude point in a [Model](#model).
```js
var CoffeeShop = loopback.createModel('coffee-shop', {
location: 'GeoPoint'
});
```
You can query LoopBack models with a GeoPoint property and an attached data source using geo-spatial filters and sorting. For example, the following code finds the three nearest coffee shops.
```js
CoffeeShop.attachTo(oracle);
var here = new GeoPoint({lat: 10.32424, lng: 5.84978});
CoffeeShop.find({where: {location: {near: here}}, limit:3}, function(err, nearbyShops) {
console.info(nearbyShops); // [CoffeeShop, ...]
});
```
### Distance Types
**Note:** all distance methods use `miles` by default.
- `miles`
- `radians`
- `kilometers`
- `meters`
- `miles`
- `feet`
- `degrees`
### Methods
#### geoPoint.distanceTo(geoPoint, options)
Get the distance to another `GeoPoint`; for example:
```js
var here = new GeoPoint({lat: 10, lng: 10});
var there = new GeoPoint({lat: 5, lng: 5});
console.log(here.distanceTo(there, {type: 'miles'})); // 438
```
#### GeoPoint.distanceBetween(a, b, options)
Get the distance between two points; for example:
```js
GeoPoint.distanceBetween(here, there, {type: 'miles'}) // 438
```
### Properties
#### geoPoint.lat
The latitude point in degrees. Range: -90 to 90.
#### geoPoint.lng
The longitude point in degrees. Range: -180 to 180.

View File

@ -1,448 +0,0 @@
## Model object
A Loopback `Model` is a vanilla JavaScript class constructor with an attached set of properties and options. A `Model` instance is created by passing a data object containing properties to the `Model` constructor. A `Model` constructor will clean the object passed to it and only set the values matching the properties you define.
```js
// valid color
var Color = loopback.createModel('color', {name: String});
var red = new Color({name: 'red'});
console.log(red.name); // red
// invalid color
var foo = new Color({bar: 'bat baz'});
console.log(foo.bar); // undefined
```
**Properties**
A model defines a list of property names, types and other validation metadata. A [DataSource](#data-source) uses this definition to validate a `Model` during operations such as `save()`.
**Options**
Some [DataSources](#data-source) may support additional `Model` options.
Define A Loopbackmodel.
```js
var User = loopback.createModel('user', {
first: String,
last: String,
age: Number
});
```
### Methods
#### Model.attachTo(dataSource)
Attach a model to a [DataSource](#data-source). Attaching a [DataSource](#data-source) updates the model with additional methods and behaviors.
```js
var oracle = loopback.createDataSource({
connector: require('loopback-connector-oracle'),
host: '111.22.333.44',
database: 'MYDB',
username: 'username',
password: 'password'
});
User.attachTo(oracle);
```
NOTE: until a model is attached to a data source it will not have any attached methods.
### Properties
#### Model.properties
An object containing a normalized set of properties supplied to `loopback.createModel(name, properties)`.
Example:
```js
var props = {
a: String,
b: {type: 'Number'},
c: {type: 'String', min: 10, max: 100},
d: Date,
e: loopback.GeoPoint
};
var MyModel = loopback.createModel('foo', props);
console.log(MyModel.properties);
```
Outputs:
```js
{
"a": {type: String},
"b": {type: Number},
"c": {
"type": String,
"min": 10,
"max": 100
},
"d": {type: Date},
"e": {type: GeoPoint},
"id": {
"id": 1
}
}
```
### CRUD and Query Mixins
Mixins are added by attaching a vanilla model to a [data source](#data-source) with a [connector](#connectors). Each [connector](#connectors) enables its own set of operations that are mixed into a `Model` as methods. To see available methods for a data source call `dataSource.operations()`.
Log the available methods for a memory data source.
```js
var ops = loopback
.createDataSource({connector: loopback.Memory})
.operations();
console.log(Object.keys(ops));
```
Outputs:
```js
[ 'create',
'updateOrCreate',
'upsert',
'findOrCreate',
'exists',
'findById',
'find',
'all',
'findOne',
'destroyAll',
'deleteAll',
'count',
'include',
'relationNameFor',
'hasMany',
'belongsTo',
'hasAndBelongsToMany',
'save',
'isNewRecord',
'destroy',
'delete',
'updateAttribute',
'updateAttributes',
'reload' ]
```
Here is the definition of the `count()` operation.
```js
{
accepts: [ { arg: 'where', type: 'object' } ],
http: { verb: 'get', path: '/count' },
remoteEnabled: true,
name: 'count'
}
```
### Static Methods
**Note:** These are the default mixin methods for a `Model` attached to a data source. See the specific connector for additional API documentation.
#### Model.create(data, [callback])
Create an instance of Model with given data and save to the attached data source. Callback is optional.
```js
User.create({first: 'Joe', last: 'Bob'}, function(err, user) {
console.log(user instanceof User); // true
});
```
**Note:** You must include a callback and use the created model provided in the callback if your code depends on your model being saved or having an `id`.
#### Model.count([query], callback)
Query count of Model instances in data source. Optional query param allows to count filtered set of Model instances.
```js
User.count({approved: true}, function(err, count) {
console.log(count); // 2081
});
```
#### Model.find(filter, callback)
Find all instances of Model, matched by query. Fields used for filter and sort should be declared with `{index: true}` in model definition.
**filter**
- **where** `Object` { key: val, key2: {gt: 'val2'}} The search criteria
- Format: {key: val} or {key: {op: val}}
- Operations:
- gt: >
- gte: >=
- lt: <
- lte: <=
- between
- inq: IN
- nin: NOT IN
- neq: !=
- like: LIKE
- nlike: NOT LIKE
- **include** `String`, `Object` or `Array` Allows you to load relations of several objects and optimize numbers of requests.
- Format:
- 'posts': Load posts
- ['posts', 'passports']: Load posts and passports
- {'owner': 'posts'}: Load owner and owner's posts
- {'owner': ['posts', 'passports']}: Load owner, owner's posts, and owner's passports
- {'owner': [{posts: 'images'}, 'passports']}: Load owner, owner's posts, owner's posts' images, and owner's passports
- **order** `String` The sorting order
- Format: 'key1 ASC, key2 DESC'
- **limit** `Number` The maximum number of instances to be returned
- **skip** `Number` Skip the number of instances
- **offset** `Number` Alias for skip
- **fields** `Object|Array|String` The included/excluded fields
- `['foo']` or `'foo'` - include only the foo property
- `['foo', 'bar']` - include the foo and bar properties
- `{foo: true}` - include only foo
- `{bat: false}` - include all properties, exclude bat
Find the second page of 10 users over age 21 in descending order exluding the password property.
```js
User.find({
where: {
age: {gt: 21}},
order: 'age DESC',
limit: 10,
skip: 10,
fields: {password: false}
},
console.log
);
```
**Note:** See the specific connector's [docs](#connectors) for more info.
#### Model.destroyAll([where], callback)
Delete all Model instances from data source. **Note:** destroyAll method does not perform destroy hooks.
```js
Product.destroyAll({price: {gt: 99}}, function(err) {
// removed matching products
});
```
> **NOTE:* `where` is optional and a where object... do NOT pass a filter object
#### Model.findById(id, callback)
Find instance by id.
```js
User.findById(23, function(err, user) {
console.info(user.id); // 23
});
```
#### Model.findOne(where, callback)
Find a single instance that matches the given where expression.
```js
User.findOne({where: {id: 23}}, function(err, user) {
console.info(user.id); // 23
});
```
#### Model.upsert(data, callback)
Update when record with id=data.id found, insert otherwise. **Note:** no setters, validations or hooks applied when using upsert.
#### Custom static methods
Define a static model method.
```js
User.login = function (username, password, fn) {
var passwordHash = hashPassword(password);
this.findOne({username: username}, function (err, user) {
var failErr = new Error('login failed');
if(err) {
fn(err);
} else if(!user) {
fn(failErr);
} else if(user.password === passwordHash) {
MyAccessTokenModel.create({userId: user.id}, function (err, accessToken) {
fn(null, accessToken.id);
});
} else {
fn(failErr);
}
});
}
```
Setup the static model method to be exposed to clients as a [remote method](#remote-method).
```js
loopback.remoteMethod(
User.login,
{
accepts: [
{arg: 'username', type: 'string', required: true},
{arg: 'password', type: 'string', required: true}
],
returns: {arg: 'sessionId', type: 'any'},
http: {path: '/sign-in'}
}
);
```
### Instance methods
**Note:** These are the default mixin methods for a `Model` attached to a data source. See the specific connector for additional API documentation.
#### model.save([options], [callback])
Save an instance of a Model to the attached data source.
```js
var joe = new User({first: 'Joe', last: 'Bob'});
joe.save(function(err, user) {
if(user.errors) {
console.log(user.errors);
} else {
console.log(user.id);
}
});
```
#### model.updateAttributes(data, [callback])
Save specified attributes to the attached data source.
```js
user.updateAttributes({
first: 'updatedFirst',
name: 'updatedLast'
}, fn);
```
#### model.destroy([callback])
Remove a model from the attached data source.
```js
model.destroy(function(err) {
// model instance destroyed
});
```
#### Custom instance methods
Define an instance method.
```js
User.prototype.logout = function (fn) {
MySessionModel.destroyAll({userId: this.id}, fn);
}
```
Define a remote model instance method.
```js
loopback.remoteMethod(User.prototype.logout)
```
### Relationships
#### Model.hasMany(Model, options)
Define a "one to many" relationship.
```js
// by referencing model
Book.hasMany(Chapter);
// specify the name
Book.hasMany('chapters', {model: Chapter});
```
Query and create the related models.
```js
Book.create(function(err, book) {
// create a chapter instance
// ready to be saved in the data source
var chapter = book.chapters.build({name: 'Chapter 1'});
// save the new chapter
chapter.save();
// you can also call the Chapter.create method with
// the `chapters` property which will build a chapter
// instance and save the it in the data source
book.chapters.create({name: 'Chapter 2'}, function(err, savedChapter) {
// this callback is optional
});
// query chapters for the book using the
book.chapters(function(err, chapters) {
// all chapters with bookId = book.id
console.log(chapters);
});
book.chapters({where: {name: 'test'}, function(err, chapters) {
// all chapters with bookId = book.id and name = 'test'
console.log(chapters);
});
});
```
#### Model.belongsTo(Model, options)
A `belongsTo` relation sets up a one-to-one connection with another model, such
that each instance of the declaring model "belongs to" one instance of the other
model. For example, if your application includes users and posts, and each post
can be written by exactly one user.
```js
Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
```
The code above basically says Post has a reference called `author` to User using
the `userId` property of Post as the foreign key. Now we can access the author
in one of the following styles:
```js
post.author(callback); // Get the User object for the post author asynchronously
post.author(); // Get the User object for the post author synchronously
post.author(user) // Set the author to be the given user
```
#### Model.hasAndBelongsToMany(Model, options)
A `hasAndBelongsToMany` relation creates a direct many-to-many connection with
another model, with no intervening model. For example, if your application
includes users and groups, with each group having many users and each user
appearing in many groups, you could declare the models this way,
```js
User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'});
user.groups(callback); // get groups of the user
user.groups.create(data, callback); // create a new group and connect it with the user
user.groups.add(group, callback); // connect an existing group with the user
user.groups.remove(group, callback); // remove the user from the group
```
### Shared methods
Any static or instance method can be decorated as `shared`. These methods are exposed over the provided transport (eg. [loopback.rest](#rest)).

View File

@ -1,17 +1,24 @@
/**
/*!
* Module dependencies.
*/
var loopback = require('../loopback');
/**
/*!
* Export the middleware.
*/
module.exports = rest;
/**
* Build a temp app for mounting resources.
* Expose models over REST.
*
* For example:
* ```js
* app.use(loopback.rest());
* ```
* For more information, see [Exposing models over a REST API](http://docs.strongloop.com/display/DOC/Exposing+models+over+a+REST+API).
* @header loopback.rest()
*/
function rest() {

View File

@ -1,9 +1,22 @@
/**
/*!
* Export the middleware.
*/
module.exports = status;
/**
* Return [HTTP response](http://expressjs.com/4x/api.html#res.send) with basic application status information:
* date the application was started and uptime, in JSON format.
* For example:
* ```js
* {
* "started": "2014-06-05T00:26:49.750Z",
* "uptime": 9.394
* }
* ```
*
* @header loopback.status()
*/
function status() {
var started = new Date();

View File

@ -12,14 +12,17 @@ var assert = require('assert');
module.exports = token;
/**
* **Options**
* Check for an access token in cookies, headers, and query string parameters.
* This function always checks for the following:
*
* - `cookies` - An `Array` of cookie names
* - `headers` - An `Array` of header names
* - `params` - An `Array` of param names
* - `model` - Specify an AccessToken class to use
* - `access_token`
* - `X-Access-Token`
* - `authorization`
*
* It checks for these values in cookies, headers, and query string parameters _in addition_ to the items
* specified in the options parameter.
*
* Each array is used to add additional keys to find an `accessToken` for a `request`.
* **NOTE:** This function only checks for [signed cookies](http://expressjs.com/api.html#req.signedCookies).
*
* The following example illustrates how to check for an `accessToken` in a custom cookie, query string parameter
* and header called `foo-auth`.
@ -28,23 +31,16 @@ module.exports = token;
* app.use(loopback.token({
* cookies: ['foo-auth'],
* headers: ['foo-auth', 'X-Foo-Auth'],
* cookies: ['foo-auth', 'foo_auth']
* params: ['foo-auth', 'foo_auth']
* }));
* ```
*
* **Defaults**
*
* By default the following names will be checked. These names are appended to any optional names. They will always
* be checked, but any names specified will be checked first.
*
* - **access_token**
* - **X-Access-Token**
* - **authorization**
* - **access_token**
*
* **NOTE:** The `loopback.token()` middleware will only check for [signed cookies](http://expressjs.com/api.html#req.signedCookies).
*
* @header loopback.token(options)
* @options {Object} [options] Each option array is used to add additional keys to find an `accessToken` for a `request`.
* @property {Array} [cookies] Array of cookie names.
* @property {Array} [headers] Array of header names.
* @property {Array} [params] Array of param names.
* @property {Array} [model] An AccessToken object to use.
* @header loopback.token([options])
*/
function token(options) {

View File

@ -1,13 +1,14 @@
/**
/*!
* Export the middleware.
* See discussion in Connect pull request #954 for more details
* https://github.com/senchalabs/connect/pull/954.
*/
module.exports = urlNotFound;
/**
* Convert any request not handled so far to a 404 error
* to be handled by error-handling middleware.
* See discussion in Connect pull request #954 for more details
* https://github.com/senchalabs/connect/pull/954
* @header loopback.urlNotFound()
*/
function urlNotFound() {
return function raiseUrlNotFoundError(req, res, next) {

View File

@ -73,14 +73,12 @@ function convertNullToNotFoundError(ctx, cb) {
}
/**
* Create new instance of Model class, saved in database
* Create new instance of Model class, saved in database.
*
* @param data [optional]
* @param callback(err, obj)
* callback called with arguments:
*
* - err (null or Error)
* - instance (null or Model)
* @param {Object} [data] Object containing model instance data.
* @callback {Function} callback Callback function; see below.
* @param {Error|null} err Error object
* @param {Model|null} Model instance
*/
DataModel.create = function (data, callback) {
@ -100,10 +98,15 @@ setRemoting(DataModel.create, {
* @param {Function} [callback] The callback function
*/
DataModel.upsert = DataModel.updateOrCreate = function upsert(data, callback) {
DataModel.upsert = function upsert(data, callback) {
throwNotAttached(this.modelName, 'updateOrCreate');
};
/**
* Alias for upsert function.
*/
DataModel.updateOrCreate = DataModel.upsert;
// upsert ~ remoting attributes
setRemoting(DataModel.upsert, {
description: 'Update an existing model instance or insert a new one into the data source',
@ -113,8 +116,8 @@ setRemoting(DataModel.upsert, {
});
/**
* Find one record, same as `all`, limited by 1 and return object, not collection,
* if not found, create using data provided as second argument
* Find one record instance, same as `all`, limited by one and return object, not collection.
* If not found, create the record using data provided as second argument.
*
* @param {Object} query - search conditions: {where: {test: 'me'}}.
* @param {Object} data - object to create.
@ -126,7 +129,7 @@ DataModel.findOrCreate = function findOrCreate(query, data, callback) {
};
/**
* Check whether a model instance exists in database
* Check whether a model instance exists in database.
*
* @param {id} id - identifier of object (primary key value)
* @param {Function} cb - callbacl called with (err, exists: Bool)
@ -263,10 +266,15 @@ setRemoting(DataModel.count, {
});
/**
* Save instance. When instance haven't id, create method called instead.
* Triggers: validate, save, update | create
* @param options {validate: true, throws: false} [optional]
* @param callback(err, obj)
* Save instance. When instance does not have an ID, create method instead.
* Triggers: validate, save, update or create.
*
* @options [options] Options
* @property {Boolean} validate Whether to validate.
* @property {Boolean} throws
* @callback {Function} callback Callback function.
* @param {Error} err Error object
* @param {Object}
*/
DataModel.prototype.save = function (options, callback) {
@ -290,7 +298,7 @@ DataModel.prototype.save = function (options, callback) {
/**
* Determine if the data model is new.
* @returns {Boolean}
* @returns {Boolean} True if data model is new.
*/
DataModel.prototype.isNewRecord = function () {
@ -310,9 +318,8 @@ DataModel.prototype.destroy = function (cb) {
};
/**
* Update single attribute
*
* equals to `updateAttributes({name: value}, cb)
* Update single attribute.
* Equivalent to `updateAttributes({name: value}, cb)`
*
* @param {String} name - name of property
* @param {Mixed} value - value of property
@ -326,7 +333,7 @@ DataModel.prototype.updateAttribute = function updateAttribute(name, value, call
/**
* Update set of attributes
*
* this method performs validation before updating
* Performs validation before updating
*
* @trigger `validation`, `save` and `update` hooks
* @param {Object} data - data to update
@ -349,7 +356,9 @@ setRemoting(DataModel.prototype.updateAttributes, {
* Reload object from persistence
*
* @requires `id` member of `object` to be able to call `find`
* @param {Function} callback - called with (err, instance) arguments
* @callback {Function} callback Callback function
* @param {Error} err
* @param {Object} instance
*/
DataModel.prototype.reload = function reload(callback) {

View File

@ -2,14 +2,12 @@
* Module Dependencies.
*/
var Model = require('../loopback').Model
, loopback = require('../loopback')
var loopback = require('../loopback')
, Model = loopback.Model
, path = require('path')
, SALT_WORK_FACTOR = 10
, crypto = require('crypto')
, bcrypt = require('bcryptjs')
, passport = require('passport')
, 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
@ -128,6 +126,23 @@ var options = {
var User = module.exports = Model.extend('User', properties, options);
/**
* Create access token for the logged in user. This method can be overridden to
* customize how access tokens are generated
*
* @param [Number} ttl The requested ttl
* @callack {Function} cb The callback function
* @param {String|Error} err The error string or object
* @param {AccessToken} token The generated access token object
*/
User.prototype.createAccessToken = function(ttl, cb) {
var userModel = this.constructor;
ttl = Math.min(ttl || userModel.settings.ttl, userModel.settings.maxTTL);
this.accessTokens.create({
ttl: ttl
}, cb);
};
/**
* Login a user by with the given `credentials`.
*
@ -144,6 +159,7 @@ var User = module.exports = Model.extend('User', properties, options);
*/
User.login = function (credentials, include, fn) {
var self = this;
if (typeof include === 'function') {
fn = include;
include = undefined;
@ -162,7 +178,7 @@ User.login = function (credentials, include, fn) {
return fn(err);
}
this.findOne({where: query}, function(err, user) {
self.findOne({where: query}, function(err, user) {
var defaultError = new Error('login failed');
defaultError.statusCode = 401;
@ -175,9 +191,7 @@ User.login = function (credentials, include, fn) {
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)
}, function(err, token) {
user.createAccessToken(credentials.ttl, function(err, token) {
if (err) return fn(err);
if (include === 'user') {
// NOTE(bajtos) We can't set token.user here:

View File

@ -26,7 +26,7 @@
"mobile",
"mBaaS"
],
"version": "1.8.6",
"version": "1.8.7",
"scripts": {
"test": "mocha -R spec"
},
@ -35,8 +35,6 @@
"express": "~3.5.0",
"strong-remoting": "~1.5.0",
"inflection": "~1.3.5",
"passport": "~0.2.0",
"passport-local": "~1.0.0",
"nodemailer": "~0.6.5",
"ejs": "~1.0.0",
"bcryptjs": "~0.7.12",

View File

@ -1,6 +1,5 @@
var User;
var AccessToken = loopback.AccessToken;
var passport = require('passport');
var MailConnector = require('../lib/connectors/mail');
var userMemory = loopback.createDataSource({
@ -9,6 +8,7 @@ var userMemory = loopback.createDataSource({
describe('User', function(){
var validCredentials = {email: 'foo@bar.com', password: 'bar'};
var validCredentialsWithTTL = {email: 'foo@bar.com', password: 'bar', ttl: 3600};
var invalidCredentials = {email: 'foo1@bar.com', password: 'bar1'};
var incompleteCredentials = {password: 'bar1'};
@ -117,6 +117,44 @@ describe('User', function(){
done();
});
});
it('Login a user by providing credentials with TTL', function(done) {
User.login(validCredentialsWithTTL, function (err, accessToken) {
assert(accessToken.userId);
assert(accessToken.id);
assert.equal(accessToken.ttl, validCredentialsWithTTL.ttl);
assert.equal(accessToken.id.length, 64);
done();
});
});
it('Login a user using a custom createAccessToken', function(done) {
var createToken = User.prototype.createAccessToken; // Save the original method
// Override createAccessToken
User.prototype.createAccessToken = function(ttl, cb) {
// Reduce the ttl by half for testing purpose
this.accessTokens.create({ttl: ttl /2 }, cb);
};
User.login(validCredentialsWithTTL, function (err, accessToken) {
assert(accessToken.userId);
assert(accessToken.id);
assert.equal(accessToken.ttl, 1800);
assert.equal(accessToken.id.length, 64);
User.findById(accessToken.userId, function(err, user) {
user.createAccessToken(120, function (err, accessToken) {
assert(accessToken.userId);
assert(accessToken.id);
assert.equal(accessToken.ttl, 60);
assert.equal(accessToken.id.length, 64);
// Restore create access token
User.prototype.createAccessToken = createToken;
done();
});
});
});
});
it('Login a user over REST by providing credentials', function(done) {
request(app)