Merge pull request #635 from strongloop/feature/builtin-models-defined-via-json
Define built-in models via JSON
This commit is contained in:
commit
dcedfa03a1
|
@ -4,24 +4,8 @@
|
||||||
|
|
||||||
var loopback = require('../../lib/loopback')
|
var loopback = require('../../lib/loopback')
|
||||||
, assert = require('assert')
|
, assert = require('assert')
|
||||||
, crypto = require('crypto')
|
|
||||||
, uid = require('uid2')
|
, uid = require('uid2')
|
||||||
, DEFAULT_TTL = 1209600 // 2 weeks in seconds
|
, DEFAULT_TOKEN_LEN = 64;
|
||||||
, DEFAULT_TOKEN_LEN = 64
|
|
||||||
, Role = require('./role').Role
|
|
||||||
, ACL = require('./acl').ACL;
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Default AccessToken properties.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var properties = {
|
|
||||||
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();
|
|
||||||
}}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token based authentication and access control.
|
* Token based authentication and access control.
|
||||||
|
@ -32,38 +16,22 @@ var properties = {
|
||||||
* - ALLOW EVERYONE create
|
* - ALLOW EVERYONE create
|
||||||
*
|
*
|
||||||
* @property {String} id Generated token ID
|
* @property {String} id Generated token ID
|
||||||
* @property {Number} ttl Time to live in seconds
|
* @property {Number} ttl Time to live in seconds, 2 weeks by default.
|
||||||
* @property {Date} created When the token was created
|
* @property {Date} created When the token was created
|
||||||
*
|
*
|
||||||
* @class
|
* @class AccessToken
|
||||||
* @inherits {PersistedModel}
|
* @inherits {PersistedModel}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var AccessToken = module.exports =
|
module.exports = function(AccessToken) {
|
||||||
loopback.PersistedModel.extend('AccessToken', properties, {
|
|
||||||
acls: [
|
|
||||||
{
|
|
||||||
principalType: ACL.ROLE,
|
|
||||||
principalId: Role.EVERYONE,
|
|
||||||
permission: 'DENY'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
principalType: ACL.ROLE,
|
|
||||||
principalId: Role.EVERYONE,
|
|
||||||
property: 'create',
|
|
||||||
permission: 'ALLOW'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
relations: {
|
|
||||||
user: {
|
|
||||||
type: 'belongsTo',
|
|
||||||
model: 'User',
|
|
||||||
foreignKey: 'userId'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||||
|
AccessToken.definition.rawProperties.created.default =
|
||||||
|
AccessToken.definition.properties.created.default = function() {
|
||||||
|
return new Date();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
* Anonymous Token
|
* Anonymous Token
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
|
@ -71,9 +39,9 @@ var AccessToken = module.exports =
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
AccessToken.ANONYMOUS = new AccessToken({id: '$anonymous'});
|
AccessToken.ANONYMOUS = new AccessToken({id: '$anonymous'});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a cryptographically random access token id.
|
* Create a cryptographically random access token id.
|
||||||
*
|
*
|
||||||
* @callback {Function} callback
|
* @callback {Function} callback
|
||||||
|
@ -81,25 +49,25 @@ AccessToken.ANONYMOUS = new AccessToken({id: '$anonymous'});
|
||||||
* @param {String} token
|
* @param {String} token
|
||||||
*/
|
*/
|
||||||
|
|
||||||
AccessToken.createAccessTokenId = function (fn) {
|
AccessToken.createAccessTokenId = function(fn) {
|
||||||
uid(this.settings.accessTokenIdLength || DEFAULT_TOKEN_LEN, function(err, guid) {
|
uid(this.settings.accessTokenIdLength || DEFAULT_TOKEN_LEN, function(err, guid) {
|
||||||
if(err) {
|
if (err) {
|
||||||
fn(err);
|
fn(err);
|
||||||
} else {
|
} else {
|
||||||
fn(null, guid);
|
fn(null, guid);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Hook to create accessToken id.
|
* Hook to create accessToken id.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
AccessToken.beforeCreate = function (next, data) {
|
AccessToken.beforeCreate = function(next, data) {
|
||||||
data = data || {};
|
data = data || {};
|
||||||
|
|
||||||
AccessToken.createAccessTokenId(function (err, id) {
|
AccessToken.createAccessTokenId(function(err, id) {
|
||||||
if(err) {
|
if (err) {
|
||||||
next(err);
|
next(err);
|
||||||
} else {
|
} else {
|
||||||
data.id = id;
|
data.id = id;
|
||||||
|
@ -107,9 +75,9 @@ AccessToken.beforeCreate = function (next, data) {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a token for the given `ServerRequest`.
|
* Find a token for the given `ServerRequest`.
|
||||||
*
|
*
|
||||||
* @param {ServerRequest} req
|
* @param {ServerRequest} req
|
||||||
|
@ -119,18 +87,18 @@ AccessToken.beforeCreate = function (next, data) {
|
||||||
* @param {AccessToken} token
|
* @param {AccessToken} token
|
||||||
*/
|
*/
|
||||||
|
|
||||||
AccessToken.findForRequest = function(req, options, cb) {
|
AccessToken.findForRequest = function(req, options, cb) {
|
||||||
var id = tokenIdForRequest(req, options);
|
var id = tokenIdForRequest(req, options);
|
||||||
|
|
||||||
if(id) {
|
if (id) {
|
||||||
this.findById(id, function(err, token) {
|
this.findById(id, function(err, token) {
|
||||||
if(err) {
|
if (err) {
|
||||||
cb(err);
|
cb(err);
|
||||||
} else if(token) {
|
} else if (token) {
|
||||||
token.validate(function(err, isValid) {
|
token.validate(function(err, isValid) {
|
||||||
if(err) {
|
if (err) {
|
||||||
cb(err);
|
cb(err);
|
||||||
} else if(isValid) {
|
} else if (isValid) {
|
||||||
cb(null, token);
|
cb(null, token);
|
||||||
} else {
|
} else {
|
||||||
var e = new Error('Invalid Access Token');
|
var e = new Error('Invalid Access Token');
|
||||||
|
@ -147,9 +115,9 @@ AccessToken.findForRequest = function(req, options, cb) {
|
||||||
cb();
|
cb();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate the token.
|
* Validate the token.
|
||||||
*
|
*
|
||||||
* @callback {Function} callback
|
* @callback {Function} callback
|
||||||
|
@ -157,7 +125,7 @@ AccessToken.findForRequest = function(req, options, cb) {
|
||||||
* @param {Boolean} isValid
|
* @param {Boolean} isValid
|
||||||
*/
|
*/
|
||||||
|
|
||||||
AccessToken.prototype.validate = function(cb) {
|
AccessToken.prototype.validate = function(cb) {
|
||||||
try {
|
try {
|
||||||
assert(
|
assert(
|
||||||
this.created && typeof this.created.getTime === 'function',
|
this.created && typeof this.created.getTime === 'function',
|
||||||
|
@ -173,19 +141,19 @@ AccessToken.prototype.validate = function(cb) {
|
||||||
var secondsToLive = this.ttl;
|
var secondsToLive = this.ttl;
|
||||||
var isValid = elapsedSeconds < secondsToLive;
|
var isValid = elapsedSeconds < secondsToLive;
|
||||||
|
|
||||||
if(isValid) {
|
if (isValid) {
|
||||||
cb(null, isValid);
|
cb(null, isValid);
|
||||||
} else {
|
} else {
|
||||||
this.destroy(function(err) {
|
this.destroy(function(err) {
|
||||||
cb(err, isValid);
|
cb(err, isValid);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
cb(e);
|
cb(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function tokenIdForRequest(req, options) {
|
function tokenIdForRequest(req, options) {
|
||||||
var params = options.params || [];
|
var params = options.params || [];
|
||||||
var headers = options.headers || [];
|
var headers = options.headers || [];
|
||||||
var cookies = options.cookies || [];
|
var cookies = options.cookies || [];
|
||||||
|
@ -197,18 +165,18 @@ function tokenIdForRequest(req, options) {
|
||||||
headers = headers.concat(['X-Access-Token', 'authorization']);
|
headers = headers.concat(['X-Access-Token', 'authorization']);
|
||||||
cookies = cookies.concat(['access_token', 'authorization']);
|
cookies = cookies.concat(['access_token', 'authorization']);
|
||||||
|
|
||||||
for(length = params.length; i < length; i++) {
|
for (length = params.length; i < length; i++) {
|
||||||
id = req.param(params[i]);
|
id = req.param(params[i]);
|
||||||
|
|
||||||
if(typeof id === 'string') {
|
if (typeof id === 'string') {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for(i = 0, length = headers.length; i < length; i++) {
|
for (i = 0, length = headers.length; i < length; i++) {
|
||||||
id = req.header(headers[i]);
|
id = req.header(headers[i]);
|
||||||
|
|
||||||
if(typeof id === 'string') {
|
if (typeof id === 'string') {
|
||||||
// Add support for oAuth 2.0 bearer token
|
// Add support for oAuth 2.0 bearer token
|
||||||
// http://tools.ietf.org/html/rfc6750
|
// http://tools.ietf.org/html/rfc6750
|
||||||
if (id.indexOf('Bearer ') === 0) {
|
if (id.indexOf('Bearer ') === 0) {
|
||||||
|
@ -221,14 +189,15 @@ function tokenIdForRequest(req, options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(req.signedCookies) {
|
if (req.signedCookies) {
|
||||||
for(i = 0, length = cookies.length; i < length; i++) {
|
for (i = 0, length = cookies.length; i < length; i++) {
|
||||||
id = req.signedCookies[cookies[i]];
|
id = req.signedCookies[cookies[i]];
|
||||||
|
|
||||||
if(typeof id === 'string') {
|
if (typeof id === 'string') {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"name": "AccessToken",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"id": true
|
||||||
|
},
|
||||||
|
"ttl": {
|
||||||
|
"type": "number",
|
||||||
|
"ttl": true,
|
||||||
|
"default": 1209600,
|
||||||
|
"description": "time to live in seconds (2 weeks by default)"
|
||||||
|
},
|
||||||
|
"created": {
|
||||||
|
"type": "Date"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"user": {
|
||||||
|
"type": "belongsTo",
|
||||||
|
"model": "User",
|
||||||
|
"foreignKey": "userId"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"acls": [
|
||||||
|
{
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"permission": "DENY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"property": "create",
|
||||||
|
"permission": "ALLOW"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -41,10 +41,12 @@ var AccessContext = ctx.AccessContext;
|
||||||
var Principal = ctx.Principal;
|
var Principal = ctx.Principal;
|
||||||
var AccessRequest = ctx.AccessRequest;
|
var AccessRequest = ctx.AccessRequest;
|
||||||
|
|
||||||
var role = require('./role');
|
var Role = loopback.Role;
|
||||||
var Role = role.Role;
|
assert(Role, 'Role model must be defined before ACL model');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* A Model for access control meta data.
|
||||||
|
*
|
||||||
* System grants permissions to principals (users/applications, can be grouped
|
* System grants permissions to principals (users/applications, can be grouped
|
||||||
* into roles).
|
* into roles).
|
||||||
*
|
*
|
||||||
|
@ -54,18 +56,6 @@ var Role = role.Role;
|
||||||
* For a given principal, such as client application and/or user, is it allowed
|
* For a given principal, such as client application and/or user, is it allowed
|
||||||
* to access (read/write/execute)
|
* to access (read/write/execute)
|
||||||
* the protected resource?
|
* the protected resource?
|
||||||
*/
|
|
||||||
var ACLSchema = {
|
|
||||||
model: String, // The name of the model
|
|
||||||
property: String, // The name of the property, method, scope, or relation
|
|
||||||
accessType: String,
|
|
||||||
permission: String,
|
|
||||||
principalType: String,
|
|
||||||
principalId: String
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Model for access control meta data.
|
|
||||||
*
|
*
|
||||||
* @header ACL
|
* @header ACL
|
||||||
* @property {String} model Name of the model.
|
* @property {String} model Name of the model.
|
||||||
|
@ -78,36 +68,37 @@ var ACLSchema = {
|
||||||
* - DENY: Explicitly denies access to the resource.
|
* - DENY: Explicitly denies access to the resource.
|
||||||
* @property {String} principalType Type of the principal; one of: Application, Use, Role.
|
* @property {String} principalType Type of the principal; one of: Application, Use, Role.
|
||||||
* @property {String} principalId ID of the principal - such as appId, userId or roleId
|
* @property {String} principalId ID of the principal - such as appId, userId or roleId
|
||||||
* @class
|
*
|
||||||
* @inherits Model
|
* @class ACL
|
||||||
|
* @inherits PersistedModel
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var ACL = loopback.PersistedModel.extend('ACL', ACLSchema);
|
module.exports = function(ACL) {
|
||||||
|
|
||||||
ACL.ALL = AccessContext.ALL;
|
ACL.ALL = AccessContext.ALL;
|
||||||
|
|
||||||
ACL.DEFAULT = AccessContext.DEFAULT; // Not specified
|
ACL.DEFAULT = AccessContext.DEFAULT; // Not specified
|
||||||
ACL.ALLOW = AccessContext.ALLOW; // Allow
|
ACL.ALLOW = AccessContext.ALLOW; // Allow
|
||||||
ACL.ALARM = AccessContext.ALARM; // Warn - send an alarm
|
ACL.ALARM = AccessContext.ALARM; // Warn - send an alarm
|
||||||
ACL.AUDIT = AccessContext.AUDIT; // Audit - record the access
|
ACL.AUDIT = AccessContext.AUDIT; // Audit - record the access
|
||||||
ACL.DENY = AccessContext.DENY; // Deny
|
ACL.DENY = AccessContext.DENY; // Deny
|
||||||
|
|
||||||
ACL.READ = AccessContext.READ; // Read operation
|
ACL.READ = AccessContext.READ; // Read operation
|
||||||
ACL.WRITE = AccessContext.WRITE; // Write operation
|
ACL.WRITE = AccessContext.WRITE; // Write operation
|
||||||
ACL.EXECUTE = AccessContext.EXECUTE; // Execute operation
|
ACL.EXECUTE = AccessContext.EXECUTE; // Execute operation
|
||||||
|
|
||||||
ACL.USER = Principal.USER;
|
ACL.USER = Principal.USER;
|
||||||
ACL.APP = ACL.APPLICATION = Principal.APPLICATION;
|
ACL.APP = ACL.APPLICATION = Principal.APPLICATION;
|
||||||
ACL.ROLE = Principal.ROLE;
|
ACL.ROLE = Principal.ROLE;
|
||||||
ACL.SCOPE = Principal.SCOPE;
|
ACL.SCOPE = Principal.SCOPE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the matching score for the given rule and request
|
* Calculate the matching score for the given rule and request
|
||||||
* @param {ACL} rule The ACL entry
|
* @param {ACL} rule The ACL entry
|
||||||
* @param {AccessRequest} req The request
|
* @param {AccessRequest} req The request
|
||||||
* @returns {Number}
|
* @returns {Number}
|
||||||
*/
|
*/
|
||||||
ACL.getMatchingScore = function getMatchingScore(rule, req) {
|
ACL.getMatchingScore = function getMatchingScore(rule, req) {
|
||||||
var props = ['model', 'property', 'accessType'];
|
var props = ['model', 'property', 'accessType'];
|
||||||
var score = 0;
|
var score = 0;
|
||||||
|
|
||||||
|
@ -139,7 +130,7 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) {
|
||||||
// - other
|
// - other
|
||||||
// user > app > role > ...
|
// user > app > role > ...
|
||||||
score = score * 4;
|
score = score * 4;
|
||||||
switch(rule.principalType) {
|
switch (rule.principalType) {
|
||||||
case ACL.USER:
|
case ACL.USER:
|
||||||
score += 4;
|
score += 4;
|
||||||
break;
|
break;
|
||||||
|
@ -150,14 +141,14 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) {
|
||||||
score += 2;
|
score += 2;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
score +=1;
|
score += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Weigh against the roles
|
// Weigh against the roles
|
||||||
// everyone < authenticated/unauthenticated < related < owner < ...
|
// everyone < authenticated/unauthenticated < related < owner < ...
|
||||||
score = score * 8;
|
score = score * 8;
|
||||||
if(rule.principalType === ACL.ROLE) {
|
if (rule.principalType === ACL.ROLE) {
|
||||||
switch(rule.principalId) {
|
switch (rule.principalId) {
|
||||||
case Role.OWNER:
|
case Role.OWNER:
|
||||||
score += 4;
|
score += 4;
|
||||||
break;
|
break;
|
||||||
|
@ -178,30 +169,30 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) {
|
||||||
score = score * 4;
|
score = score * 4;
|
||||||
score += AccessContext.permissionOrder[rule.permission || ACL.ALLOW] - 1;
|
score += AccessContext.permissionOrder[rule.permission || ACL.ALLOW] - 1;
|
||||||
return score;
|
return score;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get matching score for the given `AccessRequest`.
|
* Get matching score for the given `AccessRequest`.
|
||||||
* @param {AccessRequest} req The request
|
* @param {AccessRequest} req The request
|
||||||
* @returns {Number} score
|
* @returns {Number} score
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ACL.prototype.score = function(req) {
|
ACL.prototype.score = function(req) {
|
||||||
return this.constructor.getMatchingScore(this, req);
|
return this.constructor.getMatchingScore(this, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Resolve permission from the ACLs
|
* Resolve permission from the ACLs
|
||||||
* @param {Object[]) acls The list of ACLs
|
* @param {Object[]) acls The list of ACLs
|
||||||
* @param {Object} req The request
|
* @param {Object} req The request
|
||||||
* @returns {AccessRequest} result The effective ACL
|
* @returns {AccessRequest} result The effective ACL
|
||||||
*/
|
*/
|
||||||
ACL.resolvePermission = function resolvePermission(acls, req) {
|
ACL.resolvePermission = function resolvePermission(acls, req) {
|
||||||
if(!(req instanceof AccessRequest)) {
|
if (!(req instanceof AccessRequest)) {
|
||||||
req = new AccessRequest(req);
|
req = new AccessRequest(req);
|
||||||
}
|
}
|
||||||
// Sort by the matching score in descending order
|
// Sort by the matching score in descending order
|
||||||
acls = acls.sort(function (rule1, rule2) {
|
acls = acls.sort(function(rule1, rule2) {
|
||||||
return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req);
|
return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req);
|
||||||
});
|
});
|
||||||
var permission = ACL.DEFAULT;
|
var permission = ACL.DEFAULT;
|
||||||
|
@ -218,19 +209,19 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
|
||||||
permission = acls[i].permission;
|
permission = acls[i].permission;
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
if(req.exactlyMatches(acls[i])) {
|
if (req.exactlyMatches(acls[i])) {
|
||||||
permission = acls[i].permission;
|
permission = acls[i].permission;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// For wildcard match, find the strongest permission
|
// For wildcard match, find the strongest permission
|
||||||
if(AccessContext.permissionOrder[acls[i].permission]
|
if (AccessContext.permissionOrder[acls[i].permission]
|
||||||
> AccessContext.permissionOrder[permission]) {
|
> AccessContext.permissionOrder[permission]) {
|
||||||
permission = acls[i].permission;
|
permission = acls[i].permission;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(debug.enabled) {
|
if (debug.enabled) {
|
||||||
debug('The following ACLs were searched: ');
|
debug('The following ACLs were searched: ');
|
||||||
acls.forEach(function(acl) {
|
acls.forEach(function(acl) {
|
||||||
acl.debug();
|
acl.debug();
|
||||||
|
@ -241,20 +232,20 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
|
||||||
var res = new AccessRequest(req.model, req.property, req.accessType,
|
var res = new AccessRequest(req.model, req.property, req.accessType,
|
||||||
permission || ACL.DEFAULT);
|
permission || ACL.DEFAULT);
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Get the static ACLs from the model definition
|
* Get the static ACLs from the model definition
|
||||||
* @param {String} model The model name
|
* @param {String} model The model name
|
||||||
* @param {String} property The property/method/relation name
|
* @param {String} property The property/method/relation name
|
||||||
*
|
*
|
||||||
* @return {Object[]} An array of ACLs
|
* @return {Object[]} An array of ACLs
|
||||||
*/
|
*/
|
||||||
ACL.getStaticACLs = function getStaticACLs(model, property) {
|
ACL.getStaticACLs = function getStaticACLs(model, property) {
|
||||||
var modelClass = loopback.findModel(model);
|
var modelClass = loopback.findModel(model);
|
||||||
var staticACLs = [];
|
var staticACLs = [];
|
||||||
if (modelClass && modelClass.settings.acls) {
|
if (modelClass && modelClass.settings.acls) {
|
||||||
modelClass.settings.acls.forEach(function (acl) {
|
modelClass.settings.acls.forEach(function(acl) {
|
||||||
if (!acl.property || acl.property === ACL.ALL
|
if (!acl.property || acl.property === ACL.ALL
|
||||||
|| property === acl.property) {
|
|| property === acl.property) {
|
||||||
staticACLs.push(new ACL({
|
staticACLs.push(new ACL({
|
||||||
|
@ -274,7 +265,7 @@ ACL.getStaticACLs = function getStaticACLs(model, property) {
|
||||||
|| modelClass[property] // static method
|
|| modelClass[property] // static method
|
||||||
|| modelClass.prototype[property]); // prototype method
|
|| modelClass.prototype[property]); // prototype method
|
||||||
if (prop && prop.acls) {
|
if (prop && prop.acls) {
|
||||||
prop.acls.forEach(function (acl) {
|
prop.acls.forEach(function(acl) {
|
||||||
staticACLs.push(new ACL({
|
staticACLs.push(new ACL({
|
||||||
model: modelClass.modelName,
|
model: modelClass.modelName,
|
||||||
property: property,
|
property: property,
|
||||||
|
@ -286,9 +277,9 @@ ACL.getStaticACLs = function getStaticACLs(model, property) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return staticACLs;
|
return staticACLs;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the given principal is allowed to access the model/property
|
* Check if the given principal is allowed to access the model/property
|
||||||
* @param {String} principalType The principal type.
|
* @param {String} principalType The principal type.
|
||||||
* @param {String} principalId The principal ID.
|
* @param {String} principalId The principal ID.
|
||||||
|
@ -299,10 +290,10 @@ ACL.getStaticACLs = function getStaticACLs(model, property) {
|
||||||
* @param {String|Error} err The error object
|
* @param {String|Error} err The error object
|
||||||
* @param {AccessRequest} result The access permission
|
* @param {AccessRequest} result The access permission
|
||||||
*/
|
*/
|
||||||
ACL.checkPermission = function checkPermission(principalType, principalId,
|
ACL.checkPermission = function checkPermission(principalType, principalId,
|
||||||
model, property, accessType,
|
model, property, accessType,
|
||||||
callback) {
|
callback) {
|
||||||
if(principalId !== null && principalId !== undefined && (typeof principalId !== 'string') ) {
|
if (principalId !== null && principalId !== undefined && (typeof principalId !== 'string')) {
|
||||||
principalId = principalId.toString();
|
principalId = principalId.toString();
|
||||||
}
|
}
|
||||||
property = property || ACL.ALL;
|
property = property || ACL.ALL;
|
||||||
|
@ -316,7 +307,7 @@ ACL.checkPermission = function checkPermission(principalType, principalId,
|
||||||
|
|
||||||
var resolved = this.resolvePermission(acls, req);
|
var resolved = this.resolvePermission(acls, req);
|
||||||
|
|
||||||
if(resolved && resolved.permission === ACL.DENY) {
|
if (resolved && resolved.permission === ACL.DENY) {
|
||||||
debug('Permission denied by statically resolved permission');
|
debug('Permission denied by statically resolved permission');
|
||||||
debug(' Resolved Permission: %j', resolved);
|
debug(' Resolved Permission: %j', resolved);
|
||||||
process.nextTick(function() {
|
process.nextTick(function() {
|
||||||
|
@ -328,23 +319,23 @@ ACL.checkPermission = function checkPermission(principalType, principalId,
|
||||||
var self = this;
|
var self = this;
|
||||||
this.find({where: {principalType: principalType, principalId: principalId,
|
this.find({where: {principalType: principalType, principalId: principalId,
|
||||||
model: model, property: propertyQuery, accessType: accessTypeQuery}},
|
model: model, property: propertyQuery, accessType: accessTypeQuery}},
|
||||||
function (err, dynACLs) {
|
function(err, dynACLs) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback && callback(err);
|
callback && callback(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
acls = acls.concat(dynACLs);
|
acls = acls.concat(dynACLs);
|
||||||
resolved = self.resolvePermission(acls, req);
|
resolved = self.resolvePermission(acls, req);
|
||||||
if(resolved && resolved.permission === ACL.DEFAULT) {
|
if (resolved && resolved.permission === ACL.DEFAULT) {
|
||||||
var modelClass = loopback.findModel(model);
|
var modelClass = loopback.findModel(model);
|
||||||
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
|
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
|
||||||
}
|
}
|
||||||
callback && callback(null, resolved);
|
callback && callback(null, resolved);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
ACL.prototype.debug = function() {
|
ACL.prototype.debug = function() {
|
||||||
if(debug.enabled) {
|
if (debug.enabled) {
|
||||||
debug('---ACL---');
|
debug('---ACL---');
|
||||||
debug('model %s', this.model);
|
debug('model %s', this.model);
|
||||||
debug('property %s', this.property);
|
debug('property %s', this.property);
|
||||||
|
@ -353,9 +344,9 @@ ACL.prototype.debug = function() {
|
||||||
debug('accessType %s', this.accessType);
|
debug('accessType %s', this.accessType);
|
||||||
debug('permission %s', this.permission);
|
debug('permission %s', this.permission);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the request has the permission to access.
|
* Check if the request has the permission to access.
|
||||||
* @options {Object} context See below.
|
* @options {Object} context See below.
|
||||||
* @property {Object[]} principals An array of principals.
|
* @property {Object[]} principals An array of principals.
|
||||||
|
@ -366,8 +357,8 @@ ACL.prototype.debug = function() {
|
||||||
* @param {Function} callback Callback function
|
* @param {Function} callback Callback function
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ACL.checkAccessForContext = function (context, callback) {
|
ACL.checkAccessForContext = function(context, callback) {
|
||||||
if(!(context instanceof AccessContext)) {
|
if (!(context instanceof AccessContext)) {
|
||||||
context = new AccessContext(context);
|
context = new AccessContext(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -388,7 +379,7 @@ ACL.checkAccessForContext = function (context, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var roleModel = loopback.getModelByType(Role);
|
var roleModel = loopback.getModelByType(Role);
|
||||||
this.find({where: {model: model.modelName, property: propertyQuery,
|
this.find({where: {model: model.modelName, property: propertyQuery,
|
||||||
accessType: accessTypeQuery}}, function (err, acls) {
|
accessType: accessTypeQuery}}, function(err, acls) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback && callback(err);
|
callback && callback(err);
|
||||||
return;
|
return;
|
||||||
|
@ -397,7 +388,7 @@ ACL.checkAccessForContext = function (context, callback) {
|
||||||
|
|
||||||
acls = acls.concat(staticACLs);
|
acls = acls.concat(staticACLs);
|
||||||
|
|
||||||
acls.forEach(function (acl) {
|
acls.forEach(function(acl) {
|
||||||
// Check exact matches
|
// Check exact matches
|
||||||
for (var i = 0; i < context.principals.length; i++) {
|
for (var i = 0; i < context.principals.length; i++) {
|
||||||
var p = context.principals[i];
|
var p = context.principals[i];
|
||||||
|
@ -410,9 +401,9 @@ ACL.checkAccessForContext = function (context, callback) {
|
||||||
|
|
||||||
// Check role matches
|
// Check role matches
|
||||||
if (acl.principalType === ACL.ROLE) {
|
if (acl.principalType === ACL.ROLE) {
|
||||||
inRoleTasks.push(function (done) {
|
inRoleTasks.push(function(done) {
|
||||||
roleModel.isInRole(acl.principalId, context,
|
roleModel.isInRole(acl.principalId, context,
|
||||||
function (err, inRole) {
|
function(err, inRole) {
|
||||||
if (!err && inRole) {
|
if (!err && inRole) {
|
||||||
effectiveACLs.push(acl);
|
effectiveACLs.push(acl);
|
||||||
}
|
}
|
||||||
|
@ -422,13 +413,13 @@ ACL.checkAccessForContext = function (context, callback) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async.parallel(inRoleTasks, function (err, results) {
|
async.parallel(inRoleTasks, function(err, results) {
|
||||||
if(err) {
|
if (err) {
|
||||||
callback && callback(err, null);
|
callback && callback(err, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var resolved = self.resolvePermission(effectiveACLs, req);
|
var resolved = self.resolvePermission(effectiveACLs, req);
|
||||||
if(resolved && resolved.permission === ACL.DEFAULT) {
|
if (resolved && resolved.permission === ACL.DEFAULT) {
|
||||||
resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW;
|
resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW;
|
||||||
}
|
}
|
||||||
debug('---Resolved---');
|
debug('---Resolved---');
|
||||||
|
@ -436,9 +427,9 @@ ACL.checkAccessForContext = function (context, callback) {
|
||||||
callback && callback(null, resolved);
|
callback && callback(null, resolved);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the given access token can invoke the method
|
* Check if the given access token can invoke the method
|
||||||
* @param {AccessToken} token The access token
|
* @param {AccessToken} token The access token
|
||||||
* @param {String} model The model name
|
* @param {String} model The model name
|
||||||
|
@ -448,7 +439,7 @@ ACL.checkAccessForContext = function (context, callback) {
|
||||||
* @param {String|Error} err The error object
|
* @param {String|Error} err The error object
|
||||||
* @param {Boolean} allowed is the request allowed
|
* @param {Boolean} allowed is the request allowed
|
||||||
*/
|
*/
|
||||||
ACL.checkAccessForToken = function (token, model, modelId, method, callback) {
|
ACL.checkAccessForToken = function(token, model, modelId, method, callback) {
|
||||||
assert(token, 'Access token is required');
|
assert(token, 'Access token is required');
|
||||||
|
|
||||||
var context = new AccessContext({
|
var context = new AccessContext({
|
||||||
|
@ -459,56 +450,13 @@ ACL.checkAccessForToken = function (token, model, modelId, method, callback) {
|
||||||
modelId: modelId
|
modelId: modelId
|
||||||
});
|
});
|
||||||
|
|
||||||
this.checkAccessForContext(context, function (err, access) {
|
this.checkAccessForContext(context, function(err, access) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback && callback(err);
|
callback && callback(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
callback && callback(null, access.permission !== ACL.DENY);
|
callback && callback(null, access.permission !== ACL.DENY);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/*!
|
}
|
||||||
* Schema for Scope which represents the permissions that are granted to client
|
|
||||||
* applications by the resource owner
|
|
||||||
*/
|
|
||||||
var ScopeSchema = {
|
|
||||||
name: {type: String, required: true},
|
|
||||||
description: String
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resource owner grants/delegates permissions to client applications
|
|
||||||
*
|
|
||||||
* For a protected resource, does the client application have the authorization
|
|
||||||
* from the resource owner (user or system)?
|
|
||||||
*
|
|
||||||
* Scope has many resource access entries
|
|
||||||
* @class
|
|
||||||
*/
|
|
||||||
var Scope = loopback.createModel('Scope', ScopeSchema);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the given scope is allowed to access the model/property
|
|
||||||
* @param {String} scope The scope name
|
|
||||||
* @param {String} model The model name
|
|
||||||
* @param {String} property The property/method/relation name
|
|
||||||
* @param {String} accessType The access type
|
|
||||||
* @callback {Function} callback
|
|
||||||
* @param {String|Error} err The error object
|
|
||||||
* @param {AccessRequest} result The access permission
|
|
||||||
*/
|
|
||||||
Scope.checkPermission = function (scope, model, property, accessType, callback) {
|
|
||||||
this.findOne({where: {name: scope}}, function (err, scope) {
|
|
||||||
if (err) {
|
|
||||||
callback && callback(err);
|
|
||||||
} else {
|
|
||||||
var aclModel = loopback.getModelByType(ACL);
|
|
||||||
aclModel.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.ACL = ACL;
|
|
||||||
module.exports.Scope = Scope;
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "ACL",
|
||||||
|
"properties": {
|
||||||
|
"model": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the model"
|
||||||
|
},
|
||||||
|
"property": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the property, method, scope, or relation"
|
||||||
|
},
|
||||||
|
"accessType": "string",
|
||||||
|
"permission": "string",
|
||||||
|
"principalType": "string",
|
||||||
|
"principalId": "string"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,95 +1,5 @@
|
||||||
var loopback = require('../../lib/loopback');
|
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
|
|
||||||
// Authentication schemes
|
|
||||||
var AuthenticationSchemeSchema = {
|
|
||||||
scheme: String, // local, facebook, google, twitter, linkedin, github
|
|
||||||
credential: Object // Scheme-specific credentials
|
|
||||||
};
|
|
||||||
|
|
||||||
// See https://github.com/argon/node-apn/blob/master/doc/apn.markdown
|
|
||||||
var APNSSettingSchema = {
|
|
||||||
/**
|
|
||||||
* production or development mode. It denotes what default APNS servers to be
|
|
||||||
* used to send notifications
|
|
||||||
* - true (production mode)
|
|
||||||
* - push: gateway.push.apple.com:2195
|
|
||||||
* - feedback: feedback.push.apple.com:2196
|
|
||||||
* - false (development mode, the default)
|
|
||||||
* - push: gateway.sandbox.push.apple.com:2195
|
|
||||||
* - feedback: feedback.sandbox.push.apple.com:2196
|
|
||||||
*/
|
|
||||||
production: Boolean,
|
|
||||||
certData: String, // The certificate data loaded from the cert.pem file
|
|
||||||
keyData: String, // The key data loaded from the key.pem file
|
|
||||||
|
|
||||||
pushOptions: {type: {
|
|
||||||
gateway: String,
|
|
||||||
port: Number
|
|
||||||
}},
|
|
||||||
|
|
||||||
feedbackOptions: {type: {
|
|
||||||
gateway: String,
|
|
||||||
port: Number,
|
|
||||||
batchFeedback: Boolean,
|
|
||||||
interval: Number
|
|
||||||
}}
|
|
||||||
};
|
|
||||||
|
|
||||||
var GcmSettingsSchema = {
|
|
||||||
serverApiKey: String
|
|
||||||
};
|
|
||||||
|
|
||||||
// Push notification settings
|
|
||||||
var PushNotificationSettingSchema = {
|
|
||||||
apns: APNSSettingSchema,
|
|
||||||
gcm: GcmSettingsSchema
|
|
||||||
};
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Data model for Application
|
|
||||||
*/
|
|
||||||
var ApplicationSchema = {
|
|
||||||
id: {type: String, id: true},
|
|
||||||
// Basic information
|
|
||||||
name: {type: String, required: true}, // The name
|
|
||||||
description: String, // The description
|
|
||||||
icon: String, // The icon image url
|
|
||||||
|
|
||||||
owner: String, // The user id of the developer who registers the application
|
|
||||||
collaborators: [String], // A list of users ids who have permissions to work on this app
|
|
||||||
|
|
||||||
// EMail
|
|
||||||
email: String, // e-mail address
|
|
||||||
emailVerified: Boolean, // Is the e-mail verified
|
|
||||||
|
|
||||||
// oAuth 2.0 settings
|
|
||||||
url: String, // The application url
|
|
||||||
callbackUrls: [String], // oAuth 2.0 code/token callback url
|
|
||||||
permissions: [String], // A list of permissions required by the application
|
|
||||||
|
|
||||||
// Keys
|
|
||||||
clientKey: String,
|
|
||||||
javaScriptKey: String,
|
|
||||||
restApiKey: String,
|
|
||||||
windowsKey: String,
|
|
||||||
masterKey: String,
|
|
||||||
|
|
||||||
// Push notification
|
|
||||||
pushSettings: PushNotificationSettingSchema,
|
|
||||||
|
|
||||||
// User Authentication
|
|
||||||
authenticationEnabled: {type: Boolean, default: true},
|
|
||||||
anonymousAllowed: {type: Boolean, default: true},
|
|
||||||
authenticationSchemes: [AuthenticationSchemeSchema],
|
|
||||||
|
|
||||||
status: {type: String, default: 'sandbox'}, // Status of the application, production/sandbox/disabled
|
|
||||||
|
|
||||||
// Timestamps
|
|
||||||
created: {type: Date, default: Date},
|
|
||||||
modified: {type: Date, default: Date}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Application management functions
|
* Application management functions
|
||||||
*/
|
*/
|
||||||
|
@ -123,6 +33,9 @@ function generateKey(hmacKey, algorithm, encoding) {
|
||||||
* @property {Date} created Date Application object was created. Default: current date.
|
* @property {Date} created Date Application object was created. Default: current date.
|
||||||
* @property {Date} modified Date Application object was modified. Default: current date.
|
* @property {Date} modified Date Application object was modified. Default: current date.
|
||||||
*
|
*
|
||||||
|
* @property {Object} pushSettings.apns APNS configuration, see the options
|
||||||
|
* below and also
|
||||||
|
* https://github.com/argon/node-apn/blob/master/doc/apn.markdown
|
||||||
* @property {Boolean} pushSettings.apns.production Whether to use production Apple Push Notification Service (APNS) servers to send push notifications.
|
* @property {Boolean} pushSettings.apns.production Whether to use production Apple Push Notification Service (APNS) servers to send push notifications.
|
||||||
* If true, uses `gateway.push.apple.com:2195` and `feedback.push.apple.com:2196`.
|
* If true, uses `gateway.push.apple.com:2195` and `feedback.push.apple.com:2196`.
|
||||||
* If false, uses `gateway.sandbox.push.apple.com:2195` and `feedback.sandbox.push.apple.com:2196`
|
* If false, uses `gateway.sandbox.push.apple.com:2195` and `feedback.sandbox.push.apple.com:2196`
|
||||||
|
@ -136,17 +49,39 @@ function generateKey(hmacKey, algorithm, encoding) {
|
||||||
* @property {Number} pushSettings.apns.feedbackOptions.interval (APNS).
|
* @property {Number} pushSettings.apns.feedbackOptions.interval (APNS).
|
||||||
* @property {String} pushSettings.gcm.serverApiKey: Google Cloud Messaging API key.
|
* @property {String} pushSettings.gcm.serverApiKey: Google Cloud Messaging API key.
|
||||||
*
|
*
|
||||||
* @class
|
* @property {Boolean} authenticationEnabled
|
||||||
* @inherits {Model}
|
* @property {Boolean} anonymousAllowed
|
||||||
|
* @property {Array} authenticationSchemes List of authentication schemes
|
||||||
|
* (see below).
|
||||||
|
* @property {String} authenticationSchemes.scheme Scheme name.
|
||||||
|
* Supported values: `local`, `facebook`, `google`,
|
||||||
|
* `twitter`, `linkedin`, `github`.
|
||||||
|
* @property {Object} authenticationSchemes.credential
|
||||||
|
* Scheme-specific credentials.
|
||||||
|
*
|
||||||
|
* @class Application
|
||||||
|
* @inherits {PersistedModel}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var Application = loopback.PersistedModel.extend('Application', ApplicationSchema);
|
module.exports = function(Application) {
|
||||||
|
|
||||||
/*!
|
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||||
|
Application.definition.rawProperties.created.default =
|
||||||
|
Application.definition.properties.created.default = function() {
|
||||||
|
return new Date();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||||
|
Application.definition.rawProperties.modified.default =
|
||||||
|
Application.definition.properties.modified.default = function() {
|
||||||
|
return new Date();
|
||||||
|
};
|
||||||
|
|
||||||
|
/*!
|
||||||
* A hook to generate keys before creation
|
* A hook to generate keys before creation
|
||||||
* @param next
|
* @param next
|
||||||
*/
|
*/
|
||||||
Application.beforeCreate = function (next) {
|
Application.beforeCreate = function(next) {
|
||||||
var app = this;
|
var app = this;
|
||||||
app.created = app.modified = new Date();
|
app.created = app.modified = new Date();
|
||||||
app.id = generateKey('id', 'md5');
|
app.id = generateKey('id', 'md5');
|
||||||
|
@ -156,16 +91,16 @@ Application.beforeCreate = function (next) {
|
||||||
app.windowsKey = generateKey('windows');
|
app.windowsKey = generateKey('windows');
|
||||||
app.masterKey = generateKey('master');
|
app.masterKey = generateKey('master');
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new application
|
* Register a new application
|
||||||
* @param {String} owner Owner's user ID.
|
* @param {String} owner Owner's user ID.
|
||||||
* @param {String} name Name of the application
|
* @param {String} name Name of the application
|
||||||
* @param {Object} options Other options
|
* @param {Object} options Other options
|
||||||
* @param {Function} callback Callback function
|
* @param {Function} callback Callback function
|
||||||
*/
|
*/
|
||||||
Application.register = function (owner, name, options, cb) {
|
Application.register = function(owner, name, options, cb) {
|
||||||
assert(owner, 'owner is required');
|
assert(owner, 'owner is required');
|
||||||
assert(name, 'name is required');
|
assert(name, 'name is required');
|
||||||
|
|
||||||
|
@ -180,14 +115,14 @@ Application.register = function (owner, name, options, cb) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.create(props, cb);
|
this.create(props, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset keys for the application instance
|
* Reset keys for the application instance
|
||||||
* @callback {Function} callback
|
* @callback {Function} callback
|
||||||
* @param {Error} err
|
* @param {Error} err
|
||||||
*/
|
*/
|
||||||
Application.prototype.resetKeys = function (cb) {
|
Application.prototype.resetKeys = function(cb) {
|
||||||
this.clientKey = generateKey('client');
|
this.clientKey = generateKey('client');
|
||||||
this.javaScriptKey = generateKey('javaScript');
|
this.javaScriptKey = generateKey('javaScript');
|
||||||
this.restApiKey = generateKey('restApi');
|
this.restApiKey = generateKey('restApi');
|
||||||
|
@ -195,25 +130,25 @@ Application.prototype.resetKeys = function (cb) {
|
||||||
this.masterKey = generateKey('master');
|
this.masterKey = generateKey('master');
|
||||||
this.modified = new Date();
|
this.modified = new Date();
|
||||||
this.save(cb);
|
this.save(cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset keys for a given application by the appId
|
* Reset keys for a given application by the appId
|
||||||
* @param {Any} appId
|
* @param {Any} appId
|
||||||
* @callback {Function} callback
|
* @callback {Function} callback
|
||||||
* @param {Error} err
|
* @param {Error} err
|
||||||
*/
|
*/
|
||||||
Application.resetKeys = function (appId, cb) {
|
Application.resetKeys = function(appId, cb) {
|
||||||
this.findById(appId, function (err, app) {
|
this.findById(appId, function(err, app) {
|
||||||
if (err) {
|
if (err) {
|
||||||
cb && cb(err, app);
|
cb && cb(err, app);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
app.resetKeys(cb);
|
app.resetKeys(cb);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticate the application id and key.
|
* Authenticate the application id and key.
|
||||||
*
|
*
|
||||||
* `matched` parameter is one of:
|
* `matched` parameter is one of:
|
||||||
|
@ -229,8 +164,8 @@ Application.resetKeys = function (appId, cb) {
|
||||||
* @param {Error} err
|
* @param {Error} err
|
||||||
* @param {String} matched The matching key
|
* @param {String} matched The matching key
|
||||||
*/
|
*/
|
||||||
Application.authenticate = function (appId, key, cb) {
|
Application.authenticate = function(appId, key, cb) {
|
||||||
this.findById(appId, function (err, app) {
|
this.findById(appId, function(err, app) {
|
||||||
if (err || !app) {
|
if (err || !app) {
|
||||||
cb && cb(err, null);
|
cb && cb(err, null);
|
||||||
return;
|
return;
|
||||||
|
@ -248,7 +183,5 @@ Application.authenticate = function (appId, key, cb) {
|
||||||
}
|
}
|
||||||
cb && cb(null, result);
|
cb && cb(null, result);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Application;
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
{
|
||||||
|
"name": "Application",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"id": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"description": "string",
|
||||||
|
"icon": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The icon image url"
|
||||||
|
},
|
||||||
|
|
||||||
|
"owner": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The user id of the developer who registers the application"
|
||||||
|
},
|
||||||
|
"collaborators": {
|
||||||
|
"type": ["string"],
|
||||||
|
"description": "A list of users ids who have permissions to work on this app"
|
||||||
|
},
|
||||||
|
|
||||||
|
"email": "string",
|
||||||
|
"emailVerified": "boolean",
|
||||||
|
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The application URL for OAuth 2.0"
|
||||||
|
},
|
||||||
|
"callbackUrls": {
|
||||||
|
"type": ["string"],
|
||||||
|
"description": "OAuth 2.0 code/token callback URLs"
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"type": ["string"],
|
||||||
|
"description": "A list of permissions required by the application"
|
||||||
|
},
|
||||||
|
|
||||||
|
"clientKey": "string",
|
||||||
|
"javaScriptKey": "string",
|
||||||
|
"restApiKey": "string",
|
||||||
|
"windowsKey": "string",
|
||||||
|
"masterKey": "string",
|
||||||
|
|
||||||
|
"pushSettings": {
|
||||||
|
"apns": {
|
||||||
|
"production": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": [
|
||||||
|
"Production or development mode. It denotes what default APNS",
|
||||||
|
"servers to be used to send notifications.",
|
||||||
|
"See API documentation for more details."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"certData": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The certificate data loaded from the cert.pem file"
|
||||||
|
},
|
||||||
|
"keyData": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The key data loaded from the key.pem file"
|
||||||
|
},
|
||||||
|
|
||||||
|
"pushOptions": {
|
||||||
|
"type": {
|
||||||
|
"gateway": "string",
|
||||||
|
"port": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"feedbackOptions": {
|
||||||
|
"type": {
|
||||||
|
"gateway": "string",
|
||||||
|
"port": "number",
|
||||||
|
"batchFeedback": "boolean",
|
||||||
|
"interval": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"gcm": {
|
||||||
|
"serverApiKey": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"authenticationEnabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"anonymousAllowed": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"authenticationSchemes": [
|
||||||
|
{
|
||||||
|
"scheme": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "See the API docs for the list of supported values."
|
||||||
|
},
|
||||||
|
"credential": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Scheme-specific credentials"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "sandbox",
|
||||||
|
"description": "Status of the application, production/sandbox/disabled"
|
||||||
|
},
|
||||||
|
|
||||||
|
"created": "date",
|
||||||
|
"modified": "date"
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,26 +10,6 @@ var PersistedModel = require('../../lib/loopback').PersistedModel
|
||||||
, assert = require('assert')
|
, assert = require('assert')
|
||||||
, debug = require('debug')('loopback:change');
|
, debug = require('debug')('loopback:change');
|
||||||
|
|
||||||
/*!
|
|
||||||
* Properties
|
|
||||||
*/
|
|
||||||
|
|
||||||
var properties = {
|
|
||||||
id: {type: String, id: true},
|
|
||||||
rev: {type: String},
|
|
||||||
prev: {type: String},
|
|
||||||
checkpoint: {type: Number},
|
|
||||||
modelName: {type: String},
|
|
||||||
modelId: {type: String}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Options
|
|
||||||
*/
|
|
||||||
|
|
||||||
var options = {
|
|
||||||
trackChanges: false
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change list entry.
|
* Change list entry.
|
||||||
|
@ -41,45 +21,45 @@ var options = {
|
||||||
* @property {String} modelName Model name
|
* @property {String} modelName Model name
|
||||||
* @property {String} modelId Model ID
|
* @property {String} modelId Model ID
|
||||||
*
|
*
|
||||||
* @class
|
* @class Change
|
||||||
* @inherits {Model}
|
* @inherits {PersistedModel}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var Change = module.exports = PersistedModel.extend('Change', properties, options);
|
module.exports = function(Change) {
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Constants
|
* Constants
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.UPDATE = 'update';
|
Change.UPDATE = 'update';
|
||||||
Change.CREATE = 'create';
|
Change.CREATE = 'create';
|
||||||
Change.DELETE = 'delete';
|
Change.DELETE = 'delete';
|
||||||
Change.UNKNOWN = 'unknown';
|
Change.UNKNOWN = 'unknown';
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Conflict Class
|
* Conflict Class
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.Conflict = Conflict;
|
Change.Conflict = Conflict;
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Setup the extended model.
|
* Setup the extended model.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.setup = function() {
|
Change.setup = function() {
|
||||||
PersistedModel.setup.call(this);
|
PersistedModel.setup.call(this);
|
||||||
var Change = this;
|
var Change = this;
|
||||||
|
|
||||||
Change.getter.id = function() {
|
Change.getter.id = function() {
|
||||||
var hasModel = this.modelName && this.modelId;
|
var hasModel = this.modelName && this.modelId;
|
||||||
if(!hasModel) return null;
|
if (!hasModel) return null;
|
||||||
|
|
||||||
return Change.idForModel(this.modelName, this.modelId);
|
return Change.idForModel(this.modelName, this.modelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Change.setup();
|
Change.setup();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Track the recent change of the given modelIds.
|
* Track the recent change of the given modelIds.
|
||||||
*
|
*
|
||||||
* @param {String} modelName
|
* @param {String} modelName
|
||||||
|
@ -89,22 +69,22 @@ Change.setup();
|
||||||
* @param {Array} changes Changes that were tracked
|
* @param {Array} changes Changes that were tracked
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.rectifyModelChanges = function(modelName, modelIds, callback) {
|
Change.rectifyModelChanges = function(modelName, modelIds, callback) {
|
||||||
var tasks = [];
|
var tasks = [];
|
||||||
var Change = this;
|
var Change = this;
|
||||||
|
|
||||||
modelIds.forEach(function(id) {
|
modelIds.forEach(function(id) {
|
||||||
tasks.push(function(cb) {
|
tasks.push(function(cb) {
|
||||||
Change.findOrCreateChange(modelName, id, function(err, change) {
|
Change.findOrCreateChange(modelName, id, function(err, change) {
|
||||||
if(err) return Change.handleError(err, cb);
|
if (err) return Change.handleError(err, cb);
|
||||||
change.rectify(cb);
|
change.rectify(cb);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
async.parallel(tasks, callback);
|
async.parallel(tasks, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an identifier for a given model.
|
* Get an identifier for a given model.
|
||||||
*
|
*
|
||||||
* @param {String} modelName
|
* @param {String} modelName
|
||||||
|
@ -112,11 +92,11 @@ Change.rectifyModelChanges = function(modelName, modelIds, callback) {
|
||||||
* @return {String}
|
* @return {String}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.idForModel = function(modelName, modelId) {
|
Change.idForModel = function(modelName, modelId) {
|
||||||
return this.hash([modelName, modelId].join('-'));
|
return this.hash([modelName, modelId].join('-'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find or create a change for the given model.
|
* Find or create a change for the given model.
|
||||||
*
|
*
|
||||||
* @param {String} modelName
|
* @param {String} modelName
|
||||||
|
@ -127,14 +107,14 @@ Change.idForModel = function(modelName, modelId) {
|
||||||
* @end
|
* @end
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.findOrCreateChange = function(modelName, modelId, callback) {
|
Change.findOrCreateChange = function(modelName, modelId, callback) {
|
||||||
assert(loopback.findModel(modelName), modelName + ' does not exist');
|
assert(loopback.findModel(modelName), modelName + ' does not exist');
|
||||||
var id = this.idForModel(modelName, modelId);
|
var id = this.idForModel(modelName, modelId);
|
||||||
var Change = this;
|
var Change = this;
|
||||||
|
|
||||||
this.findById(id, function(err, change) {
|
this.findById(id, function(err, change) {
|
||||||
if(err) return callback(err);
|
if (err) return callback(err);
|
||||||
if(change) {
|
if (change) {
|
||||||
callback(null, change);
|
callback(null, change);
|
||||||
} else {
|
} else {
|
||||||
var ch = new Change({
|
var ch = new Change({
|
||||||
|
@ -146,9 +126,9 @@ Change.findOrCreateChange = function(modelName, modelId, callback) {
|
||||||
ch.save(callback);
|
ch.save(callback);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update (or create) the change with the current revision.
|
* Update (or create) the change with the current revision.
|
||||||
*
|
*
|
||||||
* @callback {Function} callback
|
* @callback {Function} callback
|
||||||
|
@ -156,7 +136,7 @@ Change.findOrCreateChange = function(modelName, modelId, callback) {
|
||||||
* @param {Change} change
|
* @param {Change} change
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.prototype.rectify = function(cb) {
|
Change.prototype.rectify = function(cb) {
|
||||||
var change = this;
|
var change = this;
|
||||||
var tasks = [
|
var tasks = [
|
||||||
updateRevision,
|
updateRevision,
|
||||||
|
@ -167,12 +147,12 @@ Change.prototype.rectify = function(cb) {
|
||||||
change.debug('rectify change');
|
change.debug('rectify change');
|
||||||
|
|
||||||
cb = cb || function(err) {
|
cb = cb || function(err) {
|
||||||
if(err) throw new Error(err);
|
if (err) throw new Error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
async.parallel(tasks, function(err) {
|
async.parallel(tasks, function(err) {
|
||||||
if(err) return cb(err);
|
if (err) return cb(err);
|
||||||
if(change.prev === Change.UNKNOWN) {
|
if (change.prev === Change.UNKNOWN) {
|
||||||
// this occurs when a record of a change doesn't exist
|
// this occurs when a record of a change doesn't exist
|
||||||
// and its current revision is null (not found)
|
// and its current revision is null (not found)
|
||||||
change.remove(cb);
|
change.remove(cb);
|
||||||
|
@ -184,10 +164,10 @@ Change.prototype.rectify = function(cb) {
|
||||||
function updateRevision(cb) {
|
function updateRevision(cb) {
|
||||||
// get the current revision
|
// get the current revision
|
||||||
change.currentRevision(function(err, rev) {
|
change.currentRevision(function(err, rev) {
|
||||||
if(err) return Change.handleError(err, cb);
|
if (err) return Change.handleError(err, cb);
|
||||||
if(rev) {
|
if (rev) {
|
||||||
// avoid setting rev and prev to the same value
|
// avoid setting rev and prev to the same value
|
||||||
if(currentRev !== rev) {
|
if (currentRev !== rev) {
|
||||||
change.rev = rev;
|
change.rev = rev;
|
||||||
change.prev = currentRev;
|
change.prev = currentRev;
|
||||||
} else {
|
} else {
|
||||||
|
@ -195,9 +175,9 @@ Change.prototype.rectify = function(cb) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
change.rev = null;
|
change.rev = null;
|
||||||
if(currentRev) {
|
if (currentRev) {
|
||||||
change.prev = currentRev;
|
change.prev = currentRev;
|
||||||
} else if(!change.prev) {
|
} else if (!change.prev) {
|
||||||
change.debug('ERROR - could not determing prev');
|
change.debug('ERROR - could not determing prev');
|
||||||
change.prev = Change.UNKNOWN;
|
change.prev = Change.UNKNOWN;
|
||||||
}
|
}
|
||||||
|
@ -209,34 +189,34 @@ Change.prototype.rectify = function(cb) {
|
||||||
|
|
||||||
function updateCheckpoint(cb) {
|
function updateCheckpoint(cb) {
|
||||||
change.constructor.getCheckpointModel().current(function(err, checkpoint) {
|
change.constructor.getCheckpointModel().current(function(err, checkpoint) {
|
||||||
if(err) return Change.handleError(err);
|
if (err) return Change.handleError(err);
|
||||||
change.checkpoint = checkpoint;
|
change.checkpoint = checkpoint;
|
||||||
cb();
|
cb();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a change's current revision based on current data.
|
* Get a change's current revision based on current data.
|
||||||
* @callback {Function} callback
|
* @callback {Function} callback
|
||||||
* @param {Error} err
|
* @param {Error} err
|
||||||
* @param {String} rev The current revision
|
* @param {String} rev The current revision
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.prototype.currentRevision = function(cb) {
|
Change.prototype.currentRevision = function(cb) {
|
||||||
var model = this.getModelCtor();
|
var model = this.getModelCtor();
|
||||||
var id = this.getModelId();
|
var id = this.getModelId();
|
||||||
model.findById(id, function(err, inst) {
|
model.findById(id, function(err, inst) {
|
||||||
if(err) return Change.handleError(err, cb);
|
if (err) return Change.handleError(err, cb);
|
||||||
if(inst) {
|
if (inst) {
|
||||||
cb(null, Change.revisionForInst(inst));
|
cb(null, Change.revisionForInst(inst));
|
||||||
} else {
|
} else {
|
||||||
cb(null, null);
|
cb(null, null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a hash of the given `string` with the `options.hashAlgorithm`.
|
* Create a hash of the given `string` with the `options.hashAlgorithm`.
|
||||||
* **Default: `sha1`**
|
* **Default: `sha1`**
|
||||||
*
|
*
|
||||||
|
@ -244,24 +224,24 @@ Change.prototype.currentRevision = function(cb) {
|
||||||
* @return {String} The hashed string
|
* @return {String} The hashed string
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.hash = function(str) {
|
Change.hash = function(str) {
|
||||||
return crypto
|
return crypto
|
||||||
.createHash(Change.settings.hashAlgorithm || 'sha1')
|
.createHash(Change.settings.hashAlgorithm || 'sha1')
|
||||||
.update(str)
|
.update(str)
|
||||||
.digest('hex');
|
.digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the revision string for the given object
|
* Get the revision string for the given object
|
||||||
* @param {Object} inst The data to get the revision string for
|
* @param {Object} inst The data to get the revision string for
|
||||||
* @return {String} The revision string
|
* @return {String} The revision string
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.revisionForInst = function(inst) {
|
Change.revisionForInst = function(inst) {
|
||||||
return this.hash(CJSON.stringify(inst));
|
return this.hash(CJSON.stringify(inst));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a change's type. Returns one of:
|
* Get a change's type. Returns one of:
|
||||||
*
|
*
|
||||||
* - `Change.UPDATE`
|
* - `Change.UPDATE`
|
||||||
|
@ -272,69 +252,69 @@ Change.revisionForInst = function(inst) {
|
||||||
* @return {String} the type of change
|
* @return {String} the type of change
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.prototype.type = function() {
|
Change.prototype.type = function() {
|
||||||
if(this.rev && this.prev) {
|
if (this.rev && this.prev) {
|
||||||
return Change.UPDATE;
|
return Change.UPDATE;
|
||||||
}
|
}
|
||||||
if(this.rev && !this.prev) {
|
if (this.rev && !this.prev) {
|
||||||
return Change.CREATE;
|
return Change.CREATE;
|
||||||
}
|
}
|
||||||
if(!this.rev && this.prev) {
|
if (!this.rev && this.prev) {
|
||||||
return Change.DELETE;
|
return Change.DELETE;
|
||||||
}
|
}
|
||||||
return Change.UNKNOWN;
|
return Change.UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare two changes.
|
* Compare two changes.
|
||||||
* @param {Change} change
|
* @param {Change} change
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.prototype.equals = function(change) {
|
Change.prototype.equals = function(change) {
|
||||||
if(!change) return false;
|
if (!change) return false;
|
||||||
var thisRev = this.rev || null;
|
var thisRev = this.rev || null;
|
||||||
var thatRev = change.rev || null;
|
var thatRev = change.rev || null;
|
||||||
return thisRev === thatRev;
|
return thisRev === thatRev;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does this change conflict with the given change.
|
* Does this change conflict with the given change.
|
||||||
* @param {Change} change
|
* @param {Change} change
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.prototype.conflictsWith = function(change) {
|
Change.prototype.conflictsWith = function(change) {
|
||||||
if(!change) return false;
|
if (!change) return false;
|
||||||
if(this.equals(change)) return false;
|
if (this.equals(change)) return false;
|
||||||
if(Change.bothDeleted(this, change)) return false;
|
if (Change.bothDeleted(this, change)) return false;
|
||||||
if(this.isBasedOn(change)) return false;
|
if (this.isBasedOn(change)) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Are both changes deletes?
|
* Are both changes deletes?
|
||||||
* @param {Change} a
|
* @param {Change} a
|
||||||
* @param {Change} b
|
* @param {Change} b
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.bothDeleted = function(a, b) {
|
Change.bothDeleted = function(a, b) {
|
||||||
return a.type() === Change.DELETE
|
return a.type() === Change.DELETE
|
||||||
&& b.type() === Change.DELETE;
|
&& b.type() === Change.DELETE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the change is based on the given change.
|
* Determine if the change is based on the given change.
|
||||||
* @param {Change} change
|
* @param {Change} change
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.prototype.isBasedOn = function(change) {
|
Change.prototype.isBasedOn = function(change) {
|
||||||
return this.prev === change.rev;
|
return this.prev === change.rev;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine the differences for a given model since a given checkpoint.
|
* Determine the differences for a given model since a given checkpoint.
|
||||||
*
|
*
|
||||||
* The callback will contain an error or `result`.
|
* The callback will contain an error or `result`.
|
||||||
|
@ -364,7 +344,7 @@ Change.prototype.isBasedOn = function(change) {
|
||||||
* @param {Object} result See above.
|
* @param {Object} result See above.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.diff = function(modelName, since, remoteChanges, callback) {
|
Change.diff = function(modelName, since, remoteChanges, callback) {
|
||||||
var remoteChangeIndex = {};
|
var remoteChangeIndex = {};
|
||||||
var modelIds = [];
|
var modelIds = [];
|
||||||
remoteChanges.forEach(function(ch) {
|
remoteChanges.forEach(function(ch) {
|
||||||
|
@ -381,7 +361,7 @@ Change.diff = function(modelName, since, remoteChanges, callback) {
|
||||||
checkpoint: {gte: since}
|
checkpoint: {gte: since}
|
||||||
}
|
}
|
||||||
}, function(err, localChanges) {
|
}, function(err, localChanges) {
|
||||||
if(err) return callback(err);
|
if (err) return callback(err);
|
||||||
var deltas = [];
|
var deltas = [];
|
||||||
var conflicts = [];
|
var conflicts = [];
|
||||||
var localModelIds = [];
|
var localModelIds = [];
|
||||||
|
@ -390,8 +370,8 @@ Change.diff = function(modelName, since, remoteChanges, callback) {
|
||||||
localChange = new Change(localChange);
|
localChange = new Change(localChange);
|
||||||
localModelIds.push(localChange.modelId);
|
localModelIds.push(localChange.modelId);
|
||||||
var remoteChange = remoteChangeIndex[localChange.modelId];
|
var remoteChange = remoteChangeIndex[localChange.modelId];
|
||||||
if(remoteChange && !localChange.equals(remoteChange)) {
|
if (remoteChange && !localChange.equals(remoteChange)) {
|
||||||
if(remoteChange.conflictsWith(localChange)) {
|
if (remoteChange.conflictsWith(localChange)) {
|
||||||
remoteChange.debug('remote conflict');
|
remoteChange.debug('remote conflict');
|
||||||
localChange.debug('local conflict');
|
localChange.debug('local conflict');
|
||||||
conflicts.push(localChange);
|
conflicts.push(localChange);
|
||||||
|
@ -403,7 +383,7 @@ Change.diff = function(modelName, since, remoteChanges, callback) {
|
||||||
});
|
});
|
||||||
|
|
||||||
modelIds.forEach(function(id) {
|
modelIds.forEach(function(id) {
|
||||||
if(localModelIds.indexOf(id) === -1) {
|
if (localModelIds.indexOf(id) === -1) {
|
||||||
deltas.push(remoteChangeIndex[id]);
|
deltas.push(remoteChangeIndex[id]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -413,49 +393,49 @@ Change.diff = function(modelName, since, remoteChanges, callback) {
|
||||||
conflicts: conflicts
|
conflicts: conflicts
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Correct all change list entries.
|
* Correct all change list entries.
|
||||||
* @param {Function} callback
|
* @param {Function} callback
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.rectifyAll = function(cb) {
|
Change.rectifyAll = function(cb) {
|
||||||
debug('rectify all');
|
debug('rectify all');
|
||||||
var Change = this;
|
var Change = this;
|
||||||
// this should be optimized
|
// this should be optimized
|
||||||
this.find(function(err, changes) {
|
this.find(function(err, changes) {
|
||||||
if(err) return cb(err);
|
if (err) return cb(err);
|
||||||
changes.forEach(function(change) {
|
changes.forEach(function(change) {
|
||||||
change = new Change(change);
|
change = new Change(change);
|
||||||
change.rectify();
|
change.rectify();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the checkpoint model.
|
* Get the checkpoint model.
|
||||||
* @return {Checkpoint}
|
* @return {Checkpoint}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.getCheckpointModel = function() {
|
Change.getCheckpointModel = function() {
|
||||||
var checkpointModel = this.Checkpoint;
|
var checkpointModel = this.Checkpoint;
|
||||||
if(checkpointModel) return checkpointModel;
|
if (checkpointModel) return checkpointModel;
|
||||||
this.checkpoint = checkpointModel = require('./checkpoint').extend('checkpoint');
|
this.checkpoint = checkpointModel = loopback.Checkpoint.extend('checkpoint');
|
||||||
assert(this.dataSource, 'Cannot getCheckpointModel(): ' + this.modelName
|
assert(this.dataSource, 'Cannot getCheckpointModel(): ' + this.modelName
|
||||||
+ ' is not attached to a dataSource');
|
+ ' is not attached to a dataSource');
|
||||||
checkpointModel.attachTo(this.dataSource);
|
checkpointModel.attachTo(this.dataSource);
|
||||||
return checkpointModel;
|
return checkpointModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
Change.handleError = function(err) {
|
Change.handleError = function(err) {
|
||||||
if(!this.settings.ignoreErrors) {
|
if (!this.settings.ignoreErrors) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Change.prototype.debug = function() {
|
Change.prototype.debug = function() {
|
||||||
if(debug.enabled) {
|
if (debug.enabled) {
|
||||||
var args = Array.prototype.slice.call(arguments);
|
var args = Array.prototype.slice.call(arguments);
|
||||||
debug.apply(this, args);
|
debug.apply(this, args);
|
||||||
debug('\tid', this.id);
|
debug('\tid', this.id);
|
||||||
|
@ -465,33 +445,33 @@ Change.prototype.debug = function() {
|
||||||
debug('\tmodelId', this.modelId);
|
debug('\tmodelId', this.modelId);
|
||||||
debug('\ttype', this.type());
|
debug('\ttype', this.type());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the `Model` class for `change.modelName`.
|
* Get the `Model` class for `change.modelName`.
|
||||||
* @return {Model}
|
* @return {Model}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Change.prototype.getModelCtor = function() {
|
Change.prototype.getModelCtor = function() {
|
||||||
return this.constructor.settings.trackModel;
|
return this.constructor.settings.trackModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
Change.prototype.getModelId = function() {
|
Change.prototype.getModelId = function() {
|
||||||
// TODO(ritch) get rid of the need to create an instance
|
// TODO(ritch) get rid of the need to create an instance
|
||||||
var Model = this.getModelCtor();
|
var Model = this.getModelCtor();
|
||||||
var id = this.modelId;
|
var id = this.modelId;
|
||||||
var m = new Model();
|
var m = new Model();
|
||||||
m.setId(id);
|
m.setId(id);
|
||||||
return m.getId();
|
return m.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
Change.prototype.getModel = function(callback) {
|
Change.prototype.getModel = function(callback) {
|
||||||
var Model = this.constructor.settings.trackModel;
|
var Model = this.constructor.settings.trackModel;
|
||||||
var id = this.getModelId();
|
var id = this.getModelId();
|
||||||
Model.findById(id, callback);
|
Model.findById(id, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When two changes conflict a conflict is created.
|
* When two changes conflict a conflict is created.
|
||||||
*
|
*
|
||||||
* **Note: call `conflict.fetch()` to get the `target` and `source` models.
|
* **Note: call `conflict.fetch()` to get the `target` and `source` models.
|
||||||
|
@ -501,17 +481,18 @@ Change.prototype.getModel = function(callback) {
|
||||||
* @param {PersistedModel} TargetModel
|
* @param {PersistedModel} TargetModel
|
||||||
* @property {ModelClass} source The source model instance
|
* @property {ModelClass} source The source model instance
|
||||||
* @property {ModelClass} target The target model instance
|
* @property {ModelClass} target The target model instance
|
||||||
|
* @class Change.Conflict
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function Conflict(modelId, SourceModel, TargetModel) {
|
function Conflict(modelId, SourceModel, TargetModel) {
|
||||||
this.SourceModel = SourceModel;
|
this.SourceModel = SourceModel;
|
||||||
this.TargetModel = TargetModel;
|
this.TargetModel = TargetModel;
|
||||||
this.SourceChange = SourceModel.getChangeModel();
|
this.SourceChange = SourceModel.getChangeModel();
|
||||||
this.TargetChange = TargetModel.getChangeModel();
|
this.TargetChange = TargetModel.getChangeModel();
|
||||||
this.modelId = modelId;
|
this.modelId = modelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the conflicting models.
|
* Fetch the conflicting models.
|
||||||
*
|
*
|
||||||
* @callback {Function} callback
|
* @callback {Function} callback
|
||||||
|
@ -520,7 +501,7 @@ function Conflict(modelId, SourceModel, TargetModel) {
|
||||||
* @param {PersistedModel} target
|
* @param {PersistedModel} target
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Conflict.prototype.models = function(cb) {
|
Conflict.prototype.models = function(cb) {
|
||||||
var conflict = this;
|
var conflict = this;
|
||||||
var SourceModel = this.SourceModel;
|
var SourceModel = this.SourceModel;
|
||||||
var TargetModel = this.TargetModel;
|
var TargetModel = this.TargetModel;
|
||||||
|
@ -534,7 +515,7 @@ Conflict.prototype.models = function(cb) {
|
||||||
|
|
||||||
function getSourceModel(cb) {
|
function getSourceModel(cb) {
|
||||||
SourceModel.findById(conflict.modelId, function(err, model) {
|
SourceModel.findById(conflict.modelId, function(err, model) {
|
||||||
if(err) return cb(err);
|
if (err) return cb(err);
|
||||||
source = model;
|
source = model;
|
||||||
cb();
|
cb();
|
||||||
});
|
});
|
||||||
|
@ -542,19 +523,19 @@ Conflict.prototype.models = function(cb) {
|
||||||
|
|
||||||
function getTargetModel(cb) {
|
function getTargetModel(cb) {
|
||||||
TargetModel.findById(conflict.modelId, function(err, model) {
|
TargetModel.findById(conflict.modelId, function(err, model) {
|
||||||
if(err) return cb(err);
|
if (err) return cb(err);
|
||||||
target = model;
|
target = model;
|
||||||
cb();
|
cb();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function done(err) {
|
function done(err) {
|
||||||
if(err) return cb(err);
|
if (err) return cb(err);
|
||||||
cb(null, source, target);
|
cb(null, source, target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the conflicting changes.
|
* Get the conflicting changes.
|
||||||
*
|
*
|
||||||
* @callback {Function} callback
|
* @callback {Function} callback
|
||||||
|
@ -563,7 +544,7 @@ Conflict.prototype.models = function(cb) {
|
||||||
* @param {Change} targetChange
|
* @param {Change} targetChange
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Conflict.prototype.changes = function(cb) {
|
Conflict.prototype.changes = function(cb) {
|
||||||
var conflict = this;
|
var conflict = this;
|
||||||
var sourceChange;
|
var sourceChange;
|
||||||
var targetChange;
|
var targetChange;
|
||||||
|
@ -577,7 +558,7 @@ Conflict.prototype.changes = function(cb) {
|
||||||
conflict.SourceChange.findOne({where: {
|
conflict.SourceChange.findOne({where: {
|
||||||
modelId: conflict.modelId
|
modelId: conflict.modelId
|
||||||
}}, function(err, change) {
|
}}, function(err, change) {
|
||||||
if(err) return cb(err);
|
if (err) return cb(err);
|
||||||
sourceChange = change;
|
sourceChange = change;
|
||||||
cb();
|
cb();
|
||||||
});
|
});
|
||||||
|
@ -587,35 +568,35 @@ Conflict.prototype.changes = function(cb) {
|
||||||
conflict.TargetChange.findOne({where: {
|
conflict.TargetChange.findOne({where: {
|
||||||
modelId: conflict.modelId
|
modelId: conflict.modelId
|
||||||
}}, function(err, change) {
|
}}, function(err, change) {
|
||||||
if(err) return cb(err);
|
if (err) return cb(err);
|
||||||
targetChange = change;
|
targetChange = change;
|
||||||
cb();
|
cb();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function done(err) {
|
function done(err) {
|
||||||
if(err) return cb(err);
|
if (err) return cb(err);
|
||||||
cb(null, sourceChange, targetChange);
|
cb(null, sourceChange, targetChange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the conflict.
|
* Resolve the conflict.
|
||||||
*
|
*
|
||||||
* @callback {Function} callback
|
* @callback {Function} callback
|
||||||
* @param {Error} err
|
* @param {Error} err
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Conflict.prototype.resolve = function(cb) {
|
Conflict.prototype.resolve = function(cb) {
|
||||||
var conflict = this;
|
var conflict = this;
|
||||||
conflict.changes(function(err, sourceChange, targetChange) {
|
conflict.changes(function(err, sourceChange, targetChange) {
|
||||||
if(err) return cb(err);
|
if (err) return cb(err);
|
||||||
sourceChange.prev = targetChange.rev;
|
sourceChange.prev = targetChange.rev;
|
||||||
sourceChange.save(cb);
|
sourceChange.save(cb);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine the conflict type.
|
* Determine the conflict type.
|
||||||
*
|
*
|
||||||
* Possible results are
|
* Possible results are
|
||||||
|
@ -629,18 +610,19 @@ Conflict.prototype.resolve = function(cb) {
|
||||||
* @param {String} type The conflict type.
|
* @param {String} type The conflict type.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Conflict.prototype.type = function(cb) {
|
Conflict.prototype.type = function(cb) {
|
||||||
var conflict = this;
|
var conflict = this;
|
||||||
this.changes(function(err, sourceChange, targetChange) {
|
this.changes(function(err, sourceChange, targetChange) {
|
||||||
if(err) return cb(err);
|
if (err) return cb(err);
|
||||||
var sourceChangeType = sourceChange.type();
|
var sourceChangeType = sourceChange.type();
|
||||||
var targetChangeType = targetChange.type();
|
var targetChangeType = targetChange.type();
|
||||||
if(sourceChangeType === Change.UPDATE && targetChangeType === Change.UPDATE) {
|
if (sourceChangeType === Change.UPDATE && targetChangeType === Change.UPDATE) {
|
||||||
return cb(null, Change.UPDATE);
|
return cb(null, Change.UPDATE);
|
||||||
}
|
}
|
||||||
if(sourceChangeType === Change.DELETE || targetChangeType === Change.DELETE) {
|
if (sourceChangeType === Change.DELETE || targetChangeType === Change.DELETE) {
|
||||||
return cb(null, Change.DELETE);
|
return cb(null, Change.DELETE);
|
||||||
}
|
}
|
||||||
return cb(null, Change.UNKNOWN);
|
return cb(null, Change.UNKNOWN);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "Change",
|
||||||
|
"trackChanges": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"id": true
|
||||||
|
},
|
||||||
|
"rev": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"prev": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"checkpoint": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"modelName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"modelId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,27 +2,7 @@
|
||||||
* Module Dependencies.
|
* Module Dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var PersistedModel = require('../../lib/loopback').PersistedModel
|
var assert = require('assert');
|
||||||
, loopback = require('../../lib/loopback')
|
|
||||||
, assert = require('assert');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Properties
|
|
||||||
*/
|
|
||||||
|
|
||||||
var properties = {
|
|
||||||
seq: {type: Number},
|
|
||||||
time: {type: Date, default: Date},
|
|
||||||
sourceId: {type: String}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options
|
|
||||||
*/
|
|
||||||
|
|
||||||
var options = {
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checkpoint list entry.
|
* Checkpoint list entry.
|
||||||
|
@ -31,47 +11,53 @@ var options = {
|
||||||
* @property time {Number} the time when the checkpoint was created
|
* @property time {Number} the time when the checkpoint was created
|
||||||
* @property sourceId {String} the source identifier
|
* @property sourceId {String} the source identifier
|
||||||
*
|
*
|
||||||
* @class
|
* @class Checkpoint
|
||||||
* @inherits {PersistedModel}
|
* @inherits {PersistedModel}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var Checkpoint = module.exports = PersistedModel.extend('Checkpoint', properties, options);
|
module.exports = function(Checkpoint) {
|
||||||
|
|
||||||
/**
|
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||||
|
Checkpoint.definition.rawProperties.time.default =
|
||||||
|
Checkpoint.definition.properties.time.default = function() {
|
||||||
|
return new Date();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
* Get the current checkpoint id
|
* Get the current checkpoint id
|
||||||
* @callback {Function} callback
|
* @callback {Function} callback
|
||||||
* @param {Error} err
|
* @param {Error} err
|
||||||
* @param {Number} checkpointId The current checkpoint id
|
* @param {Number} checkpointId The current checkpoint id
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Checkpoint.current = function(cb) {
|
Checkpoint.current = function(cb) {
|
||||||
var Checkpoint = this;
|
var Checkpoint = this;
|
||||||
this.find({
|
this.find({
|
||||||
limit: 1,
|
limit: 1,
|
||||||
order: 'seq DESC'
|
order: 'seq DESC'
|
||||||
}, function(err, checkpoints) {
|
}, function(err, checkpoints) {
|
||||||
if(err) return cb(err);
|
if (err) return cb(err);
|
||||||
var checkpoint = checkpoints[0];
|
var checkpoint = checkpoints[0];
|
||||||
if(checkpoint) {
|
if (checkpoint) {
|
||||||
cb(null, checkpoint.seq);
|
cb(null, checkpoint.seq);
|
||||||
} else {
|
} else {
|
||||||
Checkpoint.create({seq: 0}, function(err, checkpoint) {
|
Checkpoint.create({seq: 0}, function(err, checkpoint) {
|
||||||
if(err) return cb(err);
|
if (err) return cb(err);
|
||||||
cb(null, checkpoint.seq);
|
cb(null, checkpoint.seq);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Checkpoint.beforeSave = function(next, model) {
|
Checkpoint.beforeSave = function(next, model) {
|
||||||
if(!model.getId() && model.seq === undefined) {
|
if (!model.getId() && model.seq === undefined) {
|
||||||
model.constructor.current(function(err, seq) {
|
model.constructor.current(function(err, seq) {
|
||||||
if(err) return next(err);
|
if (err) return next(err);
|
||||||
model.seq = seq + 1;
|
model.seq = seq + 1;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"name": "Checkpoint",
|
||||||
|
"properties": {
|
||||||
|
"seq": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
"sourceId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,3 @@
|
||||||
/*!
|
|
||||||
* Module Dependencies.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var Model = require('../../lib/loopback').Model
|
|
||||||
, loopback = require('../../lib/loopback');
|
|
||||||
|
|
||||||
var properties = {
|
|
||||||
to: {type: String, required: true},
|
|
||||||
from: {type: String, required: true},
|
|
||||||
subject: {type: String, required: true},
|
|
||||||
text: {type: String},
|
|
||||||
html: {type: String}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property {String} to Email addressee. Required.
|
* @property {String} to Email addressee. Required.
|
||||||
* @property {String} from Email sender address. Required.
|
* @property {String} from Email sender address. Required.
|
||||||
|
@ -20,13 +5,13 @@ var properties = {
|
||||||
* @property {String} text Text body of email.
|
* @property {String} text Text body of email.
|
||||||
* @property {String} html HTML body of email.
|
* @property {String} html HTML body of email.
|
||||||
*
|
*
|
||||||
* @class
|
* @class Email
|
||||||
* @inherits {Model}
|
* @inherits {Model}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var Email = module.exports = Model.extend('Email', properties);
|
module.exports = function(Email) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send an email with the given `options`.
|
* Send an email with the given `options`.
|
||||||
*
|
*
|
||||||
* Example Options:
|
* Example Options:
|
||||||
|
@ -52,6 +37,14 @@ var Email = module.exports = Model.extend('Email', properties);
|
||||||
* @param {Function} callback Called after the e-mail is sent or the sending failed
|
* @param {Function} callback Called after the e-mail is sent or the sending failed
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Email.prototype.send = function() {
|
Email.send = function() {
|
||||||
throw new Error('You must connect the Email Model to a Mail connector');
|
throw new Error('You must connect the Email Model to a Mail connector');
|
||||||
}
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A shortcut for Email.send(this).
|
||||||
|
*/
|
||||||
|
Email.prototype.send = function() {
|
||||||
|
throw new Error('You must connect the Email Model to a Mail connector');
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"name": "Email",
|
||||||
|
"base": "Model",
|
||||||
|
"properties": {
|
||||||
|
"to": {"type": "String", "required": true},
|
||||||
|
"from": {"type": "String", "required": true},
|
||||||
|
"subject": {"type": "String", "required": true},
|
||||||
|
"text": {"type": "String"},
|
||||||
|
"html": {"type": "String"}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
/**
|
||||||
|
* The `RoleMapping` model extends from the built in `loopback.Model` type.
|
||||||
|
*
|
||||||
|
* @property {String} id Generated ID.
|
||||||
|
* @property {String} name Name of the role.
|
||||||
|
* @property {String} Description Text description.
|
||||||
|
*
|
||||||
|
* @class RoleMapping
|
||||||
|
* @inherits {PersistedModel}
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = function(RoleMapping) {
|
||||||
|
// Principal types
|
||||||
|
RoleMapping.USER = 'USER';
|
||||||
|
RoleMapping.APP = RoleMapping.APPLICATION = 'APP';
|
||||||
|
RoleMapping.ROLE = 'ROLE';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the application principal
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {Application} application
|
||||||
|
*/
|
||||||
|
RoleMapping.prototype.application = function (callback) {
|
||||||
|
if (this.principalType === RoleMapping.APPLICATION) {
|
||||||
|
var applicationModel = this.constructor.Application
|
||||||
|
|| loopback.getModelByType(loopback.Application);
|
||||||
|
applicationModel.findById(this.principalId, callback);
|
||||||
|
} else {
|
||||||
|
process.nextTick(function () {
|
||||||
|
callback && callback(null, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user principal
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {User} user
|
||||||
|
*/
|
||||||
|
RoleMapping.prototype.user = function (callback) {
|
||||||
|
if (this.principalType === RoleMapping.USER) {
|
||||||
|
var userModel = this.constructor.User
|
||||||
|
|| loopback.getModelByType(loopback.User);
|
||||||
|
userModel.findById(this.principalId, callback);
|
||||||
|
} else {
|
||||||
|
process.nextTick(function () {
|
||||||
|
callback && callback(null, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the child role principal
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {User} childUser
|
||||||
|
*/
|
||||||
|
RoleMapping.prototype.childRole = function (callback) {
|
||||||
|
if (this.principalType === RoleMapping.ROLE) {
|
||||||
|
var roleModel = this.constructor.Role || loopback.getModelByType(Role);
|
||||||
|
roleModel.findById(this.principalId, callback);
|
||||||
|
} else {
|
||||||
|
process.nextTick(function () {
|
||||||
|
callback && callback(null, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"name": "RoleMapping",
|
||||||
|
"description": "Map principals to roles",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"id": true,
|
||||||
|
"generated": true
|
||||||
|
},
|
||||||
|
"principalType": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The principal type, such as user, application, or role"
|
||||||
|
},
|
||||||
|
"principalId": "string"
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"role": {
|
||||||
|
"type": "belongsTo",
|
||||||
|
"model": "Role",
|
||||||
|
"foreignKey": "roleId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,171 +5,79 @@ var async = require('async');
|
||||||
|
|
||||||
var AccessContext = require('../../lib/access-context').AccessContext;
|
var AccessContext = require('../../lib/access-context').AccessContext;
|
||||||
|
|
||||||
// Role model
|
var RoleMapping = loopback.RoleMapping;
|
||||||
var RoleSchema = {
|
assert(RoleMapping, 'RoleMapping model must be defined before Role model');
|
||||||
id: {type: String, id: true, generated: true}, // Id
|
|
||||||
name: {type: String, required: true}, // The name of a role
|
|
||||||
description: String, // Description
|
|
||||||
|
|
||||||
// Timestamps
|
|
||||||
created: {type: Date, default: Date},
|
|
||||||
modified: {type: Date, default: Date}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Map principals to roles
|
|
||||||
*/
|
|
||||||
var RoleMappingSchema = {
|
|
||||||
id: {type: String, id: true, generated: true}, // Id
|
|
||||||
// roleId: String, // The role id, to be injected by the belongsTo relation
|
|
||||||
principalType: String, // The principal type, such as user, application, or role
|
|
||||||
principalId: String // The principal id
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `RoleMapping` model extends from the built in `loopback.Model` type.
|
|
||||||
*
|
|
||||||
* @class
|
|
||||||
* @property {String} id Generated ID.
|
|
||||||
* @property {String} name Name of the role.
|
|
||||||
* @property {String} Description Text description.
|
|
||||||
* @inherits {Model}
|
|
||||||
*/
|
|
||||||
|
|
||||||
var RoleMapping = loopback.createModel('RoleMapping', RoleMappingSchema, {
|
|
||||||
relations: {
|
|
||||||
role: {
|
|
||||||
type: 'belongsTo',
|
|
||||||
model: 'Role',
|
|
||||||
foreignKey: 'roleId'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Principal types
|
|
||||||
RoleMapping.USER = 'USER';
|
|
||||||
RoleMapping.APP = RoleMapping.APPLICATION = 'APP';
|
|
||||||
RoleMapping.ROLE = 'ROLE';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the application principal
|
|
||||||
* @callback {Function} callback
|
|
||||||
* @param {Error} err
|
|
||||||
* @param {Application} application
|
|
||||||
*/
|
|
||||||
RoleMapping.prototype.application = function (callback) {
|
|
||||||
if (this.principalType === RoleMapping.APPLICATION) {
|
|
||||||
var applicationModel = this.constructor.Application
|
|
||||||
|| loopback.getModelByType(loopback.Application);
|
|
||||||
applicationModel.findById(this.principalId, callback);
|
|
||||||
} else {
|
|
||||||
process.nextTick(function () {
|
|
||||||
callback && callback(null, null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the user principal
|
|
||||||
* @callback {Function} callback
|
|
||||||
* @param {Error} err
|
|
||||||
* @param {User} user
|
|
||||||
*/
|
|
||||||
RoleMapping.prototype.user = function (callback) {
|
|
||||||
if (this.principalType === RoleMapping.USER) {
|
|
||||||
var userModel = this.constructor.User
|
|
||||||
|| loopback.getModelByType(loopback.User);
|
|
||||||
userModel.findById(this.principalId, callback);
|
|
||||||
} else {
|
|
||||||
process.nextTick(function () {
|
|
||||||
callback && callback(null, null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the child role principal
|
|
||||||
* @callback {Function} callback
|
|
||||||
* @param {Error} err
|
|
||||||
* @param {User} childUser
|
|
||||||
*/
|
|
||||||
RoleMapping.prototype.childRole = function (callback) {
|
|
||||||
if (this.principalType === RoleMapping.ROLE) {
|
|
||||||
var roleModel = this.constructor.Role || loopback.getModelByType(Role);
|
|
||||||
roleModel.findById(this.principalId, callback);
|
|
||||||
} else {
|
|
||||||
process.nextTick(function () {
|
|
||||||
callback && callback(null, null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Role Model
|
* The Role Model
|
||||||
* @class
|
* @class Role
|
||||||
*/
|
*/
|
||||||
var Role = loopback.createModel('Role', RoleSchema, {
|
module.exports = function(Role) {
|
||||||
relations: {
|
|
||||||
principals: {
|
|
||||||
type: 'hasMany',
|
|
||||||
model: 'RoleMapping',
|
|
||||||
foreignKey: 'roleId'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set up the connection to users/applications/roles once the model
|
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||||
Role.once('dataSourceAttached', function () {
|
Role.definition.rawProperties.created.default =
|
||||||
|
Role.definition.properties.created.default = function() {
|
||||||
|
return new Date();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||||
|
Role.definition.rawProperties.modified.default =
|
||||||
|
Role.definition.properties.modified.default = function() {
|
||||||
|
return new Date();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set up the connection to users/applications/roles once the model
|
||||||
|
Role.once('dataSourceAttached', function() {
|
||||||
var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping);
|
var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping);
|
||||||
Role.prototype.users = function (callback) {
|
Role.prototype.users = function(callback) {
|
||||||
roleMappingModel.find({where: {roleId: this.id,
|
roleMappingModel.find({where: {roleId: this.id,
|
||||||
principalType: RoleMapping.USER}}, function (err, mappings) {
|
principalType: RoleMapping.USER}}, function(err, mappings) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback && callback(err);
|
callback && callback(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return mappings.map(function (m) {
|
return mappings.map(function(m) {
|
||||||
return m.principalId;
|
return m.principalId;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Role.prototype.applications = function (callback) {
|
Role.prototype.applications = function(callback) {
|
||||||
roleMappingModel.find({where: {roleId: this.id,
|
roleMappingModel.find({where: {roleId: this.id,
|
||||||
principalType: RoleMapping.APPLICATION}}, function (err, mappings) {
|
principalType: RoleMapping.APPLICATION}}, function(err, mappings) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback && callback(err);
|
callback && callback(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return mappings.map(function (m) {
|
return mappings.map(function(m) {
|
||||||
return m.principalId;
|
return m.principalId;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Role.prototype.roles = function (callback) {
|
Role.prototype.roles = function(callback) {
|
||||||
roleMappingModel.find({where: {roleId: this.id,
|
roleMappingModel.find({where: {roleId: this.id,
|
||||||
principalType: RoleMapping.ROLE}}, function (err, mappings) {
|
principalType: RoleMapping.ROLE}}, function(err, mappings) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback && callback(err);
|
callback && callback(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return mappings.map(function (m) {
|
return mappings.map(function(m) {
|
||||||
return m.principalId;
|
return m.principalId;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Special roles
|
// Special roles
|
||||||
Role.OWNER = '$owner'; // owner of the object
|
Role.OWNER = '$owner'; // owner of the object
|
||||||
Role.RELATED = "$related"; // any User with a relationship to the object
|
Role.RELATED = "$related"; // any User with a relationship to the object
|
||||||
Role.AUTHENTICATED = "$authenticated"; // authenticated user
|
Role.AUTHENTICATED = "$authenticated"; // authenticated user
|
||||||
Role.UNAUTHENTICATED = "$unauthenticated"; // authenticated user
|
Role.UNAUTHENTICATED = "$unauthenticated"; // authenticated user
|
||||||
Role.EVERYONE = "$everyone"; // everyone
|
Role.EVERYONE = "$everyone"; // everyone
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add custom handler for roles
|
* Add custom handler for roles
|
||||||
* @param role
|
* @param role
|
||||||
* @param resolver The resolver function decides if a principal is in the role
|
* @param resolver The resolver function decides if a principal is in the role
|
||||||
|
@ -177,15 +85,15 @@ Role.EVERYONE = "$everyone"; // everyone
|
||||||
*
|
*
|
||||||
* function(role, context, callback)
|
* function(role, context, callback)
|
||||||
*/
|
*/
|
||||||
Role.registerResolver = function(role, resolver) {
|
Role.registerResolver = function(role, resolver) {
|
||||||
if(!Role.resolvers) {
|
if (!Role.resolvers) {
|
||||||
Role.resolvers = {};
|
Role.resolvers = {};
|
||||||
}
|
}
|
||||||
Role.resolvers[role] = resolver;
|
Role.resolvers[role] = resolver;
|
||||||
};
|
};
|
||||||
|
|
||||||
Role.registerResolver(Role.OWNER, function(role, context, callback) {
|
Role.registerResolver(Role.OWNER, function(role, context, callback) {
|
||||||
if(!context || !context.model || !context.modelId) {
|
if (!context || !context.model || !context.modelId) {
|
||||||
process.nextTick(function() {
|
process.nextTick(function() {
|
||||||
callback && callback(null, false);
|
callback && callback(null, false);
|
||||||
});
|
});
|
||||||
|
@ -195,40 +103,40 @@ Role.registerResolver(Role.OWNER, function(role, context, callback) {
|
||||||
var modelId = context.modelId;
|
var modelId = context.modelId;
|
||||||
var userId = context.getUserId();
|
var userId = context.getUserId();
|
||||||
Role.isOwner(modelClass, modelId, userId, callback);
|
Role.isOwner(modelClass, modelId, userId, callback);
|
||||||
});
|
});
|
||||||
|
|
||||||
function isUserClass(modelClass) {
|
function isUserClass(modelClass) {
|
||||||
return modelClass === loopback.User ||
|
return modelClass === loopback.User ||
|
||||||
modelClass.prototype instanceof loopback.User;
|
modelClass.prototype instanceof loopback.User;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Check if two user ids matches
|
* Check if two user ids matches
|
||||||
* @param {*} id1
|
* @param {*} id1
|
||||||
* @param {*} id2
|
* @param {*} id2
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
function matches(id1, id2) {
|
function matches(id1, id2) {
|
||||||
if (id1 === undefined || id1 === null || id1 ===''
|
if (id1 === undefined || id1 === null || id1 === ''
|
||||||
|| id2 === undefined || id2 === null || id2 === '') {
|
|| id2 === undefined || id2 === null || id2 === '') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// The id can be a MongoDB ObjectID
|
// The id can be a MongoDB ObjectID
|
||||||
return id1 === id2 || id1.toString() === id2.toString();
|
return id1 === id2 || id1.toString() === id2.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a given userId is the owner the model instance
|
* Check if a given userId is the owner the model instance
|
||||||
* @param {Function} modelClass The model class
|
* @param {Function} modelClass The model class
|
||||||
* @param {*} modelId The model id
|
* @param {*} modelId The model id
|
||||||
* @param {*) userId The user id
|
* @param {*) userId The user id
|
||||||
* @param {Function} callback
|
* @param {Function} callback
|
||||||
*/
|
*/
|
||||||
Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
||||||
assert(modelClass, 'Model class is required');
|
assert(modelClass, 'Model class is required');
|
||||||
debug('isOwner(): %s %s userId: %s', modelClass && modelClass.modelName, modelId, userId);
|
debug('isOwner(): %s %s userId: %s', modelClass && modelClass.modelName, modelId, userId);
|
||||||
// No userId is present
|
// No userId is present
|
||||||
if(!userId) {
|
if (!userId) {
|
||||||
process.nextTick(function() {
|
process.nextTick(function() {
|
||||||
callback(null, false);
|
callback(null, false);
|
||||||
});
|
});
|
||||||
|
@ -236,7 +144,7 @@ Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is the modelClass User or a subclass of User?
|
// Is the modelClass User or a subclass of User?
|
||||||
if(isUserClass(modelClass)) {
|
if (isUserClass(modelClass)) {
|
||||||
process.nextTick(function() {
|
process.nextTick(function() {
|
||||||
callback(null, matches(modelId, userId));
|
callback(null, matches(modelId, userId));
|
||||||
});
|
});
|
||||||
|
@ -244,24 +152,24 @@ Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
modelClass.findById(modelId, function(err, inst) {
|
modelClass.findById(modelId, function(err, inst) {
|
||||||
if(err || !inst) {
|
if (err || !inst) {
|
||||||
debug('Model not found for id %j', modelId);
|
debug('Model not found for id %j', modelId);
|
||||||
callback && callback(err, false);
|
callback && callback(err, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
debug('Model found: %j', inst);
|
debug('Model found: %j', inst);
|
||||||
var ownerId = inst.userId || inst.owner;
|
var ownerId = inst.userId || inst.owner;
|
||||||
if(ownerId) {
|
if (ownerId) {
|
||||||
callback && callback(null, matches(ownerId, userId));
|
callback && callback(null, matches(ownerId, userId));
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// Try to follow belongsTo
|
// Try to follow belongsTo
|
||||||
for(var r in modelClass.relations) {
|
for (var r in modelClass.relations) {
|
||||||
var rel = modelClass.relations[r];
|
var rel = modelClass.relations[r];
|
||||||
if(rel.type === 'belongsTo' && isUserClass(rel.modelTo)) {
|
if (rel.type === 'belongsTo' && isUserClass(rel.modelTo)) {
|
||||||
debug('Checking relation %s to %s: %j', r, rel.modelTo.modelName, rel);
|
debug('Checking relation %s to %s: %j', r, rel.modelTo.modelName, rel);
|
||||||
inst[r](function(err, user) {
|
inst[r](function(err, user) {
|
||||||
if(!err && user) {
|
if (!err && user) {
|
||||||
debug('User found: %j', user.id);
|
debug('User found: %j', user.id);
|
||||||
callback && callback(null, matches(user.id, userId));
|
callback && callback(null, matches(user.id, userId));
|
||||||
} else {
|
} else {
|
||||||
|
@ -275,44 +183,44 @@ Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
||||||
callback && callback(null, false);
|
callback && callback(null, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) {
|
Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) {
|
||||||
if(!context) {
|
if (!context) {
|
||||||
process.nextTick(function() {
|
process.nextTick(function() {
|
||||||
callback && callback(null, false);
|
callback && callback(null, false);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Role.isAuthenticated(context, callback);
|
Role.isAuthenticated(context, callback);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the user id is authenticated
|
* Check if the user id is authenticated
|
||||||
* @param {Object} context The security context
|
* @param {Object} context The security context
|
||||||
* @callback {Function} callback
|
* @callback {Function} callback
|
||||||
* @param {Error} err
|
* @param {Error} err
|
||||||
* @param {Boolean} isAuthenticated
|
* @param {Boolean} isAuthenticated
|
||||||
*/
|
*/
|
||||||
Role.isAuthenticated = function isAuthenticated(context, callback) {
|
Role.isAuthenticated = function isAuthenticated(context, callback) {
|
||||||
process.nextTick(function() {
|
process.nextTick(function() {
|
||||||
callback && callback(null, context.isAuthenticated());
|
callback && callback(null, context.isAuthenticated());
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Role.registerResolver(Role.UNAUTHENTICATED, function(role, context, callback) {
|
Role.registerResolver(Role.UNAUTHENTICATED, function(role, context, callback) {
|
||||||
process.nextTick(function() {
|
process.nextTick(function() {
|
||||||
callback && callback(null, !context || !context.isAuthenticated());
|
callback && callback(null, !context || !context.isAuthenticated());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Role.registerResolver(Role.EVERYONE, function (role, context, callback) {
|
Role.registerResolver(Role.EVERYONE, function(role, context, callback) {
|
||||||
process.nextTick(function () {
|
process.nextTick(function() {
|
||||||
callback && callback(null, true); // Always true
|
callback && callback(null, true); // Always true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a given principal is in the role
|
* Check if a given principal is in the role
|
||||||
*
|
*
|
||||||
* @param {String} role The role name
|
* @param {String} role The role name
|
||||||
|
@ -321,7 +229,7 @@ Role.registerResolver(Role.EVERYONE, function (role, context, callback) {
|
||||||
* @param {Error} err
|
* @param {Error} err
|
||||||
* @param {Boolean} isInRole
|
* @param {Boolean} isInRole
|
||||||
*/
|
*/
|
||||||
Role.isInRole = function (role, context, callback) {
|
Role.isInRole = function(role, context, callback) {
|
||||||
if (!(context instanceof AccessContext)) {
|
if (!(context instanceof AccessContext)) {
|
||||||
context = new AccessContext(context);
|
context = new AccessContext(context);
|
||||||
}
|
}
|
||||||
|
@ -338,13 +246,13 @@ Role.isInRole = function (role, context, callback) {
|
||||||
|
|
||||||
if (context.principals.length === 0) {
|
if (context.principals.length === 0) {
|
||||||
debug('isInRole() returns: false');
|
debug('isInRole() returns: false');
|
||||||
process.nextTick(function () {
|
process.nextTick(function() {
|
||||||
callback && callback(null, false);
|
callback && callback(null, false);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var inRole = context.principals.some(function (p) {
|
var inRole = context.principals.some(function(p) {
|
||||||
|
|
||||||
var principalType = p.type || undefined;
|
var principalType = p.type || undefined;
|
||||||
var principalId = p.id || undefined;
|
var principalId = p.id || undefined;
|
||||||
|
@ -355,14 +263,14 @@ Role.isInRole = function (role, context, callback) {
|
||||||
|
|
||||||
if (inRole) {
|
if (inRole) {
|
||||||
debug('isInRole() returns: %j', inRole);
|
debug('isInRole() returns: %j', inRole);
|
||||||
process.nextTick(function () {
|
process.nextTick(function() {
|
||||||
callback && callback(null, true);
|
callback && callback(null, true);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping);
|
var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping);
|
||||||
this.findOne({where: {name: role}}, function (err, result) {
|
this.findOne({where: {name: role}}, function(err, result) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback && callback(err);
|
callback && callback(err);
|
||||||
return;
|
return;
|
||||||
|
@ -374,36 +282,36 @@ Role.isInRole = function (role, context, callback) {
|
||||||
debug('Role found: %j', result);
|
debug('Role found: %j', result);
|
||||||
|
|
||||||
// Iterate through the list of principals
|
// Iterate through the list of principals
|
||||||
async.some(context.principals, function (p, done) {
|
async.some(context.principals, function(p, done) {
|
||||||
var principalType = p.type || undefined;
|
var principalType = p.type || undefined;
|
||||||
var principalId = p.id || undefined;
|
var principalId = p.id || undefined;
|
||||||
var roleId = result.id.toString();
|
var roleId = result.id.toString();
|
||||||
|
|
||||||
if(principalId !== null && principalId !== undefined && (typeof principalId !== 'string') ) {
|
if (principalId !== null && principalId !== undefined && (typeof principalId !== 'string')) {
|
||||||
principalId = principalId.toString();
|
principalId = principalId.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (principalType && principalId) {
|
if (principalType && principalId) {
|
||||||
roleMappingModel.findOne({where: {roleId: roleId,
|
roleMappingModel.findOne({where: {roleId: roleId,
|
||||||
principalType: principalType, principalId: principalId}},
|
principalType: principalType, principalId: principalId}},
|
||||||
function (err, result) {
|
function(err, result) {
|
||||||
debug('Role mapping found: %j', result);
|
debug('Role mapping found: %j', result);
|
||||||
done(!err && result); // The only arg is the result
|
done(!err && result); // The only arg is the result
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
process.nextTick(function () {
|
process.nextTick(function() {
|
||||||
done(false);
|
done(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, function (inRole) {
|
}, function(inRole) {
|
||||||
debug('isInRole() returns: %j', inRole);
|
debug('isInRole() returns: %j', inRole);
|
||||||
callback && callback(null, inRole);
|
callback && callback(null, inRole);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List roles for a given principal
|
* List roles for a given principal
|
||||||
* @param {Object} context The security context
|
* @param {Object} context The security context
|
||||||
* @param {Function} callback
|
* @param {Function} callback
|
||||||
|
@ -412,13 +320,13 @@ Role.isInRole = function (role, context, callback) {
|
||||||
* @param err
|
* @param err
|
||||||
* @param {String[]} An array of role ids
|
* @param {String[]} An array of role ids
|
||||||
*/
|
*/
|
||||||
Role.getRoles = function (context, callback) {
|
Role.getRoles = function(context, callback) {
|
||||||
if(!(context instanceof AccessContext)) {
|
if (!(context instanceof AccessContext)) {
|
||||||
context = new AccessContext(context);
|
context = new AccessContext(context);
|
||||||
}
|
}
|
||||||
var roles = [];
|
var roles = [];
|
||||||
|
|
||||||
var addRole = function (role) {
|
var addRole = function(role) {
|
||||||
if (role && roles.indexOf(role) === -1) {
|
if (role && roles.indexOf(role) === -1) {
|
||||||
roles.push(role);
|
roles.push(role);
|
||||||
}
|
}
|
||||||
|
@ -427,10 +335,10 @@ Role.getRoles = function (context, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
// Check against the smart roles
|
// Check against the smart roles
|
||||||
var inRoleTasks = [];
|
var inRoleTasks = [];
|
||||||
Object.keys(Role.resolvers).forEach(function (role) {
|
Object.keys(Role.resolvers).forEach(function(role) {
|
||||||
inRoleTasks.push(function (done) {
|
inRoleTasks.push(function(done) {
|
||||||
self.isInRole(role, context, function (err, inRole) {
|
self.isInRole(role, context, function(err, inRole) {
|
||||||
if(debug.enabled) {
|
if (debug.enabled) {
|
||||||
debug('In role %j: %j', role, inRole);
|
debug('In role %j: %j', role, inRole);
|
||||||
}
|
}
|
||||||
if (!err && inRole) {
|
if (!err && inRole) {
|
||||||
|
@ -444,7 +352,7 @@ Role.getRoles = function (context, callback) {
|
||||||
});
|
});
|
||||||
|
|
||||||
var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping);
|
var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping);
|
||||||
context.principals.forEach(function (p) {
|
context.principals.forEach(function(p) {
|
||||||
// Check against the role mappings
|
// Check against the role mappings
|
||||||
var principalType = p.type || undefined;
|
var principalType = p.type || undefined;
|
||||||
var principalId = p.id || undefined;
|
var principalId = p.id || undefined;
|
||||||
|
@ -456,15 +364,15 @@ Role.getRoles = function (context, callback) {
|
||||||
|
|
||||||
if (principalType && principalId) {
|
if (principalType && principalId) {
|
||||||
// Please find() treat undefined matches all values
|
// Please find() treat undefined matches all values
|
||||||
inRoleTasks.push(function (done) {
|
inRoleTasks.push(function(done) {
|
||||||
roleMappingModel.find({where: {principalType: principalType,
|
roleMappingModel.find({where: {principalType: principalType,
|
||||||
principalId: principalId}}, function (err, mappings) {
|
principalId: principalId}}, function(err, mappings) {
|
||||||
debug('Role mappings found: %s %j', err, mappings);
|
debug('Role mappings found: %s %j', err, mappings);
|
||||||
if (err) {
|
if (err) {
|
||||||
done && done(err);
|
done && done(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mappings.forEach(function (m) {
|
mappings.forEach(function(m) {
|
||||||
addRole(m.roleId);
|
addRole(m.roleId);
|
||||||
});
|
});
|
||||||
done && done();
|
done && done();
|
||||||
|
@ -473,16 +381,9 @@ Role.getRoles = function (context, callback) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async.parallel(inRoleTasks, function (err, results) {
|
async.parallel(inRoleTasks, function(err, results) {
|
||||||
debug('getRoles() returns: %j %j', err, roles);
|
debug('getRoles() returns: %j %j', err, roles);
|
||||||
callback && callback(err, roles);
|
callback && callback(err, roles);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
Role: Role,
|
|
||||||
RoleMapping: RoleMapping
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "Role",
|
||||||
|
"properties": {
|
||||||
|
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"id": true,
|
||||||
|
"generated": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"description": "string",
|
||||||
|
|
||||||
|
"created": "date",
|
||||||
|
"modified": "date"
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"principals": {
|
||||||
|
"type": "hasMany",
|
||||||
|
"model": "RoleMapping",
|
||||||
|
"foreignKey": "roleId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
var assert = require('assert');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource owner grants/delegates permissions to client applications
|
||||||
|
*
|
||||||
|
* For a protected resource, does the client application have the authorization
|
||||||
|
* from the resource owner (user or system)?
|
||||||
|
*
|
||||||
|
* Scope has many resource access entries
|
||||||
|
*
|
||||||
|
* @class Scope
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = function(Scope) {
|
||||||
|
/**
|
||||||
|
* Check if the given scope is allowed to access the model/property
|
||||||
|
* @param {String} scope The scope name
|
||||||
|
* @param {String} model The model name
|
||||||
|
* @param {String} property The property/method/relation name
|
||||||
|
* @param {String} accessType The access type
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {String|Error} err The error object
|
||||||
|
* @param {AccessRequest} result The access permission
|
||||||
|
*/
|
||||||
|
Scope.checkPermission = function (scope, model, property, accessType, callback) {
|
||||||
|
var ACL = loopback.ACL;
|
||||||
|
assert(ACL,
|
||||||
|
'ACL model must be defined before Scope.checkPermission is called');
|
||||||
|
|
||||||
|
this.findOne({where: {name: scope}}, function (err, scope) {
|
||||||
|
if (err) {
|
||||||
|
callback && callback(err);
|
||||||
|
} else {
|
||||||
|
var aclModel = loopback.getModelByType(ACL);
|
||||||
|
aclModel.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"name": "Scope",
|
||||||
|
"description": [
|
||||||
|
"Schema for Scope which represents the permissions that are granted",
|
||||||
|
"to client applications by the resource owner"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"description": "string"
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,107 +2,18 @@
|
||||||
* Module Dependencies.
|
* Module Dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var PersistedModel = require('../../lib/loopback').PersistedModel
|
var loopback = require('../../lib/loopback')
|
||||||
, loopback = require('../../lib/loopback')
|
|
||||||
, path = require('path')
|
, path = require('path')
|
||||||
, SALT_WORK_FACTOR = 10
|
, SALT_WORK_FACTOR = 10
|
||||||
, crypto = require('crypto')
|
, crypto = require('crypto')
|
||||||
, bcrypt = require('bcryptjs')
|
, bcrypt = require('bcryptjs')
|
||||||
, 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_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
|
||||||
, Role = require('./role').Role
|
|
||||||
, ACL = require('./acl').ACL
|
|
||||||
, assert = require('assert');
|
, assert = require('assert');
|
||||||
|
|
||||||
var debug = require('debug')('loopback:user');
|
var debug = require('debug')('loopback:user');
|
||||||
|
|
||||||
/*!
|
|
||||||
* Default User properties.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var properties = {
|
|
||||||
realm: {type: String},
|
|
||||||
username: {type: String},
|
|
||||||
password: {type: String, required: true},
|
|
||||||
credentials: Object, // deprecated, to be removed in 2.x
|
|
||||||
challenges: Object, // deprecated, to be removed in 2.x
|
|
||||||
email: {type: String, required: true},
|
|
||||||
emailVerified: Boolean,
|
|
||||||
verificationToken: String,
|
|
||||||
status: String,
|
|
||||||
created: Date,
|
|
||||||
lastUpdated: Date
|
|
||||||
};
|
|
||||||
|
|
||||||
var options = {
|
|
||||||
hidden: ['password'],
|
|
||||||
acls: [
|
|
||||||
{
|
|
||||||
principalType: ACL.ROLE,
|
|
||||||
principalId: Role.EVERYONE,
|
|
||||||
permission: ACL.DENY
|
|
||||||
},
|
|
||||||
{
|
|
||||||
principalType: ACL.ROLE,
|
|
||||||
principalId: Role.EVERYONE,
|
|
||||||
permission: ACL.ALLOW,
|
|
||||||
property: 'create'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
principalType: ACL.ROLE,
|
|
||||||
principalId: Role.OWNER,
|
|
||||||
permission: ACL.ALLOW,
|
|
||||||
property: 'deleteById'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
principalType: ACL.ROLE,
|
|
||||||
principalId: Role.EVERYONE,
|
|
||||||
permission: ACL.ALLOW,
|
|
||||||
property: "login"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
principalType: ACL.ROLE,
|
|
||||||
principalId: Role.EVERYONE,
|
|
||||||
permission: ACL.ALLOW,
|
|
||||||
property: "logout"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
principalType: ACL.ROLE,
|
|
||||||
principalId: Role.OWNER,
|
|
||||||
permission: ACL.ALLOW,
|
|
||||||
property: "findById"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
principalType: ACL.ROLE,
|
|
||||||
principalId: Role.OWNER,
|
|
||||||
permission: ACL.ALLOW,
|
|
||||||
property: "updateAttributes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
principalType: ACL.ROLE,
|
|
||||||
principalId: Role.EVERYONE,
|
|
||||||
permission: ACL.ALLOW,
|
|
||||||
property: "confirm"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
principalType: ACL.ROLE,
|
|
||||||
principalId: Role.EVERYONE,
|
|
||||||
permission: ACL.ALLOW,
|
|
||||||
property: "resetPassword",
|
|
||||||
accessType: ACL.EXECUTE
|
|
||||||
}
|
|
||||||
],
|
|
||||||
relations: {
|
|
||||||
accessTokens: {
|
|
||||||
type: 'hasMany',
|
|
||||||
model: 'AccessToken',
|
|
||||||
foreignKey: 'userId'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extends from the built in `loopback.Model` type.
|
* Extends from the built in `loopback.Model` type.
|
||||||
*
|
*
|
||||||
|
@ -122,11 +33,11 @@ var options = {
|
||||||
* @property {Boolean} emailVerified Set when a user's email has been verified via `confirm()`
|
* @property {Boolean} emailVerified Set when a user's email has been verified via `confirm()`
|
||||||
* @property {String} verificationToken Set when `verify()` is called
|
* @property {String} verificationToken Set when `verify()` is called
|
||||||
*
|
*
|
||||||
* @class
|
* @class User
|
||||||
* @inherits {Model}
|
* @inherits {PersistedModel}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var User = module.exports = PersistedModel.extend('User', properties, options);
|
module.exports = function(User) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create access token for the logged in user. This method can be overridden to
|
* Create access token for the logged in user. This method can be overridden to
|
||||||
|
@ -150,8 +61,8 @@ User.prototype.createAccessToken = function(ttl, cb) {
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
* User.login({username: 'foo', password: 'bar'}, function (err, token) {
|
* User.login({username: 'foo', password: 'bar'}, function (err, token) {
|
||||||
* console.log(token.id);
|
* console.log(token.id);
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param {Object} credentials
|
* @param {Object} credentials
|
||||||
|
@ -160,7 +71,7 @@ User.prototype.createAccessToken = function(ttl, cb) {
|
||||||
* @param {AccessToken} token
|
* @param {AccessToken} token
|
||||||
*/
|
*/
|
||||||
|
|
||||||
User.login = function (credentials, include, fn) {
|
User.login = function(credentials, include, fn) {
|
||||||
var self = this;
|
var self = this;
|
||||||
if (typeof include === 'function') {
|
if (typeof include === 'function') {
|
||||||
fn = include;
|
fn = include;
|
||||||
|
@ -169,19 +80,18 @@ User.login = function (credentials, include, fn) {
|
||||||
|
|
||||||
include = (include || '');
|
include = (include || '');
|
||||||
if (Array.isArray(include)) {
|
if (Array.isArray(include)) {
|
||||||
include = include.map(function ( val ) {
|
include = include.map(function(val) {
|
||||||
return val.toLowerCase();
|
return val.toLowerCase();
|
||||||
});
|
});
|
||||||
}else {
|
} else {
|
||||||
include = include.toLowerCase();
|
include = include.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var query = {};
|
var query = {};
|
||||||
if(credentials.email) {
|
if (credentials.email) {
|
||||||
query.email = credentials.email;
|
query.email = credentials.email;
|
||||||
} else if(credentials.username) {
|
} else if (credentials.username) {
|
||||||
query.username = credentials.username;
|
query.username = credentials.username;
|
||||||
} else {
|
} else {
|
||||||
var err = new Error('username or email is required');
|
var err = new Error('username or email is required');
|
||||||
|
@ -193,10 +103,10 @@ User.login = function (credentials, include, fn) {
|
||||||
var defaultError = new Error('login failed');
|
var defaultError = new Error('login failed');
|
||||||
defaultError.statusCode = 401;
|
defaultError.statusCode = 401;
|
||||||
|
|
||||||
if(err) {
|
if (err) {
|
||||||
debug('An error is reported from User.findOne: %j', err);
|
debug('An error is reported from User.findOne: %j', err);
|
||||||
fn(defaultError);
|
fn(defaultError);
|
||||||
} else if(user) {
|
} else if (user) {
|
||||||
if (self.settings.emailVerificationRequired) {
|
if (self.settings.emailVerificationRequired) {
|
||||||
if (!user.emailVerified) {
|
if (!user.emailVerified) {
|
||||||
// Fail to log in if email verification is not done yet
|
// Fail to log in if email verification is not done yet
|
||||||
|
@ -207,10 +117,10 @@ User.login = function (credentials, include, fn) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
user.hasPassword(credentials.password, function(err, isMatch) {
|
user.hasPassword(credentials.password, function(err, isMatch) {
|
||||||
if(err) {
|
if (err) {
|
||||||
debug('An error is reported from User.hasPassword: %j', err);
|
debug('An error is reported from User.hasPassword: %j', err);
|
||||||
fn(defaultError);
|
fn(defaultError);
|
||||||
} else if(isMatch) {
|
} else if (isMatch) {
|
||||||
user.createAccessToken(credentials.ttl, function(err, token) {
|
user.createAccessToken(credentials.ttl, function(err, token) {
|
||||||
if (err) return fn(err);
|
if (err) return fn(err);
|
||||||
if (Array.isArray(include) ? include.indexOf('user') !== -1 : include === 'user') {
|
if (Array.isArray(include) ? include.indexOf('user') !== -1 : include === 'user') {
|
||||||
|
@ -241,8 +151,8 @@ User.login = function (credentials, include, fn) {
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
* User.logout('asd0a9f8dsj9s0s3223mk', function (err) {
|
* User.logout('asd0a9f8dsj9s0s3223mk', function (err) {
|
||||||
* console.log(err || 'Logged out');
|
* console.log(err || 'Logged out');
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param {String} accessTokenID
|
* @param {String} accessTokenID
|
||||||
|
@ -250,11 +160,11 @@ User.login = function (credentials, include, fn) {
|
||||||
* @param {Error} err
|
* @param {Error} err
|
||||||
*/
|
*/
|
||||||
|
|
||||||
User.logout = function (tokenId, fn) {
|
User.logout = function(tokenId, fn) {
|
||||||
this.relations.accessTokens.modelTo.findById(tokenId, function (err, accessToken) {
|
this.relations.accessTokens.modelTo.findById(tokenId, function(err, accessToken) {
|
||||||
if(err) {
|
if (err) {
|
||||||
fn(err);
|
fn(err);
|
||||||
} else if(accessToken) {
|
} else if (accessToken) {
|
||||||
accessToken.destroy(fn);
|
accessToken.destroy(fn);
|
||||||
} else {
|
} else {
|
||||||
fn(new Error('could not find accessToken'));
|
fn(new Error('could not find accessToken'));
|
||||||
|
@ -269,10 +179,10 @@ User.logout = function (tokenId, fn) {
|
||||||
* @returns {Boolean}
|
* @returns {Boolean}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
User.prototype.hasPassword = function (plain, fn) {
|
User.prototype.hasPassword = function(plain, fn) {
|
||||||
if(this.password && plain) {
|
if (this.password && plain) {
|
||||||
bcrypt.compare(plain, this.password, function(err, isMatch) {
|
bcrypt.compare(plain, this.password, function(err, isMatch) {
|
||||||
if(err) return fn(err);
|
if (err) return fn(err);
|
||||||
fn(null, isMatch);
|
fn(null, isMatch);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -285,11 +195,11 @@ User.prototype.hasPassword = function (plain, fn) {
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
* var options = {
|
* var options = {
|
||||||
* type: 'email',
|
* type: 'email',
|
||||||
* to: user.email,
|
* to: user.email,
|
||||||
* template: 'verify.ejs',
|
* template: 'verify.ejs',
|
||||||
* redirect: '/'
|
* redirect: '/'
|
||||||
* };
|
* };
|
||||||
*
|
*
|
||||||
* user.verify(options, next);
|
* user.verify(options, next);
|
||||||
* ```
|
* ```
|
||||||
|
@ -297,7 +207,7 @@ User.prototype.hasPassword = function (plain, fn) {
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
*/
|
*/
|
||||||
|
|
||||||
User.prototype.verify = function (options, fn) {
|
User.prototype.verify = function(options, fn) {
|
||||||
var user = this;
|
var user = this;
|
||||||
var userModel = this.constructor;
|
var userModel = this.constructor;
|
||||||
assert(typeof options === 'object', 'options required when calling user.verify()');
|
assert(typeof options === 'object', 'options required when calling user.verify()');
|
||||||
|
@ -334,12 +244,12 @@ User.prototype.verify = function (options, fn) {
|
||||||
var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email);
|
var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email);
|
||||||
|
|
||||||
crypto.randomBytes(64, function(err, buf) {
|
crypto.randomBytes(64, function(err, buf) {
|
||||||
if(err) {
|
if (err) {
|
||||||
fn(err);
|
fn(err);
|
||||||
} else {
|
} else {
|
||||||
user.verificationToken = buf.toString('hex');
|
user.verificationToken = buf.toString('hex');
|
||||||
user.save(function (err) {
|
user.save(function(err) {
|
||||||
if(err) {
|
if (err) {
|
||||||
fn(err);
|
fn(err);
|
||||||
} else {
|
} else {
|
||||||
sendEmail(user);
|
sendEmail(user);
|
||||||
|
@ -363,8 +273,8 @@ User.prototype.verify = function (options, fn) {
|
||||||
subject: options.subject || 'Thanks for Registering',
|
subject: options.subject || 'Thanks for Registering',
|
||||||
text: options.text,
|
text: options.text,
|
||||||
html: template(options)
|
html: template(options)
|
||||||
}, function (err, email) {
|
}, function(err, email) {
|
||||||
if(err) {
|
if (err) {
|
||||||
fn(err);
|
fn(err);
|
||||||
} else {
|
} else {
|
||||||
fn(null, {email: email, token: user.verificationToken, uid: user.id});
|
fn(null, {email: email, token: user.verificationToken, uid: user.id});
|
||||||
|
@ -383,16 +293,16 @@ User.prototype.verify = function (options, fn) {
|
||||||
* @callback {Function} callback
|
* @callback {Function} callback
|
||||||
* @param {Error} err
|
* @param {Error} err
|
||||||
*/
|
*/
|
||||||
User.confirm = function (uid, token, redirect, fn) {
|
User.confirm = function(uid, token, redirect, fn) {
|
||||||
this.findById(uid, function (err, user) {
|
this.findById(uid, function(err, user) {
|
||||||
if(err) {
|
if (err) {
|
||||||
fn(err);
|
fn(err);
|
||||||
} else {
|
} else {
|
||||||
if(user && user.verificationToken === token) {
|
if (user && user.verificationToken === token) {
|
||||||
user.verificationToken = undefined;
|
user.verificationToken = undefined;
|
||||||
user.emailVerified = true;
|
user.emailVerified = true;
|
||||||
user.save(function (err) {
|
user.save(function(err) {
|
||||||
if(err) {
|
if (err) {
|
||||||
fn(err);
|
fn(err);
|
||||||
} else {
|
} else {
|
||||||
fn();
|
fn();
|
||||||
|
@ -427,15 +337,15 @@ User.resetPassword = function(options, cb) {
|
||||||
var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL;
|
var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL;
|
||||||
|
|
||||||
options = options || {};
|
options = options || {};
|
||||||
if(typeof options.email === 'string') {
|
if (typeof options.email === 'string') {
|
||||||
UserModel.findOne({ where: {email: options.email} }, function(err, user) {
|
UserModel.findOne({ where: {email: options.email} }, function(err, user) {
|
||||||
if(err) {
|
if (err) {
|
||||||
cb(err);
|
cb(err);
|
||||||
} else if(user) {
|
} else if (user) {
|
||||||
// create a short lived access token for temp login to change password
|
// create a short lived access token for temp login to change password
|
||||||
// TODO(ritch) - eventually this should only allow password change
|
// TODO(ritch) - eventually this should only allow password change
|
||||||
user.accessTokens.create({ttl: ttl}, function(err, accessToken) {
|
user.accessTokens.create({ttl: ttl}, function(err, accessToken) {
|
||||||
if(err) {
|
if (err) {
|
||||||
cb(err);
|
cb(err);
|
||||||
} else {
|
} else {
|
||||||
cb();
|
cb();
|
||||||
|
@ -462,16 +372,16 @@ User.resetPassword = function(options, cb) {
|
||||||
* Setup an extended user model.
|
* Setup an extended user model.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
User.setup = function () {
|
User.setup = function() {
|
||||||
// We need to call the base class's setup method
|
// We need to call the base class's setup method
|
||||||
PersistedModel.setup.call(this);
|
User.base.setup.call(this);
|
||||||
var UserModel = this;
|
var UserModel = this;
|
||||||
|
|
||||||
// max ttl
|
// max ttl
|
||||||
this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL;
|
this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL;
|
||||||
this.settings.ttl = DEFAULT_TTL;
|
this.settings.ttl = DEFAULT_TTL;
|
||||||
|
|
||||||
UserModel.setter.password = function (plain) {
|
UserModel.setter.password = function(plain) {
|
||||||
var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR);
|
var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR);
|
||||||
this.$password = bcrypt.hashSync(plain, salt);
|
this.$password = bcrypt.hashSync(plain, salt);
|
||||||
}
|
}
|
||||||
|
@ -491,13 +401,11 @@ User.setup = function () {
|
||||||
description: 'Login a user with username/email and password',
|
description: 'Login a user with username/email and password',
|
||||||
accepts: [
|
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:
|
{arg: 'include', type: 'string', http: {source: 'query' }, description: 'Related objects to include in the response. ' +
|
||||||
'Related objects to include in the response. ' +
|
|
||||||
'See the description of return value for more details.'}
|
'See the description of return value for more details.'}
|
||||||
],
|
],
|
||||||
returns: {
|
returns: {
|
||||||
arg: 'accessToken', type: 'object', root: true, description:
|
arg: 'accessToken', type: 'object', root: true, description: 'The response body contains properties of the AccessToken created on login.\n' +
|
||||||
'The response body contains properties of the AccessToken created on login.\n' +
|
|
||||||
'Depending on the value of `include` parameter, the body may contain ' +
|
'Depending on the value of `include` parameter, the body may contain ' +
|
||||||
'additional properties:\n\n' +
|
'additional properties:\n\n' +
|
||||||
' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n'
|
' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n'
|
||||||
|
@ -517,8 +425,7 @@ User.setup = function () {
|
||||||
var tokenID = accessToken && accessToken.id;
|
var tokenID = accessToken && accessToken.id;
|
||||||
|
|
||||||
return tokenID;
|
return tokenID;
|
||||||
}, description:
|
}, description: 'Do not supply this argument, it is automatically extracted ' +
|
||||||
'Do not supply this argument, it is automatically extracted ' +
|
|
||||||
'from request headers.'
|
'from request headers.'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -550,9 +457,9 @@ User.setup = function () {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
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) {
|
||||||
ctx.res.redirect(ctx.req.param('redirect'));
|
ctx.res.redirect(ctx.req.param('redirect'));
|
||||||
} else {
|
} else {
|
||||||
fn(new Error('transport unsupported'));
|
fn(new Error('transport unsupported'));
|
||||||
|
@ -561,8 +468,11 @@ User.setup = function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
// default models
|
// default models
|
||||||
UserModel.email = require('./email');
|
assert(loopback.Email, 'Email model must be defined before User model');
|
||||||
UserModel.accessToken = require('./access-token');
|
UserModel.email = loopback.Email;
|
||||||
|
|
||||||
|
assert(loopback.AccessToken, 'AccessToken model must be defined before User model');
|
||||||
|
UserModel.accessToken = loopback.AccessToken;
|
||||||
|
|
||||||
// email validation regex
|
// email validation regex
|
||||||
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
@ -579,3 +489,5 @@ User.setup = function () {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
User.setup();
|
User.setup();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
{
|
||||||
|
"name": "User",
|
||||||
|
"properties": {
|
||||||
|
"realm": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"credentials": {
|
||||||
|
"type": "object",
|
||||||
|
"deprecated": true
|
||||||
|
},
|
||||||
|
"challenges": {
|
||||||
|
"type": "object",
|
||||||
|
"deprecated": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"emailVerified": "boolean",
|
||||||
|
"verificationToken": "string",
|
||||||
|
"status": "string",
|
||||||
|
"created": "date",
|
||||||
|
"lastUpdated": "date"
|
||||||
|
},
|
||||||
|
"hidden": ["password"],
|
||||||
|
"acls": [
|
||||||
|
{
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"permission": "DENY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"permission": "ALLOW",
|
||||||
|
"property": "create"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$owner",
|
||||||
|
"permission": "ALLOW",
|
||||||
|
"property": "deleteById"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"permission": "ALLOW",
|
||||||
|
"property": "login"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"permission": "ALLOW",
|
||||||
|
"property": "logout"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$owner",
|
||||||
|
"permission": "ALLOW",
|
||||||
|
"property": "findById"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$owner",
|
||||||
|
"permission": "ALLOW",
|
||||||
|
"property": "updateAttributes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"permission": "ACL.ALLOW",
|
||||||
|
"property": "confirm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"permission": "ALLOW",
|
||||||
|
"property": "resetPassword",
|
||||||
|
"accessType": "EXECUTE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relations": {
|
||||||
|
"accessTokens": {
|
||||||
|
"type": "hasMany",
|
||||||
|
"model": "AccessToken",
|
||||||
|
"foreignKey": "userId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,8 +16,10 @@
|
||||||
{ "title": "Built-in models", "depth": 2 },
|
{ "title": "Built-in models", "depth": 2 },
|
||||||
"common/models/access-token.js",
|
"common/models/access-token.js",
|
||||||
"common/models/acl.js",
|
"common/models/acl.js",
|
||||||
|
"common/models/scope.js",
|
||||||
"common/models/application.js",
|
"common/models/application.js",
|
||||||
"common/models/email.js",
|
"common/models/email.js",
|
||||||
|
"common/models/role-mapping.js",
|
||||||
"common/models/role.js",
|
"common/models/role.js",
|
||||||
"common/models/user.js",
|
"common/models/user.js",
|
||||||
"common/models/change.js"
|
"common/models/change.js"
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
var loopback = require('./loopback');
|
var loopback = require('./loopback');
|
||||||
var AccessToken = require('../common/models/access-token');
|
|
||||||
var debug = require('debug')('loopback:security:access-context');
|
var debug = require('debug')('loopback:security:access-context');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,7 +48,9 @@ function AccessContext(context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.accessType = context.accessType || AccessContext.ALL;
|
this.accessType = context.accessType || AccessContext.ALL;
|
||||||
this.accessToken = context.accessToken || AccessToken.ANONYMOUS;
|
assert(loopback.AccessToken,
|
||||||
|
'AccessToken model must be defined before AccessContext model');
|
||||||
|
this.accessToken = context.accessToken || loopback.AccessToken.ANONYMOUS;
|
||||||
|
|
||||||
var principalType = context.principalType || Principal.USER;
|
var principalType = context.principalType || Principal.USER;
|
||||||
var principalId = context.principalId || undefined;
|
var principalId = context.principalId || undefined;
|
||||||
|
@ -280,7 +281,7 @@ AccessRequest.prototype.exactlyMatches = function(acl) {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
AccessRequest.prototype.isAllowed = function() {
|
AccessRequest.prototype.isAllowed = function() {
|
||||||
return this.permission !== require('../common/models/acl').ACL.DENY;
|
return this.permission !== loopback.ACL.DENY;
|
||||||
}
|
}
|
||||||
|
|
||||||
AccessRequest.prototype.debug = function() {
|
AccessRequest.prototype.debug = function() {
|
||||||
|
|
|
@ -1,13 +1,45 @@
|
||||||
module.exports = function(loopback) {
|
module.exports = function(loopback) {
|
||||||
loopback.Email = require('../common/models/email');
|
// NOTE(bajtos) we must use static require() due to browserify limitations
|
||||||
loopback.User = require('../common/models/user');
|
|
||||||
loopback.Application = require('../common/models/application');
|
loopback.Email = createModel(
|
||||||
loopback.AccessToken = require('../common/models/access-token');
|
require('../common/models/email.json'),
|
||||||
loopback.Role = require('../common/models/role').Role;
|
require('../common/models/email.js'));
|
||||||
loopback.RoleMapping = require('../common/models/role').RoleMapping;
|
|
||||||
loopback.ACL = require('../common/models/acl').ACL;
|
loopback.Application = createModel(
|
||||||
loopback.Scope = require('../common/models/acl').Scope;
|
require('../common/models/application.json'),
|
||||||
loopback.Change = require('../common/models/change');
|
require('../common/models/application.js'));
|
||||||
|
|
||||||
|
loopback.AccessToken = createModel(
|
||||||
|
require('../common/models/access-token.json'),
|
||||||
|
require('../common/models/access-token.js'));
|
||||||
|
|
||||||
|
loopback.RoleMapping = createModel(
|
||||||
|
require('../common/models/role-mapping.json'),
|
||||||
|
require('../common/models/role-mapping.js'));
|
||||||
|
|
||||||
|
loopback.Role = createModel(
|
||||||
|
require('../common/models/role.json'),
|
||||||
|
require('../common/models/role.js'));
|
||||||
|
|
||||||
|
loopback.ACL = createModel(
|
||||||
|
require('../common/models/acl.json'),
|
||||||
|
require('../common/models/acl.js'));
|
||||||
|
|
||||||
|
loopback.Scope = createModel(
|
||||||
|
require('../common/models/scope.json'),
|
||||||
|
require('../common/models/scope.js'));
|
||||||
|
|
||||||
|
loopback.User = createModel(
|
||||||
|
require('../common/models/user.json'),
|
||||||
|
require('../common/models/user.js'));
|
||||||
|
|
||||||
|
loopback.Change = createModel(
|
||||||
|
require('../common/models/change.json'),
|
||||||
|
require('../common/models/change.js'));
|
||||||
|
|
||||||
|
loopback.Checkpoint = createModel(
|
||||||
|
require('../common/models/checkpoint.json'),
|
||||||
|
require('../common/models/checkpoint.js'));
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Automatically attach these models to dataSources
|
* Automatically attach these models to dataSources
|
||||||
|
@ -27,4 +59,10 @@ module.exports = function(loopback) {
|
||||||
loopback.ACL.autoAttach = dataSourceTypes.DB;
|
loopback.ACL.autoAttach = dataSourceTypes.DB;
|
||||||
loopback.Scope.autoAttach = dataSourceTypes.DB;
|
loopback.Scope.autoAttach = dataSourceTypes.DB;
|
||||||
loopback.Application.autoAttach = dataSourceTypes.DB;
|
loopback.Application.autoAttach = dataSourceTypes.DB;
|
||||||
|
|
||||||
|
function createModel(definitionJson, customizeFn) {
|
||||||
|
var Model = loopback.createModel(definitionJson);
|
||||||
|
customizeFn(Model);
|
||||||
|
return Model;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -960,7 +960,10 @@ PersistedModel.enableChangeTracking = function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
PersistedModel._defineChangeModel = function() {
|
PersistedModel._defineChangeModel = function() {
|
||||||
var BaseChangeModel = require('./../common/models/change');
|
var BaseChangeModel = loopback.Change;
|
||||||
|
assert(BaseChangeModel,
|
||||||
|
'Change model must be defined before enabling change replication');
|
||||||
|
|
||||||
return this.Change = BaseChangeModel.extend(this.modelName + '-change',
|
return this.Change = BaseChangeModel.extend(this.modelName + '-change',
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
|
|
|
@ -109,8 +109,6 @@ describe('AccessToken', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('app.enableAuth()', function() {
|
describe('app.enableAuth()', function() {
|
||||||
this.timeout(0);
|
|
||||||
|
|
||||||
beforeEach(createTestingToken);
|
beforeEach(createTestingToken);
|
||||||
|
|
||||||
it('prevents remote call with 401 status on denied ACL', function (done) {
|
it('prevents remote call with 401 status on denied ACL', function (done) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ var async = require('async');
|
||||||
var loopback = require('../');
|
var loopback = require('../');
|
||||||
|
|
||||||
// create a unique Checkpoint model
|
// create a unique Checkpoint model
|
||||||
var Checkpoint = require('../common/models/checkpoint').extend('TestCheckpoint');
|
var Checkpoint = loopback.Checkpoint.extend('TestCheckpoint');
|
||||||
Checkpoint.attachTo(loopback.memory());
|
Checkpoint.attachTo(loopback.memory());
|
||||||
|
|
||||||
describe('Checkpoint', function() {
|
describe('Checkpoint', function() {
|
||||||
|
|
|
@ -16,7 +16,9 @@ module.exports = function(config) {
|
||||||
files: [
|
files: [
|
||||||
'node_modules/es5-shim/es5-shim.js',
|
'node_modules/es5-shim/es5-shim.js',
|
||||||
'test/support.js',
|
'test/support.js',
|
||||||
|
'test/loopback.test.js',
|
||||||
'test/model.test.js',
|
'test/model.test.js',
|
||||||
|
'test/model.application.test.js',
|
||||||
'test/geo-point.test.js',
|
'test/geo-point.test.js',
|
||||||
'test/app.test.js'
|
'test/app.test.js'
|
||||||
],
|
],
|
||||||
|
|
|
@ -241,4 +241,28 @@ describe('loopback', function() {
|
||||||
expect(owner._targetClass).to.equal('User');
|
expect(owner._targetClass).to.equal('User');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('loopback object', function() {
|
||||||
|
it('exports all built-in models', function() {
|
||||||
|
var expectedModelNames = [
|
||||||
|
'Email',
|
||||||
|
'User',
|
||||||
|
'Application',
|
||||||
|
'AccessToken',
|
||||||
|
'Role',
|
||||||
|
'RoleMapping',
|
||||||
|
'ACL',
|
||||||
|
'Scope',
|
||||||
|
'Change',
|
||||||
|
'Checkpoint'
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(Object.keys(loopback)).to.include.members(expectedModelNames);
|
||||||
|
|
||||||
|
expectedModelNames.forEach(function(name) {
|
||||||
|
expect(loopback[name], name).to.be.a('function');
|
||||||
|
expect(loopback[name].modelName, name + '.modelName').to.eql(name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -600,4 +600,16 @@ describe('User', function(){
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ctor', function() {
|
||||||
|
it('exports default Email model', function() {
|
||||||
|
expect(User.email, 'User.email').to.be.a('function');
|
||||||
|
expect(User.email.modelName, 'modelName').to.eql('email');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exports default AccessToken model', function() {
|
||||||
|
expect(User.accessToken, 'User.accessToken').to.be.a('function');
|
||||||
|
expect(User.accessToken.modelName, 'modelName').to.eql('AccessToken');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue