2013-06-25 15:43:15 +04:00
var User ,
Users ,
2013-09-24 14:46:30 +04:00
_ = require ( 'underscore' ) ,
uuid = require ( 'node-uuid' ) ,
when = require ( 'when' ) ,
errors = require ( '../errorHandling' ) ,
nodefn = require ( 'when/node/function' ) ,
2013-10-23 17:00:28 +04:00
bcrypt = require ( 'bcryptjs' ) ,
2013-09-24 14:46:30 +04:00
Posts = require ( './post' ) . Posts ,
2013-09-23 02:20:08 +04:00
ghostBookshelf = require ( './base' ) ,
2013-09-24 14:46:30 +04:00
Role = require ( './role' ) . Role ,
2013-11-11 23:55:22 +04:00
Permission = require ( './permission' ) . Permission ,
http = require ( 'http' ) ,
crypto = require ( 'crypto' ) ;
2013-06-25 15:43:15 +04:00
2013-08-20 22:52:44 +04:00
function validatePasswordLength ( password ) {
try {
2013-11-22 07:17:38 +04:00
ghostBookshelf . validator . check ( password , "Your password must be at least 8 characters long." ) . len ( 8 ) ;
2013-08-20 22:52:44 +04:00
} catch ( error ) {
return when . reject ( error ) ;
}
return when . resolve ( ) ;
}
2013-11-22 07:17:38 +04:00
function generatePasswordHash ( password ) {
// Generate a new salt
return nodefn . call ( bcrypt . genSalt ) . then ( function ( salt ) {
// Hash the provided password with bcrypt
return nodefn . call ( bcrypt . hash , password , salt ) ;
} ) ;
}
2013-09-23 02:20:08 +04:00
User = ghostBookshelf . Model . extend ( {
2013-06-25 15:43:15 +04:00
tableName : 'users' ,
2013-08-25 14:49:31 +04:00
permittedAttributes : [
2013-09-14 23:01:46 +04:00
'id' , 'uuid' , 'name' , 'slug' , 'password' , 'email' , 'image' , 'cover' , 'bio' , 'website' , 'location' ,
2013-11-03 21:13:19 +04:00
'accessibility' , 'status' , 'language' , 'meta_title' , 'meta_description' , 'last_login' , 'created_at' ,
'created_by' , 'updated_at' , 'updated_by'
2013-08-25 14:49:31 +04:00
] ,
2013-08-20 22:52:44 +04:00
validate : function ( ) {
2013-09-23 02:20:08 +04:00
ghostBookshelf . validator . check ( this . get ( 'email' ) , "Please enter a valid email address. That one looks a bit dodgy." ) . isEmail ( ) ;
ghostBookshelf . validator . check ( this . get ( 'bio' ) , "We're not writing a novel here! I'm afraid your bio has to stay under 200 characters." ) . len ( 0 , 200 ) ;
2013-09-14 23:01:46 +04:00
if ( this . get ( 'website' ) && this . get ( 'website' ) . length > 0 ) {
2013-09-23 02:20:08 +04:00
ghostBookshelf . validator . check ( this . get ( 'website' ) , "Looks like your website is not actually a website. Try again?" ) . isUrl ( ) ;
2013-08-20 22:52:44 +04:00
}
2013-10-17 15:22:24 +04:00
ghostBookshelf . validator . check ( this . get ( 'location' ) , 'This seems a little too long! Please try and keep your location under 150 characters.' ) . len ( 0 , 150 ) ;
2013-08-20 22:52:44 +04:00
return true ;
} ,
2013-09-14 23:01:46 +04:00
creating : function ( ) {
var self = this ;
2013-08-25 14:49:31 +04:00
2013-09-23 02:20:08 +04:00
ghostBookshelf . Model . prototype . creating . call ( this ) ;
2013-09-14 23:01:46 +04:00
if ( ! this . get ( 'slug' ) ) {
// Generating a slug requires a db call to look for conflicting slugs
2014-01-05 21:55:59 +04:00
return ghostBookshelf . Model . generateSlug ( User , this . get ( 'name' ) )
2013-09-14 23:01:46 +04:00
. then ( function ( slug ) {
self . set ( { slug : slug } ) ;
} ) ;
}
2013-08-25 14:49:31 +04:00
} ,
2013-10-07 21:02:57 +04:00
saving : function ( ) {
2014-01-07 00:17:20 +04:00
// disabling sanitization until we can implement a better version
// this.set('name', this.sanitize('name'));
2014-01-15 02:47:17 +04:00
// this.set('email', this.sanitize('email'));
2014-01-07 00:17:20 +04:00
// this.set('location', this.sanitize('location'));
// this.set('website', this.sanitize('website'));
// this.set('bio', this.sanitize('bio'));
2013-10-07 21:02:57 +04:00
2013-09-23 02:20:08 +04:00
return ghostBookshelf . Model . prototype . saving . apply ( this , arguments ) ;
2013-10-07 21:02:57 +04:00
} ,
2013-06-25 15:43:15 +04:00
posts : function ( ) {
return this . hasMany ( Posts , 'created_by' ) ;
} ,
roles : function ( ) {
return this . belongsToMany ( Role ) ;
} ,
permissions : function ( ) {
return this . belongsToMany ( Permission ) ;
}
} , {
/ * *
* Naive user add
* @ param _user
*
* Hashes the password provided before saving to the database .
* /
add : function ( _user ) {
2013-08-16 03:22:08 +04:00
var self = this ,
// Clone the _user so we don't expose the hashed password unnecessarily
userData = _ . extend ( { } , _user ) ;
2013-06-01 18:47:41 +04:00
/ * *
2013-06-25 15:43:15 +04:00
* This only allows one user to be added to the database , otherwise fails .
* @ param { object } user
* @ author javorszky
2013-06-01 18:47:41 +04:00
* /
2013-08-20 22:52:44 +04:00
return validatePasswordLength ( userData . password ) . then ( function ( ) {
return self . forge ( ) . fetch ( ) ;
} ) . then ( function ( user ) {
2013-08-16 03:22:08 +04:00
// Check if user exists
2013-06-25 15:43:15 +04:00
if ( user ) {
return when . reject ( new Error ( 'A user is already registered. Only one user for now!' ) ) ;
}
2013-08-20 22:52:44 +04:00
} ) . then ( function ( ) {
2013-11-22 07:17:38 +04:00
// Generate a new password hash
return generatePasswordHash ( _user . password ) ;
2013-08-16 03:22:08 +04:00
} ) . then ( function ( hash ) {
// Assign the hashed password
userData . password = hash ;
2013-11-11 23:55:22 +04:00
// LookupGravatar
return self . gravatarLookup ( userData ) ;
} ) . then ( function ( userData ) {
2013-08-16 03:22:08 +04:00
// Save the user with the hashed password
2013-09-23 02:20:08 +04:00
return ghostBookshelf . Model . add . call ( self , userData ) ;
2013-08-16 03:22:08 +04:00
} ) . then ( function ( addedUser ) {
// Assign the userData to our created user so we can pass it back
userData = addedUser ;
// Add this user to the admin role (assumes admin = role_id: 1)
2013-09-13 18:06:17 +04:00
return userData . roles ( ) . attach ( 1 ) ;
2013-08-16 03:22:08 +04:00
} ) . then ( function ( addedUserRole ) {
2013-10-31 22:02:34 +04:00
/*jslint unparam:true*/
2013-08-16 03:22:08 +04:00
// Return the added user as expected
2013-09-01 02:20:12 +04:00
2013-08-16 03:22:08 +04:00
return when . resolve ( userData ) ;
2013-08-19 01:50:42 +04:00
} ) ;
2013-06-01 18:47:41 +04:00
2013-06-25 15:43:15 +04:00
/ * *
* Temporarily replacing the function below with another one that checks
* whether there ' s anyone registered at all . This is due to # 138
* @ author javorszky
* /
2013-06-01 18:47:41 +04:00
2013-09-14 23:01:46 +04:00
// return this.forge({email: userData.email}).fetch().then(function (user) {
2013-08-09 05:22:49 +04:00
// if (user !== null) {
// return when.reject(new Error('A user with that email address already exists.'));
// }
// return nodefn.call(bcrypt.hash, _user.password, null, null).then(function (hash) {
// userData.password = hash;
2013-09-23 02:20:08 +04:00
// ghostBookshelf.Model.add.call(UserRole, userRoles);
// return ghostBookshelf.Model.add.call(User, userData);
2013-08-09 05:22:49 +04:00
// }, errors.logAndThrowError);
// }, errors.logAndThrowError);
2013-06-25 15:43:15 +04:00
} ,
2013-11-29 04:28:01 +04:00
setWarning : function ( user ) {
var status = user . get ( 'status' ) ,
regexp = /warn-(\d+)/i ,
level ;
if ( status === 'active' ) {
user . set ( 'status' , 'warn-1' ) ;
level = 1 ;
} else {
level = parseInt ( status . match ( regexp ) [ 1 ] , 10 ) + 1 ;
if ( level > 3 ) {
user . set ( 'status' , 'locked' ) ;
} else {
user . set ( 'status' , 'warn-' + level ) ;
}
}
return when ( user . save ( ) ) . then ( function ( ) {
return 5 - level ;
} ) ;
} ,
2013-08-06 23:27:56 +04:00
// Finds the user by email, and checks the password
2013-06-25 15:43:15 +04:00
check : function ( _userdata ) {
2013-11-29 04:28:01 +04:00
var self = this ,
s ;
2014-01-15 02:47:17 +04:00
return this . getByEmail ( _userdata . email ) . then ( function ( user ) {
2013-11-29 04:28:01 +04:00
if ( user . get ( 'status' ) !== 'locked' ) {
return nodefn . call ( bcrypt . compare , _userdata . pw , user . get ( 'password' ) ) . then ( function ( matched ) {
if ( ! matched ) {
return when ( self . setWarning ( user ) ) . then ( function ( remaining ) {
s = ( remaining > 1 ) ? 's' : '' ;
return when . reject ( new Error ( 'Your password is incorrect.<br>' +
remaining + ' attempt' + s + ' remaining!' ) ) ;
} ) ;
}
return when ( user . set ( 'status' , 'active' ) . save ( ) ) . then ( function ( user ) {
return user ;
} ) ;
} , errors . logAndThrowError ) ;
}
return when . reject ( new Error ( 'Your account is locked due to too many ' +
'login attempts. Please reset your password to log in again by clicking ' +
'the "Forgotten password?" link!' ) ) ;
2013-08-09 05:22:49 +04:00
} , function ( error ) {
2014-01-15 02:47:17 +04:00
if ( error . message === 'NotFound' || error . message === 'EmptyResponse' ) {
return when . reject ( new Error ( 'There is no user with that email address.' ) ) ;
}
return when . reject ( error ) ;
2013-08-09 05:22:49 +04:00
} ) ;
2013-06-25 15:43:15 +04:00
} ,
2013-08-06 03:49:06 +04:00
/ * *
* Naive change password method
* @ param { object } _userdata email , old pw , new pw , new pw2
*
* /
changePassword : function ( _userdata ) {
2013-08-20 22:52:44 +04:00
var self = this ,
userid = _userdata . currentUser ,
2013-08-06 03:49:06 +04:00
oldPassword = _userdata . oldpw ,
newPassword = _userdata . newpw ,
2013-09-01 02:20:12 +04:00
ne2Password = _userdata . ne2pw ,
user = null ;
2013-08-06 03:49:06 +04:00
if ( newPassword !== ne2Password ) {
2013-08-20 22:52:44 +04:00
return when . reject ( new Error ( 'Your new passwords do not match' ) ) ;
2013-08-06 03:49:06 +04:00
}
2013-08-20 22:52:44 +04:00
return validatePasswordLength ( newPassword ) . then ( function ( ) {
return self . forge ( { id : userid } ) . fetch ( { require : true } ) ;
2013-09-01 02:20:12 +04:00
} ) . then ( function ( _user ) {
user = _user ;
return nodefn . call ( bcrypt . compare , oldPassword , user . get ( 'password' ) ) ;
} ) . then ( function ( matched ) {
if ( ! matched ) {
return when . reject ( new Error ( 'Your password is incorrect' ) ) ;
}
2013-10-23 17:00:28 +04:00
return nodefn . call ( bcrypt . genSalt ) ;
} ) . then ( function ( salt ) {
return nodefn . call ( bcrypt . hash , newPassword , salt ) ;
2013-09-01 02:20:12 +04:00
} ) . then ( function ( hash ) {
user . save ( { password : hash } ) ;
return user ;
2013-08-06 03:49:06 +04:00
} ) ;
2013-09-01 02:20:12 +04:00
} ,
2013-11-22 07:17:38 +04:00
generateResetToken : function ( email , expires , dbHash ) {
2014-01-15 02:47:17 +04:00
return this . getByEmail ( email ) . then ( function ( foundUser ) {
2013-11-22 07:17:38 +04:00
var hash = crypto . createHash ( 'sha256' ) ,
text = "" ;
2013-08-20 22:52:44 +04:00
2013-11-22 07:17:38 +04:00
// Token:
// BASE64(TIMESTAMP + email + HASH(TIMESTAMP + email + oldPasswordHash + dbHash ))
hash . update ( String ( expires ) ) ;
hash . update ( email . toLocaleLowerCase ( ) ) ;
hash . update ( foundUser . get ( 'password' ) ) ;
hash . update ( String ( dbHash ) ) ;
text += [ expires , email , hash . digest ( 'base64' ) ] . join ( '|' ) ;
return new Buffer ( text ) . toString ( 'base64' ) ;
} ) ;
} ,
validateToken : function ( token , dbHash ) {
// TODO: Is there a chance the use of ascii here will cause problems if oldPassword has weird characters?
var tokenText = new Buffer ( token , 'base64' ) . toString ( 'ascii' ) ,
parts ,
expires ,
email ;
parts = tokenText . split ( '|' ) ;
// Check if invalid structure
if ( ! parts || parts . length !== 3 ) {
return when . reject ( new Error ( "Invalid token structure" ) ) ;
}
expires = parseInt ( parts [ 0 ] , 10 ) ;
email = parts [ 1 ] ;
if ( isNaN ( expires ) ) {
return when . reject ( new Error ( "Invalid token expiration" ) ) ;
}
// This is easy to fake, but still check anyway.
if ( expires < Date . now ( ) ) {
return when . reject ( new Error ( "Expired token" ) ) ;
}
return this . generateResetToken ( email , expires , dbHash ) . then ( function ( generatedToken ) {
// Check for matching tokens
if ( token === generatedToken ) {
return when . resolve ( email ) ;
}
return when . reject ( new Error ( "Invalid token" ) ) ;
} ) ;
} ,
resetPassword : function ( token , newPassword , ne2Password , dbHash ) {
var self = this ;
if ( newPassword !== ne2Password ) {
return when . reject ( new Error ( "Your new passwords do not match" ) ) ;
}
return validatePasswordLength ( newPassword ) . then ( function ( ) {
// Validate the token; returns the email address from token
return self . validateToken ( token , dbHash ) ;
} ) . then ( function ( email ) {
// Fetch the user by email, and hash the password at the same time.
return when . join (
self . forge ( { email : email . toLocaleLowerCase ( ) } ) . fetch ( { require : true } ) ,
generatePasswordHash ( newPassword )
) ;
} ) . then ( function ( results ) {
// Update the user with the new password hash
var foundUser = results [ 0 ] ,
passwordHash = results [ 1 ] ;
2013-11-29 04:28:01 +04:00
foundUser . save ( { password : passwordHash , status : 'active' } ) ;
2013-11-22 07:17:38 +04:00
return foundUser ;
2013-09-01 02:20:12 +04:00
} ) ;
2013-08-06 03:49:06 +04:00
} ,
2013-06-25 15:43:15 +04:00
effectivePermissions : function ( id ) {
return this . read ( { id : id } , { withRelated : [ 'permissions' , 'roles.permissions' ] } )
. then ( function ( foundUser ) {
var seenPerms = { } ,
rolePerms = _ . map ( foundUser . related ( 'roles' ) . models , function ( role ) {
return role . related ( 'permissions' ) . models ;
} ) ,
allPerms = [ ] ;
rolePerms . push ( foundUser . related ( 'permissions' ) . models ) ;
_ . each ( rolePerms , function ( rolePermGroup ) {
_ . each ( rolePermGroup , function ( perm ) {
var key = perm . get ( 'action_type' ) + '-' + perm . get ( 'object_type' ) + '-' + perm . get ( 'object_id' ) ;
// Only add perms once
if ( seenPerms [ key ] ) {
return ;
}
allPerms . push ( perm ) ;
seenPerms [ key ] = true ;
2013-06-05 07:47:11 +04:00
} ) ;
2013-06-25 15:43:15 +04:00
} ) ;
2013-06-05 07:47:11 +04:00
2013-06-25 15:43:15 +04:00
return when . resolve ( allPerms ) ;
} , errors . logAndThrowError ) ;
2013-11-11 23:55:22 +04:00
} ,
gravatarLookup : function ( userData ) {
2013-12-17 20:21:00 +04:00
var gravatarUrl = '//www.gravatar.com/avatar/' +
2013-11-11 23:55:22 +04:00
crypto . createHash ( 'md5' ) . update ( userData . email . toLowerCase ( ) . trim ( ) ) . digest ( 'hex' ) +
"?d=404" ,
checkPromise = when . defer ( ) ;
2014-01-01 21:47:12 +04:00
http . get ( 'http:' + gravatarUrl , function ( res ) {
2013-11-11 23:55:22 +04:00
if ( res . statusCode !== 404 ) {
userData . image = gravatarUrl ;
}
checkPromise . resolve ( userData ) ;
} ) . on ( 'error' , function ( ) {
//Error making request just continue.
checkPromise . resolve ( userData ) ;
} ) ;
return checkPromise . promise ;
2014-01-15 02:47:17 +04:00
} ,
// Get the user by email address, enforces case insensitivity rejects if the user is not found
// When multi-user support is added, email addresses must be deduplicated with case insensitivity, so that
// joe@bloggs.com and JOE@BLOGGS.COM cannot be created as two separate users.
getByEmail : function ( email ) {
// We fetch all users and process them in JS as there is no easy way to make this query across all DBs
// Although they all support `lower()`, sqlite can't case transform unicode characters
// This is somewhat mute, as validator.isEmail() also doesn't support unicode, but this is much easier / more
// likely to be fixed in the near future.
return Users . forge ( ) . fetch ( { require : true } ) . then ( function ( users ) {
var userWithEmail = users . find ( function ( user ) {
return user . get ( 'email' ) . toLowerCase ( ) === email . toLowerCase ( ) ;
} ) ;
if ( userWithEmail ) {
return when . resolve ( userWithEmail ) ;
}
2013-06-01 18:47:41 +04:00
2014-01-15 02:47:17 +04:00
return when . reject ( new Error ( 'NotFound' ) ) ;
} ) ;
}
2013-06-25 15:43:15 +04:00
} ) ;
2013-06-01 18:47:41 +04:00
2013-09-23 02:20:08 +04:00
Users = ghostBookshelf . Collection . extend ( {
2013-06-25 15:43:15 +04:00
model : User
} ) ;
2013-06-01 18:47:41 +04:00
2013-06-25 15:43:15 +04:00
module . exports = {
User : User ,
Users : Users
2013-08-06 03:49:06 +04:00
} ;