2015-07-20 00:09:47 -07:00
/* jshint node:true */
'use strict' ;
exports = module . exports = {
UserError : UserError ,
list : listUsers ,
create : createUser ,
verify : verify ,
verifyWithEmail : verifyWithEmail ,
remove : removeUser ,
get : getUser ,
getByResetToken : getByResetToken ,
changeAdmin : changeAdmin ,
2016-01-15 16:04:33 +01:00
getAllAdmins : getAllAdmins ,
2015-07-20 00:09:47 -07:00
resetPasswordByIdentifier : resetPasswordByIdentifier ,
setPassword : setPassword ,
changePassword : changePassword ,
update : updateUser ,
2016-01-13 12:28:38 -08:00
createOwner : createOwner ,
2016-01-18 15:16:18 +01:00
getOwner : getOwner ,
sendInvite : sendInvite
2015-07-20 00:09:47 -07:00
} ;
var assert = require ( 'assert' ) ,
crypto = require ( 'crypto' ) ,
DatabaseError = require ( './databaseerror.js' ) ,
mailer = require ( './mailer.js' ) ,
hat = require ( 'hat' ) ,
userdb = require ( './userdb.js' ) ,
tokendb = require ( './tokendb.js' ) ,
clientdb = require ( './clientdb.js' ) ,
util = require ( 'util' ) ,
validator = require ( 'validator' ) ,
_ = require ( 'underscore' ) ;
var CRYPTO _SALT _SIZE = 64 ; // 512-bit salt
var CRYPTO _ITERATIONS = 10000 ; // iterations
var CRYPTO _KEY _LENGTH = 512 ; // bits
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function UserError ( reason , errorOrMessage ) {
assert . strictEqual ( typeof reason , 'string' ) ;
assert ( errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined' ) ;
Error . call ( this ) ;
Error . captureStackTrace ( this , this . constructor ) ;
this . name = this . constructor . name ;
this . reason = reason ;
if ( typeof errorOrMessage === 'undefined' ) {
this . message = reason ;
} else if ( typeof errorOrMessage === 'string' ) {
this . message = errorOrMessage ;
} else {
this . message = 'Internal error' ;
this . nestedError = errorOrMessage ;
}
}
util . inherits ( UserError , Error ) ;
UserError . INTERNAL _ERROR = 'Internal Error' ;
UserError . ALREADY _EXISTS = 'Already Exists' ;
UserError . NOT _FOUND = 'Not Found' ;
UserError . WRONG _PASSWORD = 'Wrong User or Password' ;
UserError . BAD _FIELD = 'Bad field' ;
UserError . BAD _USERNAME = 'Bad username' ;
UserError . BAD _EMAIL = 'Bad email' ;
UserError . BAD _PASSWORD = 'Bad password' ;
UserError . BAD _TOKEN = 'Bad token' ;
UserError . NOT _ALLOWED = 'Not Allowed' ;
2016-01-20 14:38:37 +01:00
// http://www.w3resource.com/javascript/form/example4-javascript-form-validation-password.html
var gPasswordTestRegExp = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,20}$/ ;
2015-07-20 00:09:47 -07:00
function listUsers ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
userdb . getAll ( function ( error , result ) {
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
return callback ( null , result . map ( function ( obj ) { return _ . pick ( obj , 'id' , 'username' , 'email' , 'admin' ) ; } ) ) ;
} ) ;
}
function validateUsername ( username ) {
assert . strictEqual ( typeof username , 'string' ) ;
if ( username . length <= 2 ) return new UserError ( UserError . BAD _USERNAME , 'Username must be atleast 3 chars' ) ;
if ( username . length > 256 ) return new UserError ( UserError . BAD _USERNAME , 'Username too long' ) ;
return null ;
}
function validatePassword ( password ) {
assert . strictEqual ( typeof password , 'string' ) ;
2016-01-20 14:49:45 +01:00
if ( ! password . match ( gPasswordTestRegExp ) ) return new UserError ( UserError . BAD _PASSWORD , 'Password must be 8-20 character with at least one uppercase, one numeric and one special character' ) ;
2015-07-20 00:09:47 -07:00
return null ;
}
function validateEmail ( email ) {
assert . strictEqual ( typeof email , 'string' ) ;
if ( ! validator . isEmail ( email ) ) return new UserError ( UserError . BAD _EMAIL , 'Invalid email' ) ;
return null ;
}
function validateToken ( token ) {
assert . strictEqual ( typeof token , 'string' ) ;
if ( token . length !== 64 ) return new UserError ( UserError . BAD _TOKEN , 'Invalid token' ) ; // 256-bit hex coded token
return null ;
}
2016-01-19 23:34:49 -08:00
function validateDisplayName ( name ) {
assert . strictEqual ( typeof name , 'string' ) ;
return null ;
}
function createUser ( username , password , email , displayName , admin , invitor , sendInvite , callback ) {
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof username , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
assert . strictEqual ( typeof email , 'string' ) ;
2016-01-19 23:34:49 -08:00
assert . strictEqual ( typeof displayName , 'string' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof admin , 'boolean' ) ;
assert ( invitor || admin ) ;
2016-01-18 13:48:10 +01:00
assert . strictEqual ( typeof sendInvite , 'boolean' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
var error = validateUsername ( username ) ;
if ( error ) return callback ( error ) ;
error = validatePassword ( password ) ;
if ( error ) return callback ( error ) ;
error = validateEmail ( email ) ;
if ( error ) return callback ( error ) ;
2016-01-19 23:34:49 -08:00
error = validateDisplayName ( displayName ) ;
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
crypto . randomBytes ( CRYPTO _SALT _SIZE , function ( error , salt ) {
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
crypto . pbkdf2 ( password , salt , CRYPTO _ITERATIONS , CRYPTO _KEY _LENGTH , function ( error , derivedKey ) {
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
2015-09-17 13:50:20 -07:00
var now = ( new Date ( ) ) . toISOString ( ) ;
2015-07-20 00:09:47 -07:00
var user = {
id : username ,
username : username ,
email : email ,
password : new Buffer ( derivedKey , 'binary' ) . toString ( 'hex' ) ,
admin : admin ,
salt : salt . toString ( 'hex' ) ,
createdAt : now ,
modifiedAt : now ,
2016-01-19 12:40:50 +01:00
resetToken : hat ( 256 ) ,
2016-01-19 23:34:49 -08:00
displayName : displayName
2015-07-20 00:09:47 -07:00
} ;
userdb . add ( user . id , user , function ( error ) {
if ( error && error . reason === DatabaseError . ALREADY _EXISTS ) return callback ( new UserError ( UserError . ALREADY _EXISTS ) ) ;
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
callback ( null , user ) ;
2016-01-20 12:39:28 +01:00
// WARNING do not send email for admins (this can only be the case for the owner, the first user creation during activation)
2016-01-20 12:40:54 +01:00
if ( ! admin ) mailer . userAdded ( user , sendInvite ) ;
2016-01-18 16:11:00 +01:00
if ( sendInvite ) mailer . sendInvite ( user , invitor ) ;
2015-07-20 00:09:47 -07:00
} ) ;
} ) ;
} ) ;
}
function verify ( username , password , callback ) {
assert . strictEqual ( typeof username , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
userdb . get ( username , function ( error , user ) {
if ( error && error . reason == DatabaseError . NOT _FOUND ) return callback ( new UserError ( UserError . NOT _FOUND ) ) ;
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
var saltBinary = new Buffer ( user . salt , 'hex' ) ;
crypto . pbkdf2 ( password , saltBinary , CRYPTO _ITERATIONS , CRYPTO _KEY _LENGTH , function ( error , derivedKey ) {
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
var derivedKeyHex = new Buffer ( derivedKey , 'binary' ) . toString ( 'hex' ) ;
if ( derivedKeyHex !== user . password ) return callback ( new UserError ( UserError . WRONG _PASSWORD ) ) ;
callback ( null , user ) ;
} ) ;
} ) ;
}
function verifyWithEmail ( email , password , callback ) {
assert . strictEqual ( typeof email , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
userdb . getByEmail ( email , function ( error , user ) {
if ( error && error . reason == DatabaseError . NOT _FOUND ) return callback ( new UserError ( UserError . NOT _FOUND ) ) ;
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
var saltBinary = new Buffer ( user . salt , 'hex' ) ;
crypto . pbkdf2 ( password , saltBinary , CRYPTO _ITERATIONS , CRYPTO _KEY _LENGTH , function ( error , derivedKey ) {
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
var derivedKeyHex = new Buffer ( derivedKey , 'binary' ) . toString ( 'hex' ) ;
if ( derivedKeyHex !== user . password ) return callback ( new UserError ( UserError . WRONG _PASSWORD ) ) ;
callback ( null , user ) ;
} ) ;
} ) ;
}
2015-09-08 16:38:02 -07:00
function removeUser ( userId , callback ) {
assert . strictEqual ( typeof userId , 'string' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2015-09-08 16:38:02 -07:00
userdb . del ( userId , function ( error ) {
2015-07-20 00:09:47 -07:00
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new UserError ( UserError . NOT _FOUND ) ) ;
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
callback ( null ) ;
2015-09-08 16:38:02 -07:00
mailer . userRemoved ( userId ) ;
2015-07-20 00:09:47 -07:00
} ) ;
}
function getUser ( userId , callback ) {
assert . strictEqual ( typeof userId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
userdb . get ( userId , function ( error , result ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new UserError ( UserError . NOT _FOUND ) ) ;
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
return callback ( null , result ) ;
} ) ;
}
function getByResetToken ( resetToken , callback ) {
assert . strictEqual ( typeof resetToken , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var error = validateToken ( resetToken ) ;
if ( error ) return callback ( error ) ;
userdb . getByResetToken ( resetToken , function ( error , result ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new UserError ( UserError . NOT _FOUND ) ) ;
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
callback ( null , result ) ;
} ) ;
}
function updateUser ( userId , username , email , callback ) {
assert . strictEqual ( typeof userId , 'string' ) ;
assert . strictEqual ( typeof username , 'string' ) ;
assert . strictEqual ( typeof email , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var error = validateUsername ( username ) ;
if ( error ) return callback ( error ) ;
error = validateEmail ( email ) ;
if ( error ) return callback ( error ) ;
userdb . update ( userId , { username : username , email : email } , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new UserError ( UserError . NOT _FOUND , error ) ) ;
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
callback ( null ) ;
} ) ;
}
function changeAdmin ( username , admin , callback ) {
assert . strictEqual ( typeof username , 'string' ) ;
assert . strictEqual ( typeof admin , 'boolean' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
getUser ( username , function ( error , user ) {
if ( error ) return callback ( error ) ;
userdb . getAllAdmins ( function ( error , result ) {
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
// protect from a system where there is no admin left
if ( result . length <= 1 && ! admin ) return callback ( new UserError ( UserError . NOT _ALLOWED , 'Only admin' ) ) ;
user . admin = admin ;
userdb . update ( username , user , function ( error ) {
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
callback ( null ) ;
mailer . adminChanged ( user ) ;
} ) ;
} ) ;
} ) ;
}
2016-01-15 16:04:33 +01:00
function getAllAdmins ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
userdb . getAllAdmins ( function ( error , admins ) {
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
callback ( null , admins ) ;
} ) ;
}
2015-07-20 00:09:47 -07:00
function resetPasswordByIdentifier ( identifier , callback ) {
assert . strictEqual ( typeof identifier , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var getter ;
if ( identifier . indexOf ( '@' ) === - 1 ) getter = userdb . getByUsername ;
else getter = userdb . getByEmail ;
getter ( identifier , function ( error , result ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new UserError ( UserError . NOT _FOUND ) ) ;
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
result . resetToken = hat ( 256 ) ;
userdb . update ( result . id , result , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new UserError ( UserError . NOT _FOUND ) ) ;
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
mailer . passwordReset ( result ) ;
callback ( null ) ;
} ) ;
} ) ;
}
function setPassword ( userId , newPassword , callback ) {
assert . strictEqual ( typeof userId , 'string' ) ;
assert . strictEqual ( typeof newPassword , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var error = validatePassword ( newPassword ) ;
if ( error ) return callback ( error ) ;
userdb . get ( userId , function ( error , user ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new UserError ( UserError . NOT _FOUND ) ) ;
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
var saltBuffer = new Buffer ( user . salt , 'hex' ) ;
crypto . pbkdf2 ( newPassword , saltBuffer , CRYPTO _ITERATIONS , CRYPTO _KEY _LENGTH , function ( error , derivedKey ) {
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
2015-09-17 13:50:20 -07:00
user . modifiedAt = ( new Date ( ) ) . toISOString ( ) ;
2015-07-20 00:09:47 -07:00
user . password = new Buffer ( derivedKey , 'binary' ) . toString ( 'hex' ) ;
user . resetToken = '' ;
userdb . update ( userId , user , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new UserError ( UserError . NOT _FOUND ) ) ;
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
// Also generate a token so the new user can get logged in immediately
2015-10-15 16:31:45 -07:00
clientdb . getByAppIdAndType ( 'webadmin' , clientdb . TYPE _ADMIN , function ( error , result ) {
2015-07-20 00:09:47 -07:00
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
var token = tokendb . generateToken ( ) ;
var expiresAt = Date . now ( ) + 24 * 60 * 60 * 1000 ; // 1 day
tokendb . add ( token , tokendb . PREFIX _USER + user . id , result . id , expiresAt , '*' , function ( error ) {
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
callback ( null , { token : token , expiresAt : expiresAt } ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
}
function changePassword ( username , oldPassword , newPassword , callback ) {
assert . strictEqual ( typeof username , 'string' ) ;
assert . strictEqual ( typeof oldPassword , 'string' ) ;
assert . strictEqual ( typeof newPassword , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var error = validatePassword ( newPassword ) ;
if ( error ) return callback ( error ) ;
verify ( username , oldPassword , function ( error , user ) {
if ( error ) return callback ( error ) ;
setPassword ( user . id , newPassword , callback ) ;
} ) ;
}
2016-01-19 23:34:49 -08:00
function createOwner ( username , password , email , displayName , callback ) {
2015-07-20 00:09:47 -07:00
userdb . count ( function ( error , count ) {
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
if ( count !== 0 ) return callback ( new UserError ( UserError . ALREADY _EXISTS ) ) ;
2016-01-19 23:34:49 -08:00
createUser ( username , password , email , displayName , true /* admin */ , null /* invitor */ , false /* sendInvite */ , callback ) ;
2015-07-20 00:09:47 -07:00
} ) ;
}
2016-01-13 12:28:38 -08:00
function getOwner ( callback ) {
userdb . getOwner ( function ( error , owner ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new UserError ( UserError . NOT _FOUND ) ) ;
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
return callback ( null , owner ) ;
} ) ;
}
2016-01-18 15:16:18 +01:00
function sendInvite ( userId , callback ) {
assert . strictEqual ( typeof userId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
userdb . get ( userId , function ( error , userObject ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new UserError ( UserError . NOT _FOUND ) ) ;
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
userObject . resetToken = hat ( 256 ) ;
userdb . update ( userId , userObject , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new UserError ( UserError . NOT _FOUND ) ) ;
if ( error ) return callback ( new UserError ( UserError . INTERNAL _ERROR , error ) ) ;
2016-01-18 16:11:00 +01:00
mailer . sendInvite ( userObject , null ) ;
2016-01-18 15:16:18 +01:00
callback ( null ) ;
} ) ;
} ) ;
}