diff --git a/.jscsrc b/.jscsrc index b2b3515a..ed1f2e3e 100644 --- a/.jscsrc +++ b/.jscsrc @@ -16,7 +16,7 @@ }, "validateJSDoc": { "checkParamNames": false, - "checkRedundantParams": true, + "checkRedundantParams": false, "requireParamTypes": true } } diff --git a/.jshintrc b/.jshintrc index 361fd3ae..5665b3fe 100644 --- a/.jshintrc +++ b/.jshintrc @@ -5,8 +5,6 @@ "indent": 2, "undef": true, "quotmark": "single", -"maxlen": 150, -"trailing": true, "newcap": true, "nonew": true, "sub": true, diff --git a/Gruntfile.js b/Gruntfile.js index e3627b6f..877fca96 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -33,7 +33,9 @@ module.exports = function(grunt) { lib: { src: ['lib/**/*.js'] }, - // TODO(bajtos) - common/**/*.js + common: { + src: ['common/**/*.js'] + }, // TODO tests don't pass yet // test: { // src: ['test/**/*.js'] @@ -41,8 +43,9 @@ module.exports = function(grunt) { }, jscs: { gruntfile: 'Gruntfile.js', - lib: ['lib/**/*.js'] - // TODO(bajtos) - common/**/*.js + lib: ['lib/**/*.js'], + common: ['common/**/*.js'] + // TODO(bajtos) - test/**/*.js }, watch: { gruntfile: { diff --git a/common/models/access-token.js b/common/models/access-token.js index 03185009..1c4f1f7e 100644 --- a/common/models/access-token.js +++ b/common/models/access-token.js @@ -2,10 +2,10 @@ * Module Dependencies. */ -var loopback = require('../../lib/loopback') - , assert = require('assert') - , uid = require('uid2') - , DEFAULT_TOKEN_LEN = 64; +var loopback = require('../../lib/loopback'); +var assert = require('assert'); +var uid = require('uid2'); +var DEFAULT_TOKEN_LEN = 64; /** * Token based authentication and access control. @@ -57,7 +57,7 @@ module.exports = function(AccessToken) { fn(null, guid); } }); - } + }; /*! * Hook to create accessToken id. @@ -75,7 +75,7 @@ module.exports = function(AccessToken) { next(); } }); - } + }; /** * Find a token for the given `ServerRequest`. @@ -115,7 +115,7 @@ module.exports = function(AccessToken) { cb(); }); } - } + }; /** * Validate the token. @@ -151,7 +151,7 @@ module.exports = function(AccessToken) { } catch (e) { cb(e); } - } + }; function tokenIdForRequest(req, options) { var params = options.params || []; diff --git a/common/models/acl.js b/common/models/acl.js index 1bb81407..b10fa1c1 100644 --- a/common/models/acl.js +++ b/common/models/acl.js @@ -179,7 +179,7 @@ module.exports = function(ACL) { ACL.prototype.score = function(req) { return this.constructor.getMatchingScore(this, req); - } + }; /*! * Resolve permission from the ACLs @@ -199,24 +199,26 @@ module.exports = function(ACL) { var score = 0; for (var i = 0; i < acls.length; i++) { - score = ACL.getMatchingScore(acls[i], req); + var candidate = acls[i]; + score = ACL.getMatchingScore(candidate, req); if (score < 0) { // the highest scored ACL did not match break; } if (!req.isWildcard()) { // We should stop from the first match for non-wildcard - permission = acls[i].permission; + permission = candidate.permission; break; } else { - if (req.exactlyMatches(acls[i])) { - permission = acls[i].permission; + if (req.exactlyMatches(candidate)) { + permission = candidate.permission; break; } // For wildcard match, find the strongest permission - if (AccessContext.permissionOrder[acls[i].permission] - > AccessContext.permissionOrder[permission]) { - permission = acls[i].permission; + var candidateOrder = AccessContext.permissionOrder[candidate.permission]; + var permissionOrder = AccessContext.permissionOrder[permission]; + if (candidateOrder > permissionOrder) { + permission = candidate.permission; } } } @@ -246,8 +248,7 @@ module.exports = function(ACL) { var staticACLs = []; if (modelClass && modelClass.settings.acls) { modelClass.settings.acls.forEach(function(acl) { - if (!acl.property || acl.property === ACL.ALL - || property === acl.property) { + if (!acl.property || acl.property === ACL.ALL || property === acl.property) { staticACLs.push(new ACL({ model: model, property: acl.property || ACL.ALL, @@ -259,11 +260,15 @@ module.exports = function(ACL) { } }); } - var prop = modelClass && - (modelClass.definition.properties[property] // regular property - || (modelClass._scopeMeta && modelClass._scopeMeta[property]) // relation/scope - || modelClass[property] // static method - || modelClass.prototype[property]); // prototype method + var prop = modelClass && ( + // regular property + modelClass.definition.properties[property] || + // relation/scope + (modelClass._scopeMeta && modelClass._scopeMeta[property]) || + // static method + modelClass[property] || + // prototype method + modelClass.prototype[property]); if (prop && prop.acls) { prop.acls.forEach(function(acl) { staticACLs.push(new ACL({ @@ -311,7 +316,7 @@ module.exports = function(ACL) { debug('Permission denied by statically resolved permission'); debug(' Resolved Permission: %j', resolved); process.nextTick(function() { - callback && callback(null, resolved); + if (callback) callback(null, resolved); }); return; } @@ -321,7 +326,7 @@ module.exports = function(ACL) { model: model, property: propertyQuery, accessType: accessTypeQuery}}, function(err, dynACLs) { if (err) { - callback && callback(err); + if (callback) callback(err); return; } acls = acls.concat(dynACLs); @@ -330,7 +335,7 @@ module.exports = function(ACL) { var modelClass = loopback.findModel(model); resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW; } - callback && callback(null, resolved); + if (callback) callback(null, resolved); }); }; @@ -344,7 +349,7 @@ module.exports = function(ACL) { debug('accessType %s', this.accessType); debug('permission %s', this.permission); } - } + }; /** * Check if the request has the permission to access. @@ -381,7 +386,7 @@ module.exports = function(ACL) { this.find({where: {model: model.modelName, property: propertyQuery, accessType: accessTypeQuery}}, function(err, acls) { if (err) { - callback && callback(err); + if (callback) callback(err); return; } var inRoleTasks = []; @@ -392,8 +397,9 @@ module.exports = function(ACL) { // Check exact matches for (var i = 0; i < context.principals.length; i++) { var p = context.principals[i]; - if (p.type === acl.principalType - && String(p.id) === String(acl.principalId)) { + var typeMatch = p.type === acl.principalType; + var idMatch = String(p.id) === String(acl.principalId); + if (typeMatch && idMatch) { effectiveACLs.push(acl); return; } @@ -415,7 +421,7 @@ module.exports = function(ACL) { async.parallel(inRoleTasks, function(err, results) { if (err) { - callback && callback(err, null); + if (callback) callback(err, null); return; } var resolved = self.resolvePermission(effectiveACLs, req); @@ -424,7 +430,7 @@ module.exports = function(ACL) { } debug('---Resolved---'); resolved.debug(); - callback && callback(null, resolved); + if (callback) callback(null, resolved); }); }); }; @@ -452,11 +458,10 @@ module.exports = function(ACL) { this.checkAccessForContext(context, function(err, access) { if (err) { - callback && callback(err); + if (callback) callback(err); return; } - callback && callback(null, access.permission !== ACL.DENY); + if (callback) callback(null, access.permission !== ACL.DENY); }); }; - -} +}; diff --git a/common/models/application.js b/common/models/application.js index b466de51..1f6148ff 100644 --- a/common/models/application.js +++ b/common/models/application.js @@ -141,7 +141,7 @@ module.exports = function(Application) { Application.resetKeys = function(appId, cb) { this.findById(appId, function(err, app) { if (err) { - cb && cb(err, app); + if (cb) cb(err, app); return; } app.resetKeys(cb); @@ -166,7 +166,7 @@ module.exports = function(Application) { Application.authenticate = function(appId, key, cb) { this.findById(appId, function(err, app) { if (err || !app) { - cb && cb(err, null); + if (cb) cb(err, null); return; } var result = null; @@ -180,7 +180,7 @@ module.exports = function(Application) { break; } } - cb && cb(null, result); + if (cb) cb(null, result); }); }; }; diff --git a/common/models/change.js b/common/models/change.js index 75124a85..d4aa33b4 100644 --- a/common/models/change.js +++ b/common/models/change.js @@ -2,14 +2,13 @@ * Module Dependencies. */ -var PersistedModel = require('../../lib/loopback').PersistedModel - , loopback = require('../../lib/loopback') - , crypto = require('crypto') - , CJSON = {stringify: require('canonical-json')} - , async = require('async') - , assert = require('assert') - , debug = require('debug')('loopback:change'); - +var PersistedModel = require('../../lib/loopback').PersistedModel; +var loopback = require('../../lib/loopback'); +var crypto = require('crypto'); +var CJSON = {stringify: require('canonical-json')}; +var async = require('async'); +var assert = require('assert'); +var debug = require('debug')('loopback:change'); /** * Change list entry. @@ -55,8 +54,8 @@ module.exports = function(Change) { if (!hasModel) return null; return Change.idForModel(this.modelName, this.modelId); - } - } + }; + }; Change.setup(); /** @@ -82,7 +81,7 @@ module.exports = function(Change) { }); }); async.parallel(tasks, callback); - } + }; /** * Get an identifier for a given model. @@ -94,7 +93,7 @@ module.exports = function(Change) { Change.idForModel = function(modelName, modelId) { return this.hash([modelName, modelId].join('-')); - } + }; /** * Find or create a change for the given model. @@ -126,7 +125,7 @@ module.exports = function(Change) { ch.save(callback); } }); - } + }; /** * Update (or create) the change with the current revision. @@ -148,7 +147,7 @@ module.exports = function(Change) { cb = cb || function(err) { if (err) throw new Error(err); - } + }; async.parallel(tasks, function(err) { if (err) return cb(err); @@ -194,7 +193,7 @@ module.exports = function(Change) { cb(); }); } - } + }; /** * Get a change's current revision based on current data. @@ -214,7 +213,7 @@ module.exports = function(Change) { cb(null, null); } }); - } + }; /** * Create a hash of the given `string` with the `options.hashAlgorithm`. @@ -229,7 +228,7 @@ module.exports = function(Change) { .createHash(Change.settings.hashAlgorithm || 'sha1') .update(str) .digest('hex'); - } + }; /** * Get the revision string for the given object @@ -239,7 +238,7 @@ module.exports = function(Change) { Change.revisionForInst = function(inst) { return this.hash(CJSON.stringify(inst)); - } + }; /** * Get a change's type. Returns one of: @@ -263,7 +262,7 @@ module.exports = function(Change) { return Change.DELETE; } return Change.UNKNOWN; - } + }; /** * Compare two changes. @@ -276,7 +275,7 @@ module.exports = function(Change) { var thisRev = this.rev || null; var thatRev = change.rev || null; return thisRev === thatRev; - } + }; /** * Does this change conflict with the given change. @@ -290,7 +289,7 @@ module.exports = function(Change) { if (Change.bothDeleted(this, change)) return false; if (this.isBasedOn(change)) return false; return true; - } + }; /** * Are both changes deletes? @@ -300,9 +299,9 @@ module.exports = function(Change) { */ Change.bothDeleted = function(a, b) { - return a.type() === Change.DELETE - && b.type() === Change.DELETE; - } + return a.type() === Change.DELETE && + b.type() === Change.DELETE; + }; /** * Determine if the change is based on the given change. @@ -312,7 +311,7 @@ module.exports = function(Change) { Change.prototype.isBasedOn = function(change) { return this.prev === change.rev; - } + }; /** * Determine the differences for a given model since a given checkpoint. @@ -393,11 +392,11 @@ module.exports = function(Change) { conflicts: conflicts }); }); - } + }; /** * Correct all change list entries. - * @param {Function} callback + * @param {Function} cb */ Change.rectifyAll = function(cb) { @@ -410,7 +409,7 @@ module.exports = function(Change) { change.rectify(); }); }); - } + }; /** * Get the checkpoint model. @@ -425,13 +424,13 @@ module.exports = function(Change) { + ' is not attached to a dataSource'); checkpointModel.attachTo(this.dataSource); return checkpointModel; - } + }; Change.handleError = function(err) { if (!this.settings.ignoreErrors) { throw err; } - } + }; Change.prototype.debug = function() { if (debug.enabled) { @@ -444,7 +443,7 @@ module.exports = function(Change) { debug('\tmodelId', this.modelId); debug('\ttype', this.type()); } - } + }; /** * Get the `Model` class for `change.modelName`. @@ -453,7 +452,7 @@ module.exports = function(Change) { Change.prototype.getModelCtor = function() { return this.constructor.settings.trackModel; - } + }; Change.prototype.getModelId = function() { // TODO(ritch) get rid of the need to create an instance @@ -462,13 +461,13 @@ module.exports = function(Change) { var m = new Model(); m.setId(id); return m.getId(); - } + }; Change.prototype.getModel = function(callback) { var Model = this.constructor.settings.trackModel; var id = this.getModelId(); Model.findById(id, callback); - } + }; /** * When two changes conflict a conflict is created. @@ -532,7 +531,7 @@ module.exports = function(Change) { if (err) return cb(err); cb(null, source, target); } - } + }; /** * Get the conflicting changes. @@ -577,7 +576,7 @@ module.exports = function(Change) { if (err) return cb(err); cb(null, sourceChange, targetChange); } - } + }; /** * Resolve the conflict. @@ -593,7 +592,7 @@ module.exports = function(Change) { sourceChange.prev = targetChange.rev; sourceChange.save(cb); }); - } + }; /** * Determine the conflict type. @@ -623,5 +622,5 @@ module.exports = function(Change) { } return cb(null, Change.UNKNOWN); }); - } + }; }; diff --git a/common/models/checkpoint.js b/common/models/checkpoint.js index ed57de53..2bba736a 100644 --- a/common/models/checkpoint.js +++ b/common/models/checkpoint.js @@ -47,7 +47,7 @@ module.exports = function(Checkpoint) { }); } }); - } + }; Checkpoint.beforeSave = function(next, model) { if (!model.getId() && model.seq === undefined) { @@ -59,5 +59,5 @@ module.exports = function(Checkpoint) { } else { next(); } - } + }; }; diff --git a/common/models/role-mapping.js b/common/models/role-mapping.js index a6bfc4e7..3e87e563 100644 --- a/common/models/role-mapping.js +++ b/common/models/role-mapping.js @@ -23,14 +23,14 @@ module.exports = function(RoleMapping) { * @param {Error} err * @param {Application} application */ - RoleMapping.prototype.application = function (callback) { + RoleMapping.prototype.application = function(callback) { if (this.principalType === RoleMapping.APPLICATION) { - var applicationModel = this.constructor.Application - || loopback.getModelByType(loopback.Application); + var applicationModel = this.constructor.Application || + loopback.getModelByType(loopback.Application); applicationModel.findById(this.principalId, callback); } else { - process.nextTick(function () { - callback && callback(null, null); + process.nextTick(function() { + if (callback) callback(null, null); }); } }; @@ -41,14 +41,14 @@ module.exports = function(RoleMapping) { * @param {Error} err * @param {User} user */ - RoleMapping.prototype.user = function (callback) { + RoleMapping.prototype.user = function(callback) { if (this.principalType === RoleMapping.USER) { - var userModel = this.constructor.User - || loopback.getModelByType(loopback.User); + var userModel = this.constructor.User || + loopback.getModelByType(loopback.User); userModel.findById(this.principalId, callback); } else { - process.nextTick(function () { - callback && callback(null, null); + process.nextTick(function() { + if (callback) callback(null, null); }); } }; @@ -59,14 +59,14 @@ module.exports = function(RoleMapping) { * @param {Error} err * @param {User} childUser */ - RoleMapping.prototype.childRole = function (callback) { + RoleMapping.prototype.childRole = function(callback) { if (this.principalType === RoleMapping.ROLE) { var roleModel = this.constructor.Role || loopback.getModelByType(loopback.Role); roleModel.findById(this.principalId, callback); } else { - process.nextTick(function () { - callback && callback(null, null); + process.nextTick(function() { + if (callback) callback(null, null); }); } }; diff --git a/common/models/role.js b/common/models/role.js index 406389d9..595b4eac 100644 --- a/common/models/role.js +++ b/common/models/role.js @@ -33,7 +33,7 @@ module.exports = function(Role) { roleMappingModel.find({where: {roleId: this.id, principalType: RoleMapping.USER}}, function(err, mappings) { if (err) { - callback && callback(err); + if (callback) callback(err); return; } return mappings.map(function(m) { @@ -46,7 +46,7 @@ module.exports = function(Role) { roleMappingModel.find({where: {roleId: this.id, principalType: RoleMapping.APPLICATION}}, function(err, mappings) { if (err) { - callback && callback(err); + if (callback) callback(err); return; } return mappings.map(function(m) { @@ -59,7 +59,7 @@ module.exports = function(Role) { roleMappingModel.find({where: {roleId: this.id, principalType: RoleMapping.ROLE}}, function(err, mappings) { if (err) { - callback && callback(err); + if (callback) callback(err); return; } return mappings.map(function(m) { @@ -72,10 +72,10 @@ module.exports = function(Role) { // Special roles Role.OWNER = '$owner'; // owner of the object - Role.RELATED = "$related"; // any User with a relationship to the object - Role.AUTHENTICATED = "$authenticated"; // authenticated user - Role.UNAUTHENTICATED = "$unauthenticated"; // authenticated user - Role.EVERYONE = "$everyone"; // everyone + Role.RELATED = '$related'; // any User with a relationship to the object + Role.AUTHENTICATED = '$authenticated'; // authenticated user + Role.UNAUTHENTICATED = '$unauthenticated'; // authenticated user + Role.EVERYONE = '$everyone'; // everyone /** * Add custom handler for roles. @@ -93,7 +93,7 @@ module.exports = function(Role) { Role.registerResolver(Role.OWNER, function(role, context, callback) { if (!context || !context.model || !context.modelId) { process.nextTick(function() { - callback && callback(null, false); + if (callback) callback(null, false); }); return; } @@ -152,13 +152,13 @@ module.exports = function(Role) { modelClass.findById(modelId, function(err, inst) { if (err || !inst) { debug('Model not found for id %j', modelId); - callback && callback(err, false); + if (callback) callback(err, false); return; } debug('Model found: %j', inst); var ownerId = inst.userId || inst.owner; if (ownerId) { - callback && callback(null, matches(ownerId, userId)); + if (callback) callback(null, matches(ownerId, userId)); return; } else { // Try to follow belongsTo @@ -166,19 +166,21 @@ module.exports = function(Role) { var rel = modelClass.relations[r]; if (rel.type === 'belongsTo' && isUserClass(rel.modelTo)) { debug('Checking relation %s to %s: %j', r, rel.modelTo.modelName, rel); - inst[r](function(err, user) { - if (!err && user) { - debug('User found: %j', user.id); - callback && callback(null, matches(user.id, userId)); - } else { - callback && callback(err, false); - } - }); + inst[r](processRelatedUser); return; } } debug('No matching belongsTo relation found for model %j and user: %j', modelId, userId); - callback && callback(null, false); + if (callback) callback(null, false); + } + + function processRelatedUser(err, user) { + if (!err && user) { + debug('User found: %j', user.id); + if (callback) callback(null, matches(user.id, userId)); + } else { + if (callback) callback(err, false); + } } }); }; @@ -186,7 +188,7 @@ module.exports = function(Role) { Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) { if (!context) { process.nextTick(function() { - callback && callback(null, false); + if (callback) callback(null, false); }); return; } @@ -202,19 +204,19 @@ module.exports = function(Role) { */ Role.isAuthenticated = function isAuthenticated(context, callback) { process.nextTick(function() { - callback && callback(null, context.isAuthenticated()); + if (callback) callback(null, context.isAuthenticated()); }); }; Role.registerResolver(Role.UNAUTHENTICATED, function(role, context, callback) { process.nextTick(function() { - callback && callback(null, !context || !context.isAuthenticated()); + if (callback) callback(null, !context || !context.isAuthenticated()); }); }); Role.registerResolver(Role.EVERYONE, function(role, context, callback) { process.nextTick(function() { - callback && callback(null, true); // Always true + if (callback) callback(null, true); // Always true }); }); @@ -245,7 +247,7 @@ module.exports = function(Role) { if (context.principals.length === 0) { debug('isInRole() returns: false'); process.nextTick(function() { - callback && callback(null, false); + if (callback) callback(null, false); }); return; } @@ -262,7 +264,7 @@ module.exports = function(Role) { if (inRole) { debug('isInRole() returns: %j', inRole); process.nextTick(function() { - callback && callback(null, true); + if (callback) callback(null, true); }); return; } @@ -270,11 +272,11 @@ module.exports = function(Role) { var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping); this.findOne({where: {name: role}}, function(err, result) { if (err) { - callback && callback(err); + if (callback) callback(err); return; } if (!result) { - callback && callback(null, false); + if (callback) callback(null, false); return; } debug('Role found: %j', result); @@ -303,7 +305,7 @@ module.exports = function(Role) { } }, function(inRole) { debug('isInRole() returns: %j', inRole); - callback && callback(null, inRole); + if (callback) callback(null, inRole); }); }); @@ -315,8 +317,8 @@ module.exports = function(Role) { * @param {Function} callback * * @callback {Function} callback - * @param err - * @param {String[]} An array of role ids + * @param {Error=} err + * @param {String[]} roles An array of role ids */ Role.getRoles = function(context, callback) { if (!(context instanceof AccessContext)) { @@ -354,8 +356,8 @@ module.exports = function(Role) { // Check against the role mappings var principalType = p.type || undefined; var principalId = p.id == null ? undefined : p.id; - - if(typeof principalId !== 'string' && principalId != null) { + + if (typeof principalId !== 'string' && principalId != null) { principalId = principalId.toString(); } @@ -371,13 +373,13 @@ module.exports = function(Role) { principalId: principalId}}, function(err, mappings) { debug('Role mappings found: %s %j', err, mappings); if (err) { - done && done(err); + if (done) done(err); return; } mappings.forEach(function(m) { addRole(m.roleId); }); - done && done(); + if (done) done(); }); }); } @@ -385,7 +387,7 @@ module.exports = function(Role) { async.parallel(inRoleTasks, function(err, results) { debug('getRoles() returns: %j %j', err, roles); - callback && callback(err, roles); + if (callback) callback(err, roles); }); }; }; diff --git a/common/models/scope.js b/common/models/scope.js index 7ffe1d1d..39bd9bde 100644 --- a/common/models/scope.js +++ b/common/models/scope.js @@ -23,14 +23,14 @@ module.exports = function(Scope) { * @param {String|Error} err The error object * @param {AccessRequest} result The access permission */ - Scope.checkPermission = function (scope, model, property, accessType, callback) { + 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) { + this.findOne({where: {name: scope}}, function(err, scope) { if (err) { - callback && callback(err); + if (callback) callback(err); } else { var aclModel = loopback.getModelByType(ACL); aclModel.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback); diff --git a/common/models/user.js b/common/models/user.js index a07a346d..019894c5 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -2,15 +2,15 @@ * Module Dependencies. */ -var loopback = require('../../lib/loopback') - , path = require('path') - , SALT_WORK_FACTOR = 10 - , crypto = require('crypto') - , bcrypt = require('bcryptjs') - , DEFAULT_TTL = 1209600 // 2 weeks in seconds - , DEFAULT_RESET_PW_TTL = 15 * 60 // 15 mins in seconds - , DEFAULT_MAX_TTL = 31556926 // 1 year in seconds - , assert = require('assert'); +var loopback = require('../../lib/loopback'); +var path = require('path'); +var SALT_WORK_FACTOR = 10; +var crypto = require('crypto'); +var bcrypt = require('bcryptjs'); +var DEFAULT_TTL = 1209600; // 2 weeks in seconds +var DEFAULT_RESET_PW_TTL = 15 * 60; // 15 mins in seconds +var DEFAULT_MAX_TTL = 31556926; // 1 year in seconds +var assert = require('assert'); var debug = require('debug')('loopback:user'); @@ -40,533 +40,533 @@ var debug = require('debug')('loopback:user'); module.exports = function(User) { -/** - * Create access token for the logged in user. This method can be overridden to - * customize how access tokens are generated - * - * @param [Number} ttl The requested ttl - * @callack {Function} cb The callback function - * @param {String|Error} err The error string or object - * @param {AccessToken} token The generated access token object - */ -User.prototype.createAccessToken = function(ttl, cb) { - var userModel = this.constructor; - ttl = Math.min(ttl || userModel.settings.ttl, userModel.settings.maxTTL); - this.accessTokens.create({ - ttl: ttl - }, cb); -}; + /** + * Create access token for the logged in user. This method can be overridden to + * customize how access tokens are generated + * + * @param {Number} ttl The requested ttl + * @callack {Function} cb The callback function + * @param {String|Error} err The error string or object + * @param {AccessToken} token The generated access token object + */ + User.prototype.createAccessToken = function(ttl, cb) { + var userModel = this.constructor; + ttl = Math.min(ttl || userModel.settings.ttl, userModel.settings.maxTTL); + this.accessTokens.create({ + ttl: ttl + }, cb); + }; -function splitPrincipal(name, realmDelimiter) { - var parts = [null, name]; - if(!realmDelimiter) { + function splitPrincipal(name, realmDelimiter) { + var parts = [null, name]; + if (!realmDelimiter) { + return parts; + } + var index = name.indexOf(realmDelimiter); + if (index !== -1) { + parts[0] = name.substring(0, index); + parts[1] = name.substring(index + realmDelimiter.length); + } return parts; } - var index = name.indexOf(realmDelimiter); - if (index !== -1) { - parts[0] = name.substring(0, index); - parts[1] = name.substring(index + realmDelimiter.length); - } - return parts; -} -/** - * Normalize the credentials - * @param {Object} credentials The credential object - * @param {Boolean} realmRequired - * @param {String} realmDelimiter The realm delimiter, if not set, no realm is needed - * @returns {Object} The normalized credential object - */ -User.normalizeCredentials = function(credentials, realmRequired, realmDelimiter) { - var query = {}; - credentials = credentials || {}; - if(!realmRequired) { - if (credentials.email) { - query.email = credentials.email; - } else if (credentials.username) { - query.username = credentials.username; - } - } else { - if (credentials.realm) { - query.realm = credentials.realm; - } - var parts; - if (credentials.email) { - parts = splitPrincipal(credentials.email, realmDelimiter); - query.email = parts[1]; - if (parts[0]) { - query.realm = parts[0]; + /** + * Normalize the credentials + * @param {Object} credentials The credential object + * @param {Boolean} realmRequired + * @param {String} realmDelimiter The realm delimiter, if not set, no realm is needed + * @returns {Object} The normalized credential object + */ + User.normalizeCredentials = function(credentials, realmRequired, realmDelimiter) { + var query = {}; + credentials = credentials || {}; + if (!realmRequired) { + if (credentials.email) { + query.email = credentials.email; + } else if (credentials.username) { + query.username = credentials.username; } - } else if (credentials.username) { - parts = splitPrincipal(credentials.username, realmDelimiter); - query.username = parts[1]; - if (parts[0]) { - query.realm = parts[0]; + } else { + if (credentials.realm) { + query.realm = credentials.realm; } - } - } - return query; -} - -/** - * Login a user by with the given `credentials`. - * - * ```js - * User.login({username: 'foo', password: 'bar'}, function (err, token) { -* console.log(token.id); -* }); - * ``` - * - * @param {Object} credentials username/password or email/password - * @param {String[]|String} [include] Optionally set it to "user" to include - * the user info - * @callback {Function} callback Callback function - * @param {Error} err Error object - * @param {AccessToken} token Access token if login is successful - */ - -User.login = function(credentials, include, fn) { - var self = this; - if (typeof include === 'function') { - fn = include; - include = undefined; - } - - include = (include || ''); - if (Array.isArray(include)) { - include = include.map(function(val) { - return val.toLowerCase(); - }); - } else { - include = include.toLowerCase(); - } - - var realmDelimiter; - // Check if realm is required - var realmRequired = !!(self.settings.realmRequired || - self.settings.realmDelimiter); - if (realmRequired) { - realmDelimiter = self.settings.realmDelimiter; - } - var query = self.normalizeCredentials(credentials, realmRequired, - realmDelimiter); - - if(realmRequired && !query.realm) { - var err1 = new Error('realm is required'); - err1.statusCode = 400; - return fn(err1); - } - if (!query.email && !query.username) { - var err2 = new Error('username or email is required'); - err2.statusCode = 400; - return fn(err2); - } - - self.findOne({where: query}, function(err, user) { - var defaultError = new Error('login failed'); - defaultError.statusCode = 401; - - if (err) { - debug('An error is reported from User.findOne: %j', err); - fn(defaultError); - } else if (user) { - if (self.settings.emailVerificationRequired) { - if (!user.emailVerified) { - // Fail to log in if email verification is not done yet - debug('User email has not been verified'); - err = new Error('login failed as the email has not been verified'); - err.statusCode = 401; - return fn(err); + var parts; + if (credentials.email) { + parts = splitPrincipal(credentials.email, realmDelimiter); + query.email = parts[1]; + if (parts[0]) { + query.realm = parts[0]; + } + } else if (credentials.username) { + parts = splitPrincipal(credentials.username, realmDelimiter); + query.username = parts[1]; + if (parts[0]) { + query.realm = parts[0]; } } - user.hasPassword(credentials.password, function(err, isMatch) { - if (err) { - debug('An error is reported from User.hasPassword: %j', err); - fn(defaultError); - } else if (isMatch) { - user.createAccessToken(credentials.ttl, function(err, token) { - if (err) return fn(err); - if (Array.isArray(include) ? include.indexOf('user') !== -1 : include === 'user') { - // NOTE(bajtos) We can't set token.user here: - // 1. token.user already exists, it's a function injected by - // "AccessToken belongsTo User" relation - // 2. ModelBaseClass.toJSON() ignores own properties, thus - // the value won't be included in the HTTP response - // See also loopback#161 and loopback#162 - token.__data.user = user; - } - fn(err, token); - }); - } else { - debug('The password is invalid for user %s', query.email || query.username); - fn(defaultError); - } + } + return query; + }; + + /** + * Login a user by with the given `credentials`. + * + * ```js + * User.login({username: 'foo', password: 'bar'}, function (err, token) { + * console.log(token.id); + * }); + * ``` + * + * @param {Object} credentials username/password or email/password + * @param {String[]|String} [include] Optionally set it to "user" to include + * the user info + * @callback {Function} callback Callback function + * @param {Error} err Error object + * @param {AccessToken} token Access token if login is successful + */ + + User.login = function(credentials, include, fn) { + var self = this; + if (typeof include === 'function') { + fn = include; + include = undefined; + } + + include = (include || ''); + if (Array.isArray(include)) { + include = include.map(function(val) { + return val.toLowerCase(); }); } else { - debug('No matching record is found for user %s', query.email || query.username); - fn(defaultError); + include = include.toLowerCase(); } - }); -}; -/** - * Logout a user with the given accessToken id. - * - * ```js - * User.logout('asd0a9f8dsj9s0s3223mk', function (err) { -* console.log(err || 'Logged out'); -* }); - * ``` - * - * @param {String} accessTokenID - * @callback {Function} callback - * @param {Error} err - */ - -User.logout = function(tokenId, fn) { - this.relations.accessTokens.modelTo.findById(tokenId, function(err, accessToken) { - if (err) { - fn(err); - } else if (accessToken) { - accessToken.destroy(fn); - } else { - fn(new Error('could not find accessToken')); + var realmDelimiter; + // Check if realm is required + var realmRequired = !!(self.settings.realmRequired || + self.settings.realmDelimiter); + if (realmRequired) { + realmDelimiter = self.settings.realmDelimiter; } - }); -} + var query = self.normalizeCredentials(credentials, realmRequired, + realmDelimiter); -/** - * Compare the given `password` with the users hashed password. - * - * @param {String} password The plain text password - * @returns {Boolean} - */ + if (realmRequired && !query.realm) { + var err1 = new Error('realm is required'); + err1.statusCode = 400; + return fn(err1); + } + if (!query.email && !query.username) { + var err2 = new Error('username or email is required'); + err2.statusCode = 400; + return fn(err2); + } -User.prototype.hasPassword = function(plain, fn) { - if (this.password && plain) { - bcrypt.compare(plain, this.password, function(err, isMatch) { - if (err) return fn(err); - fn(null, isMatch); - }); - } else { - fn(null, false); - } -} + self.findOne({where: query}, function(err, user) { + var defaultError = new Error('login failed'); + defaultError.statusCode = 401; -/** - * Verify a user's identity by sending them a confirmation email. - * - * ```js - * var options = { -* type: 'email', -* to: user.email, -* template: 'verify.ejs', -* redirect: '/' -* }; - * - * user.verify(options, next); - * ``` - * - * @options {Object} options - * @property {String} type Must be 'email'. - * @property {String} to Email address to which verification email is sent. - * @property {String} from Sender email addresss, for example - * `'noreply@myapp.com'`. - * @property {String} subject Subject line text. - * @property {String} text Text of email. - * @property {String} template Name of template that displays verification - * page, for example, `'verify.ejs'. - * @property {String} redirect Page to which user will be redirected after - * they verify their email, for example `'/'` for root URI. - */ - -User.prototype.verify = function(options, fn) { - var user = this; - var userModel = this.constructor; - assert(typeof options === 'object', 'options required when calling user.verify()'); - assert(options.type, 'You must supply a verification type (options.type)'); - assert(options.type === 'email', 'Unsupported verification type'); - assert(options.to || this.email, 'Must include options.to when calling user.verify() or the user must have an email property'); - assert(options.from, 'Must include options.from when calling user.verify() or the user must have an email property'); - - options.redirect = options.redirect || '/'; - options.template = path.resolve(options.template || path.join(__dirname, '..', '..', 'templates', 'verify.ejs')); - options.user = this; - options.protocol = options.protocol || 'http'; - - var app = userModel.app; - options.host = options.host || (app && app.get('host')) || 'localhost'; - options.port = options.port || (app && app.get('port')) || 3000; - options.restApiRoot = options.restApiRoot || (app && app.get('restApiRoot')) || '/api'; - options.verifyHref = options.verifyHref || - options.protocol - + '://' - + options.host - + ':' - + options.port - + options.restApiRoot - + userModel.http.path - + userModel.confirm.http.path - + '?uid=' - + options.user.id - + '&redirect=' - + options.redirect; - - - // Email model - var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email); - - crypto.randomBytes(64, function(err, buf) { - if (err) { - fn(err); - } else { - user.verificationToken = buf.toString('hex'); - user.save(function(err) { - if (err) { - fn(err); - } else { - sendEmail(user); + if (err) { + debug('An error is reported from User.findOne: %j', err); + fn(defaultError); + } else if (user) { + if (self.settings.emailVerificationRequired) { + if (!user.emailVerified) { + // Fail to log in if email verification is not done yet + debug('User email has not been verified'); + err = new Error('login failed as the email has not been verified'); + err.statusCode = 401; + return fn(err); + } } + user.hasPassword(credentials.password, function(err, isMatch) { + if (err) { + debug('An error is reported from User.hasPassword: %j', err); + fn(defaultError); + } else if (isMatch) { + user.createAccessToken(credentials.ttl, function(err, token) { + if (err) return fn(err); + if (Array.isArray(include) ? include.indexOf('user') !== -1 : include === 'user') { + // NOTE(bajtos) We can't set token.user here: + // 1. token.user already exists, it's a function injected by + // "AccessToken belongsTo User" relation + // 2. ModelBaseClass.toJSON() ignores own properties, thus + // the value won't be included in the HTTP response + // See also loopback#161 and loopback#162 + token.__data.user = user; + } + fn(err, token); + }); + } else { + debug('The password is invalid for user %s', query.email || query.username); + fn(defaultError); + } + }); + } else { + debug('No matching record is found for user %s', query.email || query.username); + fn(defaultError); + } + }); + }; + + /** + * Logout a user with the given accessToken id. + * + * ```js + * User.logout('asd0a9f8dsj9s0s3223mk', function (err) { + * console.log(err || 'Logged out'); + * }); + * ``` + * + * @param {String} accessTokenID + * @callback {Function} callback + * @param {Error} err + */ + + User.logout = function(tokenId, fn) { + this.relations.accessTokens.modelTo.findById(tokenId, function(err, accessToken) { + if (err) { + fn(err); + } else if (accessToken) { + accessToken.destroy(fn); + } else { + fn(new Error('could not find accessToken')); + } + }); + }; + + /** + * Compare the given `password` with the users hashed password. + * + * @param {String} password The plain text password + * @returns {Boolean} + */ + + User.prototype.hasPassword = function(plain, fn) { + if (this.password && plain) { + bcrypt.compare(plain, this.password, function(err, isMatch) { + if (err) return fn(err); + fn(null, isMatch); }); + } else { + fn(null, false); } - }); + }; - // TODO - support more verification types - function sendEmail(user) { - options.verifyHref += '&token=' + user.verificationToken; + /** + * Verify a user's identity by sending them a confirmation email. + * + * ```js + * var options = { + * type: 'email', + * to: user.email, + * template: 'verify.ejs', + * redirect: '/' + * }; + * + * user.verify(options, next); + * ``` + * + * @options {Object} options + * @property {String} type Must be 'email'. + * @property {String} to Email address to which verification email is sent. + * @property {String} from Sender email addresss, for example + * `'noreply@myapp.com'`. + * @property {String} subject Subject line text. + * @property {String} text Text of email. + * @property {String} template Name of template that displays verification + * page, for example, `'verify.ejs'. + * @property {String} redirect Page to which user will be redirected after + * they verify their email, for example `'/'` for root URI. + */ - options.text = options.text || 'Please verify your email by opening this link in a web browser:\n\t{href}'; + User.prototype.verify = function(options, fn) { + var user = this; + var userModel = this.constructor; + assert(typeof options === 'object', 'options required when calling user.verify()'); + assert(options.type, 'You must supply a verification type (options.type)'); + assert(options.type === 'email', 'Unsupported verification type'); + assert(options.to || this.email, 'Must include options.to when calling user.verify() or the user must have an email property'); + assert(options.from, 'Must include options.from when calling user.verify() or the user must have an email property'); - options.text = options.text.replace('{href}', options.verifyHref); + options.redirect = options.redirect || '/'; + options.template = path.resolve(options.template || path.join(__dirname, '..', '..', 'templates', 'verify.ejs')); + options.user = this; + options.protocol = options.protocol || 'http'; - var template = loopback.template(options.template); - Email.send({ - to: options.to || user.email, - from: options.from, - subject: options.subject || 'Thanks for Registering', - text: options.text, - html: template(options), - headers: options.headers || {} - }, function (err, email) { - if(err) { + var app = userModel.app; + options.host = options.host || (app && app.get('host')) || 'localhost'; + options.port = options.port || (app && app.get('port')) || 3000; + options.restApiRoot = options.restApiRoot || (app && app.get('restApiRoot')) || '/api'; + options.verifyHref = options.verifyHref || + options.protocol + + '://' + + options.host + + ':' + + options.port + + options.restApiRoot + + userModel.http.path + + userModel.confirm.http.path + + '?uid=' + + options.user.id + + '&redirect=' + + options.redirect; + + // Email model + var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email); + + crypto.randomBytes(64, function(err, buf) { + if (err) { fn(err); } else { - fn(null, {email: email, token: user.verificationToken, uid: user.id}); - } - }); - } -} - - -/** - * Confirm the user's identity. - * - * @param {Any} userId - * @param {String} token The validation token - * @param {String} redirect URL to redirect the user to once confirmed - * @callback {Function} callback - * @param {Error} err - */ -User.confirm = function(uid, token, redirect, fn) { - this.findById(uid, function(err, user) { - if (err) { - fn(err); - } else { - if (user && user.verificationToken === token) { - user.verificationToken = undefined; - user.emailVerified = true; + user.verificationToken = buf.toString('hex'); user.save(function(err) { if (err) { fn(err); } else { - fn(); + sendEmail(user); } }); - } else { - if (user) { - err = new Error('Invalid token: ' + token); - err.statusCode = 400; + } + }); + + // TODO - support more verification types + function sendEmail(user) { + options.verifyHref += '&token=' + user.verificationToken; + + options.text = options.text || 'Please verify your email by opening this link in a web browser:\n\t{href}'; + + options.text = options.text.replace('{href}', options.verifyHref); + + var template = loopback.template(options.template); + Email.send({ + to: options.to || user.email, + from: options.from, + subject: options.subject || 'Thanks for Registering', + text: options.text, + html: template(options), + headers: options.headers || {} + }, function(err, email) { + if (err) { + fn(err); } else { - err = new Error('User not found: ' + uid); - err.statusCode = 404; + fn(null, {email: email, token: user.verificationToken, uid: user.id}); } - fn(err); - } + }); } - }); -} + }; -/** - * Create a short lived acess token for temporary login. Allows users - * to change passwords if forgotten. - * - * @options {Object} options - * @prop {String} email The user's email address - * @callback {Function} callback - * @param {Error} err - */ - -User.resetPassword = function(options, cb) { - var UserModel = this; - var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL; - - options = options || {}; - if (typeof options.email === 'string') { - UserModel.findOne({ where: {email: options.email} }, function(err, user) { + /** + * Confirm the user's identity. + * + * @param {Any} userId + * @param {String} token The validation token + * @param {String} redirect URL to redirect the user to once confirmed + * @callback {Function} callback + * @param {Error} err + */ + User.confirm = function(uid, token, redirect, fn) { + this.findById(uid, function(err, user) { if (err) { - cb(err); - } else if (user) { - // create a short lived access token for temp login to change password - // TODO(ritch) - eventually this should only allow password change - user.accessTokens.create({ttl: ttl}, function(err, accessToken) { - if (err) { - cb(err); + fn(err); + } else { + if (user && user.verificationToken === token) { + user.verificationToken = undefined; + user.emailVerified = true; + user.save(function(err) { + if (err) { + fn(err); + } else { + fn(); + } + }); + } else { + if (user) { + err = new Error('Invalid token: ' + token); + err.statusCode = 400; } else { - cb(); - UserModel.emit('resetPasswordRequest', { - email: options.email, - accessToken: accessToken, - user: user - }); + err = new Error('User not found: ' + uid); + err.statusCode = 404; } - }) - } else { - cb(); - } - }); - } else { - var err = new Error('email is required'); - err.statusCode = 400; - - cb(err); - } -} - -/*! - * Setup an extended user model. - */ - -User.setup = function() { - // We need to call the base class's setup method - User.base.setup.call(this); - var UserModel = this; - - // max ttl - this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL; - this.settings.ttl = DEFAULT_TTL; - - UserModel.setter.password = function(plain) { - var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR); - this.$password = bcrypt.hashSync(plain, salt); - } - - // Make sure emailVerified is not set by creation - UserModel.beforeRemote('create', function(ctx, user, next) { - var body = ctx.req.body; - if (body && body.emailVerified) { - body.emailVerified = false; - } - next(); - }); - - loopback.remoteMethod( - UserModel.login, - { - description: 'Login a user with username/email and password', - accepts: [ - {arg: 'credentials', type: 'object', required: true, http: {source: 'body'}}, - {arg: 'include', type: 'string', http: {source: 'query' }, description: 'Related objects to include in the response. ' + - 'See the description of return value for more details.'} - ], - returns: { - arg: 'accessToken', type: 'object', root: true, description: 'The response body contains properties of the AccessToken created on login.\n' + - 'Depending on the value of `include` parameter, the body may contain ' + - 'additional properties:\n\n' + - ' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n' - }, - http: {verb: 'post'} - } - ); - - loopback.remoteMethod( - UserModel.logout, - { - description: 'Logout a user with access token', - accepts: [ - {arg: 'access_token', type: 'string', required: true, http: function(ctx) { - var req = ctx && ctx.req; - var accessToken = req && req.accessToken; - var tokenID = accessToken && accessToken.id; - - return tokenID; - }, description: 'Do not supply this argument, it is automatically extracted ' + - 'from request headers.' + fn(err); } - ], - http: {verb: 'all'} - } - ); - - loopback.remoteMethod( - UserModel.confirm, - { - description: 'Confirm a user registration with email verification token', - accepts: [ - {arg: 'uid', type: 'string', required: true}, - {arg: 'token', type: 'string', required: true}, - {arg: 'redirect', type: 'string', required: true} - ], - http: {verb: 'get', path: '/confirm'} - } - ); - - loopback.remoteMethod( - UserModel.resetPassword, - { - description: 'Reset password for a user with email', - accepts: [ - {arg: 'options', type: 'object', required: true, http: {source: 'body'}} - ], - http: {verb: 'post', path: '/reset'} - } - ); - - UserModel.on('attached', function() { - UserModel.afterRemote('confirm', function(ctx, inst, next) { - if (ctx.req) { - ctx.res.redirect(ctx.req.param('redirect')); - } else { - next(new Error('transport unsupported')); } }); - }); + }; - // default models - assert(loopback.Email, 'Email model must be defined before User model'); - UserModel.email = loopback.Email; + /** + * Create a short lived acess token for temporary login. Allows users + * to change passwords if forgotten. + * + * @options {Object} options + * @prop {String} email The user's email address + * @callback {Function} callback + * @param {Error} err + */ - assert(loopback.AccessToken, 'AccessToken model must be defined before User model'); - UserModel.accessToken = loopback.AccessToken; + User.resetPassword = function(options, cb) { + var UserModel = this; + var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL; - // 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,}))$/; + options = options || {}; + if (typeof options.email === 'string') { + UserModel.findOne({ where: {email: options.email} }, function(err, user) { + if (err) { + cb(err); + } else if (user) { + // create a short lived access token for temp login to change password + // TODO(ritch) - eventually this should only allow password change + user.accessTokens.create({ttl: ttl}, function(err, accessToken) { + if (err) { + cb(err); + } else { + cb(); + UserModel.emit('resetPasswordRequest', { + email: options.email, + accessToken: accessToken, + user: user + }); + } + }); + } else { + cb(); + } + }); + } else { + var err = new Error('email is required'); + err.statusCode = 400; + cb(err); + } + }; - UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'}); + /*! + * Setup an extended user model. + */ - // FIXME: We need to add support for uniqueness of composite keys in juggler - if (!(UserModel.settings.realmRequired || UserModel.settings.realmDelimiter)) { - UserModel.validatesUniquenessOf('email', {message: 'Email already exists'}); - UserModel.validatesUniquenessOf('username', {message: 'User already exists'}); - } + User.setup = function() { + // We need to call the base class's setup method + User.base.setup.call(this); + var UserModel = this; - return UserModel; -} + // max ttl + this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL; + this.settings.ttl = DEFAULT_TTL; -/*! - * Setup the base user. - */ + UserModel.setter.password = function(plain) { + var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR); + this.$password = bcrypt.hashSync(plain, salt); + }; -User.setup(); + // Make sure emailVerified is not set by creation + UserModel.beforeRemote('create', function(ctx, user, next) { + var body = ctx.req.body; + if (body && body.emailVerified) { + body.emailVerified = false; + } + next(); + }); + + loopback.remoteMethod( + UserModel.login, + { + description: 'Login a user with username/email and password', + accepts: [ + {arg: 'credentials', type: 'object', required: true, http: {source: 'body'}}, + {arg: 'include', type: 'string', http: {source: 'query' }, + description: 'Related objects to include in the response. ' + + 'See the description of return value for more details.'} + ], + returns: { + arg: 'accessToken', type: 'object', root: true, + description: + 'The response body contains properties of the AccessToken created on login.\n' + + 'Depending on the value of `include` parameter, the body may contain ' + + 'additional properties:\n\n' + + ' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n' + }, + http: {verb: 'post'} + } + ); + + loopback.remoteMethod( + UserModel.logout, + { + description: 'Logout a user with access token', + accepts: [ + {arg: 'access_token', type: 'string', required: true, http: function(ctx) { + var req = ctx && ctx.req; + var accessToken = req && req.accessToken; + var tokenID = accessToken && accessToken.id; + + return tokenID; + }, description: 'Do not supply this argument, it is automatically extracted ' + + 'from request headers.' + } + ], + http: {verb: 'all'} + } + ); + + loopback.remoteMethod( + UserModel.confirm, + { + description: 'Confirm a user registration with email verification token', + accepts: [ + {arg: 'uid', type: 'string', required: true}, + {arg: 'token', type: 'string', required: true}, + {arg: 'redirect', type: 'string', required: true} + ], + http: {verb: 'get', path: '/confirm'} + } + ); + + loopback.remoteMethod( + UserModel.resetPassword, + { + description: 'Reset password for a user with email', + accepts: [ + {arg: 'options', type: 'object', required: true, http: {source: 'body'}} + ], + http: {verb: 'post', path: '/reset'} + } + ); + + UserModel.on('attached', function() { + UserModel.afterRemote('confirm', function(ctx, inst, next) { + if (ctx.req) { + ctx.res.redirect(ctx.req.param('redirect')); + } else { + next(new Error('transport unsupported')); + } + }); + }); + + // default models + assert(loopback.Email, 'Email model must be defined before User model'); + UserModel.email = loopback.Email; + + assert(loopback.AccessToken, 'AccessToken model must be defined before User model'); + UserModel.accessToken = loopback.AccessToken; + + // 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,}))$/; + + UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'}); + + // FIXME: We need to add support for uniqueness of composite keys in juggler + if (!(UserModel.settings.realmRequired || UserModel.settings.realmDelimiter)) { + UserModel.validatesUniquenessOf('email', {message: 'Email already exists'}); + UserModel.validatesUniquenessOf('username', {message: 'User already exists'}); + } + + return UserModel; + }; + + /*! + * Setup the base user. + */ + + User.setup(); };