models: move User LDL def into `user.json`

This commit is contained in:
Miroslav Bajtoš 2014-10-10 11:53:22 +02:00
parent 01d17e636a
commit 920d3be6a3
3 changed files with 190 additions and 169 deletions

View File

@ -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();
};

96
common/models/user.json Normal file
View File

@ -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"
}
}
}

View File

@ -1,12 +1,19 @@
module.exports = function(loopback) { module.exports = function(loopback) {
// NOTE(bajtos) we must use static require() due to browserify limitations
loopback.Email = require('../common/models/email'); loopback.Email = require('../common/models/email');
loopback.User = require('../common/models/user');
loopback.Application = require('../common/models/application'); loopback.Application = require('../common/models/application');
loopback.AccessToken = require('../common/models/access-token'); loopback.AccessToken = require('../common/models/access-token');
loopback.Role = require('../common/models/role').Role; loopback.Role = require('../common/models/role').Role;
loopback.RoleMapping = require('../common/models/role').RoleMapping; loopback.RoleMapping = require('../common/models/role').RoleMapping;
loopback.ACL = require('../common/models/acl').ACL; loopback.ACL = require('../common/models/acl').ACL;
loopback.Scope = require('../common/models/acl').Scope; loopback.Scope = require('../common/models/acl').Scope;
loopback.User = createModel(
require('../common/models/user.json'),
require('../common/models/user.js'));
loopback.Change = require('../common/models/change'); loopback.Change = require('../common/models/change');
loopback.Checkpoint = require('../common/models/checkpoint'); loopback.Checkpoint = require('../common/models/checkpoint');
@ -28,4 +35,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;
}
}; };