2016-05-03 22:50:21 +00:00
// Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
2014-04-14 21:49:29 +00:00
/ * !
2013-07-01 23:50:03 +00:00
* Module Dependencies .
* /
2016-11-15 21:46:23 +00:00
'use strict' ;
2016-09-16 19:31:48 +00:00
var g = require ( '../../lib/globalize' ) ;
2016-09-06 10:50:03 +00:00
var isEmail = require ( 'isemail' ) ;
2014-11-04 12:52:49 +00:00
var loopback = require ( '../../lib/loopback' ) ;
2015-07-01 11:43:25 +00:00
var utils = require ( '../../lib/utils' ) ;
2014-11-04 12:52:49 +00:00
var path = require ( 'path' ) ;
var SALT _WORK _FACTOR = 10 ;
var crypto = require ( 'crypto' ) ;
2016-08-03 23:01:33 +00:00
var MAX _PASSWORD _LENGTH = 72 ;
2014-12-03 17:03:36 +00:00
var bcrypt ;
try {
// Try the native module first
bcrypt = require ( 'bcrypt' ) ;
2014-12-08 22:59:21 +00:00
// Browserify returns an empty object
if ( bcrypt && typeof bcrypt . compare !== 'function' ) {
bcrypt = require ( 'bcryptjs' ) ;
}
2014-12-03 17:03:36 +00:00
} catch ( err ) {
// Fall back to pure JS impl
bcrypt = require ( 'bcryptjs' ) ;
}
2014-11-04 12:52:49 +00:00
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' ) ;
2013-07-01 23:50:03 +00:00
2014-01-27 22:31:38 +00:00
var debug = require ( 'debug' ) ( 'loopback:user' ) ;
2014-04-14 21:49:29 +00:00
2013-07-01 23:50:03 +00:00
/ * *
2014-10-15 07:07:30 +00:00
* Built - in User model .
* Extends LoopBack [ PersistedModel ] ( # persistedmodel - new - persistedmodel ) .
2013-12-20 01:49:47 +00:00
*
* Default ` User ` ACLs .
2014-04-10 03:01:58 +00:00
*
2013-12-20 01:49:47 +00:00
* - DENY EVERYONE ` * `
* - ALLOW EVERYONE ` create `
2014-07-16 16:09:07 +00:00
* - ALLOW OWNER ` deleteById `
2013-12-20 01:49:47 +00:00
* - ALLOW EVERYONE ` login `
* - ALLOW EVERYONE ` logout `
2015-09-12 00:57:55 +00:00
* - ALLOW OWNER ` findById `
2013-12-20 01:49:47 +00:00
* - ALLOW OWNER ` updateAttributes `
*
2015-12-03 16:41:04 +00:00
* @ property { String } username Must be unique .
* @ property { String } password Hidden from remote clients .
* @ property { String } email Must be valid email .
* @ property { Boolean } emailVerified Set when a user ' s email has been verified via ` confirm() ` .
* @ property { String } verificationToken Set when ` verify() ` is called .
2016-11-04 20:47:12 +00:00
* @ property { String } realm The namespace the user belongs to . See [ Partitioning users with realms ] ( http : //loopback.io/doc/en/lb2/Partitioning-users-with-realms.html) for details.
2015-02-04 22:09:01 +00:00
* @ property { Object } settings Extends the ` Model.settings ` object .
* @ property { Boolean } settings . emailVerificationRequired Require the email verification
* process before allowing a login .
* @ property { Number } settings . ttl Default time to live ( in seconds ) for the ` AccessToken ` created by ` User.login() / user.createAccessToken() ` .
* Default is ` 1209600 ` ( 2 weeks )
* @ property { Number } settings . maxTTL The max value a user can request a token to be alive / valid for .
* Default is ` 31556926 ` ( 1 year )
2015-02-23 21:13:52 +00:00
* @ property { Boolean } settings . realmRequired Require a realm when logging in a user .
* @ property { String } settings . realmDelimiter When set a realm is required .
* @ property { Number } settings . resetPasswordTokenTTL Time to live for password reset ` AccessToken ` . Default is ` 900 ` ( 15 minutes ) .
2015-02-04 22:09:01 +00:00
* @ property { Number } settings . saltWorkFactor The ` bcrypt ` salt work factor . Default is ` 10 ` .
2015-11-09 21:21:38 +00:00
* @ property { Boolean } settings . caseSensitiveEmail Enable case sensitive email .
2014-10-01 17:33:36 +00:00
*
2014-10-10 09:53:22 +00:00
* @ class User
* @ inherits { PersistedModel }
2013-07-01 23:50:03 +00:00
* /
2014-10-10 09:53:22 +00:00
module . exports = function ( User ) {
2014-11-04 12:52:49 +00:00
/ * *
* 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
2015-03-02 22:48:08 +00:00
* @ param { Object } [ options ] The options for access token , such as scope , appId
* @ callback { Function } cb The callback function
2014-11-04 12:52:49 +00:00
* @ param { String | Error } err The error string or object
* @ param { AccessToken } token The generated access token object
2016-02-04 20:31:39 +00:00
* @ promise
2014-11-04 12:52:49 +00:00
* /
2015-03-02 22:48:08 +00:00
User . prototype . createAccessToken = function ( ttl , options , cb ) {
if ( cb === undefined && typeof options === 'function' ) {
// createAccessToken(ttl, cb)
cb = options ;
options = undefined ;
}
2015-07-01 11:43:25 +00:00
cb = cb || utils . createPromiseCallback ( ) ;
2015-03-02 22:48:08 +00:00
if ( typeof ttl === 'object' && ! options ) {
// createAccessToken(options, cb)
options = ttl ;
ttl = options . ttl ;
}
options = options || { } ;
2014-11-04 12:52:49 +00:00
var userModel = this . constructor ;
ttl = Math . min ( ttl || userModel . settings . ttl , userModel . settings . maxTTL ) ;
this . accessTokens . create ( {
2016-04-01 09:14:26 +00:00
ttl : ttl ,
2014-11-04 12:52:49 +00:00
} , cb ) ;
2015-07-01 11:43:25 +00:00
return cb . promise ;
2014-11-04 12:52:49 +00:00
} ;
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 ) ;
}
2014-10-23 18:10:39 +00:00
return parts ;
}
2014-11-04 12:52:49 +00:00
/ * *
* 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 ;
2014-10-23 18:10:39 +00:00
}
2014-11-04 12:52:49 +00:00
} else {
if ( credentials . realm ) {
query . realm = credentials . realm ;
2014-10-23 18:10:39 +00:00
}
2014-11-04 12:52:49 +00:00
var parts ;
if ( credentials . email ) {
parts = splitPrincipal ( credentials . email , realmDelimiter ) ;
query . email = parts [ 1 ] ;
if ( parts [ 0 ] ) {
query . realm = parts [ 0 ] ;
2014-07-07 21:09:45 +00:00
}
2014-11-04 12:52:49 +00:00
} else if ( credentials . username ) {
parts = splitPrincipal ( credentials . username , realmDelimiter ) ;
query . username = parts [ 1 ] ;
if ( parts [ 0 ] ) {
query . realm = parts [ 0 ] ;
2013-07-02 23:51:38 +00:00
}
2014-11-04 12:52:49 +00:00
}
}
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
2016-02-04 20:31:39 +00:00
* @ promise
2014-11-04 12:52:49 +00:00
* /
User . login = function ( credentials , include , fn ) {
var self = this ;
if ( typeof include === 'function' ) {
fn = include ;
include = undefined ;
2013-07-02 23:51:38 +00:00
}
2013-07-03 05:37:31 +00:00
2015-07-01 11:43:25 +00:00
fn = fn || utils . createPromiseCallback ( ) ;
2014-11-04 12:52:49 +00:00
include = ( include || '' ) ;
if ( Array . isArray ( include ) ) {
include = include . map ( function ( val ) {
return val . toLowerCase ( ) ;
} ) ;
2013-07-03 05:37:31 +00:00
} else {
2014-11-04 12:52:49 +00:00
include = include . toLowerCase ( ) ;
2013-07-03 05:37:31 +00:00
}
2014-11-04 12:52:49 +00:00
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 ) ;
2013-07-02 23:51:38 +00:00
2014-11-04 12:52:49 +00:00
if ( realmRequired && ! query . realm ) {
2016-08-05 19:49:43 +00:00
var err1 = new Error ( g . f ( '{{realm}} is required' ) ) ;
2014-11-04 12:52:49 +00:00
err1 . statusCode = 400 ;
2014-12-18 20:26:27 +00:00
err1 . code = 'REALM_REQUIRED' ;
2015-07-01 11:43:25 +00:00
fn ( err1 ) ;
return fn . promise ;
2014-11-04 12:52:49 +00:00
}
if ( ! query . email && ! query . username ) {
2016-06-07 14:48:28 +00:00
var err2 = new Error ( g . f ( '{{username}} or {{email}} is required' ) ) ;
2014-11-04 12:52:49 +00:00
err2 . statusCode = 400 ;
2014-12-18 20:26:27 +00:00
err2 . code = 'USERNAME_EMAIL_REQUIRED' ;
2015-07-01 11:43:25 +00:00
fn ( err2 ) ;
return fn . promise ;
2014-11-04 12:52:49 +00:00
}
2013-07-02 23:51:38 +00:00
2016-11-15 21:46:23 +00:00
self . findOne ( { where : query } , function ( err , user ) {
2016-06-07 14:48:28 +00:00
var defaultError = new Error ( g . f ( 'login failed' ) ) ;
2014-11-04 12:52:49 +00:00
defaultError . statusCode = 401 ;
2014-12-18 20:26:27 +00:00
defaultError . code = 'LOGIN_FAILED' ;
2013-07-02 23:51:38 +00:00
2015-03-02 22:48:08 +00:00
function tokenHandler ( 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 ) ;
}
2014-11-04 12:52:49 +00:00
if ( err ) {
debug ( 'An error is reported from User.findOne: %j' , err ) ;
fn ( defaultError ) ;
} else if ( user ) {
user . hasPassword ( credentials . password , function ( err , isMatch ) {
if ( err ) {
debug ( 'An error is reported from User.hasPassword: %j' , err ) ;
fn ( defaultError ) ;
} else if ( isMatch ) {
2014-12-18 18:07:31 +00:00
if ( self . settings . emailVerificationRequired && ! user . emailVerified ) {
// Fail to log in if email verification is not done yet
debug ( 'User email has not been verified' ) ;
2016-06-07 14:48:28 +00:00
err = new Error ( g . f ( 'login failed as the email has not been verified' ) ) ;
2014-12-18 18:07:31 +00:00
err . statusCode = 401 ;
2014-12-18 20:26:27 +00:00
err . code = 'LOGIN_FAILED_EMAIL_NOT_VERIFIED' ;
2015-07-01 11:43:25 +00:00
fn ( err ) ;
2014-12-18 18:07:31 +00:00
} else {
2015-03-02 22:48:08 +00:00
if ( user . createAccessToken . length === 2 ) {
user . createAccessToken ( credentials . ttl , tokenHandler ) ;
} else {
user . createAccessToken ( credentials . ttl , credentials , tokenHandler ) ;
}
2014-12-18 18:07:31 +00:00
}
2014-11-04 12:52:49 +00:00
} 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 ) ;
}
} ) ;
2015-07-01 11:43:25 +00:00
return fn . promise ;
2014-11-04 12:52:49 +00:00
} ;
/ * *
* 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
2016-02-04 20:31:39 +00:00
* @ promise
2014-11-04 12:52:49 +00:00
* /
User . logout = function ( tokenId , fn ) {
2015-07-01 11:43:25 +00:00
fn = fn || utils . createPromiseCallback ( ) ;
2015-07-01 19:13:09 +00:00
2017-01-31 12:44:32 +00:00
var err ;
2015-07-01 19:13:09 +00:00
if ( ! tokenId ) {
2017-01-31 12:44:32 +00:00
err = new Error ( g . f ( '{{accessToken}} is required to logout' ) ) ;
2015-07-01 19:13:09 +00:00
err . status = 401 ;
process . nextTick ( fn , err ) ;
return fn . promise ;
}
this . relations . accessTokens . modelTo . destroyById ( tokenId , function ( err , info ) {
2014-11-04 12:52:49 +00:00
if ( err ) {
2013-07-03 05:37:31 +00:00
fn ( err ) ;
2015-07-01 19:13:09 +00:00
} else if ( 'count' in info && info . count === 0 ) {
2017-01-31 12:44:32 +00:00
err = new Error ( g . f ( 'Could not find {{accessToken}}' ) ) ;
2015-07-01 19:13:09 +00:00
err . status = 401 ;
fn ( err ) ;
2013-07-03 05:37:31 +00:00
} else {
2015-07-01 19:13:09 +00:00
fn ( ) ;
2013-07-03 05:37:31 +00:00
}
} ) ;
2015-07-01 11:43:25 +00:00
return fn . promise ;
2014-11-04 12:52:49 +00:00
} ;
2016-07-07 15:30:57 +00:00
User . observe ( 'before delete' , function ( ctx , next ) {
var AccessToken = ctx . Model . relations . accessTokens . modelTo ;
var pkName = ctx . Model . definition . idName ( ) || 'id' ;
2016-11-15 21:46:23 +00:00
ctx . Model . find ( { where : ctx . where , fields : [ pkName ] } , function ( err , list ) {
2016-07-07 15:30:57 +00:00
if ( err ) return next ( err ) ;
var ids = list . map ( function ( u ) { return u [ pkName ] ; } ) ;
ctx . where = { } ;
2016-11-15 21:46:23 +00:00
ctx . where [ pkName ] = { inq : ids } ;
2016-07-07 15:30:57 +00:00
2016-11-15 21:46:23 +00:00
AccessToken . destroyAll ( { userId : { inq : ids } } , next ) ;
2016-07-07 15:30:57 +00:00
} ) ;
} ) ;
2014-11-04 12:52:49 +00:00
/ * *
* Compare the given ` password ` with the users hashed password .
*
* @ param { String } password The plain text password
2016-01-11 19:28:10 +00:00
* @ callback { Function } callback Callback function
* @ param { Error } err Error object
* @ param { Boolean } isMatch Returns true if the given ` password ` matches record
2016-02-04 20:31:39 +00:00
* @ promise
2014-11-04 12:52:49 +00:00
* /
User . prototype . hasPassword = function ( plain , fn ) {
2015-07-01 11:43:25 +00:00
fn = fn || utils . createPromiseCallback ( ) ;
2014-11-04 12:52:49 +00:00
if ( this . password && plain ) {
bcrypt . compare ( plain , this . password , function ( err , isMatch ) {
if ( err ) return fn ( err ) ;
fn ( null , isMatch ) ;
} ) ;
2013-07-03 05:37:31 +00:00
} else {
2014-11-04 12:52:49 +00:00
fn ( null , false ) ;
}
2015-07-01 11:43:25 +00:00
return fn . promise ;
2014-11-04 12:52:49 +00:00
} ;
/ * *
* Verify a user ' s identity by sending them a confirmation email .
*
* ` ` ` js
* var options = {
2015-03-12 14:56:43 +00:00
* type : 'email' ,
* to : user . email ,
* template : 'verify.ejs' ,
* redirect : '/' ,
* tokenGenerator : function ( user , cb ) { cb ( "random-token" ) ; }
* } ;
2014-11-04 12:52:49 +00:00
*
* 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'.
2016-11-11 13:28:49 +00:00
* @ property { Function } templateFn A function generating the email HTML body
* from ` verify() ` options object and generated attributes like ` options.verifyHref ` .
* It must accept the option object and a callback function with ` (err, html) `
* as parameters
2014-11-04 12:52:49 +00:00
* @ property { String } redirect Page to which user will be redirected after
* they verify their email , for example ` '/' ` for root URI .
2015-03-12 14:56:43 +00:00
* @ property { Function } generateVerificationToken A function to be used to
* generate the verification token . It must accept the user object and a
* callback function . This function should NOT add the token to the user
* object , instead simply execute the callback with the token ! User saving
* and email sending will be handled in the ` verify() ` method .
2016-02-04 20:31:39 +00:00
* @ callback { Function } fn Callback function .
* @ param { Error } err Error object .
* @ param { Object } object Contains email , token , uid .
* @ promise
2014-11-04 12:52:49 +00:00
* /
User . prototype . verify = function ( options , fn ) {
2015-07-01 11:43:25 +00:00
fn = fn || utils . createPromiseCallback ( ) ;
2014-11-04 12:52:49 +00:00
var user = this ;
var userModel = this . constructor ;
2015-04-01 21:50:36 +00:00
var registry = userModel . registry ;
2017-01-05 15:34:15 +00:00
var pkName = userModel . definition . idName ( ) || 'id' ;
2014-11-04 12:52:49 +00:00
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' ) ;
2016-04-01 09:14:26 +00:00
assert ( options . to || this . email ,
'Must include options.to when calling user.verify() ' +
'or the user must have an email property' ) ;
2015-08-26 16:25:09 +00:00
assert ( options . from , 'Must include options.from when calling user.verify()' ) ;
2014-11-04 12:52:49 +00:00
options . redirect = options . redirect || '/' ;
2016-04-01 09:14:26 +00:00
var defaultTemplate = path . join ( _ _dirname , '..' , '..' , 'templates' , 'verify.ejs' ) ;
options . template = path . resolve ( options . template || defaultTemplate ) ;
2014-11-04 12:52:49 +00:00
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' ;
2015-09-18 09:51:17 +00:00
var displayPort = (
( options . protocol === 'http' && options . port == '80' ) ||
( options . protocol === 'https' && options . port == '443' )
) ? '' : ':' + options . port ;
2016-09-06 11:55:54 +00:00
var urlPath = joinUrlPath (
options . restApiRoot ,
userModel . http . path ,
userModel . sharedClass . findMethodByName ( 'confirm' ) . http . path
) ;
2014-11-04 12:52:49 +00:00
options . verifyHref = options . verifyHref ||
options . protocol +
'://' +
options . host +
2015-09-18 09:51:17 +00:00
displayPort +
2016-09-06 11:55:54 +00:00
urlPath +
2014-11-04 12:52:49 +00:00
'?uid=' +
2017-01-05 15:34:15 +00:00
options . user [ pkName ] +
2014-11-04 12:52:49 +00:00
'&redirect=' +
options . redirect ;
2016-11-11 13:28:49 +00:00
options . templateFn = options . templateFn || createVerificationEmailBody ;
2014-11-04 12:52:49 +00:00
// Email model
2016-11-15 21:46:23 +00:00
var Email =
options . mailer || this . constructor . email || registry . getModelByType ( loopback . Email ) ;
2014-11-04 12:52:49 +00:00
2015-03-12 14:56:43 +00:00
// Set a default token generation function if one is not provided
var tokenGenerator = options . generateVerificationToken || User . generateVerificationToken ;
tokenGenerator ( user , function ( err , token ) {
if ( err ) { return fn ( err ) ; }
user . verificationToken = token ;
user . save ( function ( err ) {
if ( err ) {
fn ( err ) ;
} else {
sendEmail ( user ) ;
}
} ) ;
2014-11-04 12:52:49 +00:00
} ) ;
2013-11-20 18:59:29 +00:00
2014-11-04 12:52:49 +00:00
// TODO - support more verification types
function sendEmail ( user ) {
options . verifyHref += '&token=' + user . verificationToken ;
2016-08-28 05:42:21 +00:00
options . text = options . text || g . f ( 'Please verify your email by opening ' +
'this link in a web browser:\n\t%s' , options . verifyHref ) ;
2013-12-20 01:49:47 +00:00
2016-05-08 11:10:56 +00:00
options . text = options . text . replace ( /\{href\}/g , options . verifyHref ) ;
2013-11-20 18:59:29 +00:00
2015-05-06 13:52:07 +00:00
options . to = options . to || user . email ;
2016-08-28 05:42:21 +00:00
options . subject = options . subject || g . f ( 'Thanks for Registering' ) ;
2016-06-07 14:48:28 +00:00
2015-05-06 13:52:07 +00:00
options . headers = options . headers || { } ;
2016-11-11 13:28:49 +00:00
options . templateFn ( options , function ( err , html ) {
2014-11-04 12:52:49 +00:00
if ( err ) {
fn ( err ) ;
} else {
2016-11-11 13:28:49 +00:00
setHtmlContentAndSend ( html ) ;
2014-11-04 12:52:49 +00:00
}
} ) ;
2016-11-11 13:28:49 +00:00
function setHtmlContentAndSend ( html ) {
options . html = html ;
2016-12-06 14:58:27 +00:00
// Remove options.template to prevent rejection by certain
// nodemailer transport plugins.
delete options . template ;
2016-11-11 13:28:49 +00:00
Email . send ( options , function ( err , email ) {
if ( err ) {
fn ( err ) ;
} else {
2017-01-05 15:34:15 +00:00
fn ( null , { email : email , token : user . verificationToken , uid : user [ pkName ] } ) ;
2016-11-11 13:28:49 +00:00
}
} ) ;
}
2014-11-04 12:52:49 +00:00
}
2015-07-01 11:43:25 +00:00
return fn . promise ;
2014-11-04 12:52:49 +00:00
} ;
2016-11-11 13:28:49 +00:00
function createVerificationEmailBody ( options , cb ) {
var template = loopback . template ( options . template ) ;
var body = template ( options ) ;
cb ( null , body ) ;
}
2015-03-12 14:56:43 +00:00
/ * *
* A default verification token generator which accepts the user the token is
* being generated for and a callback function to indicate completion .
* This one uses the crypto library and 64 random bytes ( converted to hex )
* for the token . When used in combination with the user . verify ( ) method this
* function will be called with the ` user ` object as it ' s context ( ` this ` ) .
*
* @ param { object } user The User this token is being generated for .
* @ param { Function } cb The generator must pass back the new token with this function call
* /
User . generateVerificationToken = function ( user , cb ) {
crypto . randomBytes ( 64 , function ( err , buf ) {
cb ( err , buf && buf . toString ( 'hex' ) ) ;
} ) ;
} ;
2014-11-04 12:52:49 +00:00
/ * *
* 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
2016-02-04 20:31:39 +00:00
* @ promise
2014-11-04 12:52:49 +00:00
* /
User . confirm = function ( uid , token , redirect , fn ) {
2015-07-01 11:43:25 +00:00
fn = fn || utils . createPromiseCallback ( ) ;
2014-11-04 12:52:49 +00:00
this . findById ( uid , function ( err , user ) {
2014-10-10 09:53:22 +00:00
if ( err ) {
2014-11-04 12:52:49 +00:00
fn ( err ) ;
} else {
if ( user && user . verificationToken === token ) {
2016-06-16 06:20:33 +00:00
user . verificationToken = null ;
2014-11-04 12:52:49 +00:00
user . emailVerified = true ;
user . save ( function ( err ) {
if ( err ) {
fn ( err ) ;
} else {
fn ( ) ;
}
} ) ;
} else {
if ( user ) {
2016-06-07 14:48:28 +00:00
err = new Error ( g . f ( 'Invalid token: %s' , token ) ) ;
2014-11-04 12:52:49 +00:00
err . statusCode = 400 ;
2014-12-18 20:26:27 +00:00
err . code = 'INVALID_TOKEN' ;
2013-11-20 18:59:29 +00:00
} else {
2016-06-07 14:48:28 +00:00
err = new Error ( g . f ( 'User not found: %s' , uid ) ) ;
2014-11-04 12:52:49 +00:00
err . statusCode = 404 ;
2014-12-18 20:26:27 +00:00
err . code = 'USER_NOT_FOUND' ;
2013-11-20 18:59:29 +00:00
}
2014-11-04 12:52:49 +00:00
fn ( err ) ;
}
2013-11-20 18:59:29 +00:00
}
} ) ;
2015-07-01 11:43:25 +00:00
return fn . promise ;
2014-11-04 12:52:49 +00:00
} ;
/ * *
2016-12-03 20:25:13 +00:00
* Create a short lived access token for temporary login . Allows users
2014-11-04 12:52:49 +00:00
* to change passwords if forgotten .
*
* @ options { Object } options
* @ prop { String } email The user ' s email address
2016-12-03 20:25:13 +00:00
* @ property { String } realm The user ' s realm ( optional )
2014-11-04 12:52:49 +00:00
* @ callback { Function } callback
* @ param { Error } err
2016-02-04 20:31:39 +00:00
* @ promise
2014-11-04 12:52:49 +00:00
* /
User . resetPassword = function ( options , cb ) {
2015-07-01 11:43:25 +00:00
cb = cb || utils . createPromiseCallback ( ) ;
2014-11-04 12:52:49 +00:00
var UserModel = this ;
var ttl = UserModel . settings . resetPasswordTokenTTL || DEFAULT _RESET _PW _TTL ;
options = options || { } ;
2015-10-30 21:59:31 +00:00
if ( typeof options . email !== 'string' ) {
2016-06-07 14:48:28 +00:00
var err = new Error ( g . f ( 'Email is required' ) ) ;
2014-11-04 12:52:49 +00:00
err . statusCode = 400 ;
2014-12-18 20:26:27 +00:00
err . code = 'EMAIL_REQUIRED' ;
2014-11-04 12:52:49 +00:00
cb ( err ) ;
2015-10-30 21:59:31 +00:00
return cb . promise ;
2014-11-04 12:52:49 +00:00
}
2015-10-30 21:59:31 +00:00
2016-08-03 23:01:33 +00:00
try {
if ( options . password ) {
UserModel . validatePassword ( options . password ) ;
}
} catch ( err ) {
return cb ( err ) ;
}
2016-12-03 20:25:13 +00:00
var where = {
email : options . email ,
} ;
if ( options . realm ) {
where . realm = options . realm ;
}
UserModel . findOne ( { where : where } , function ( err , user ) {
2015-10-30 21:59:31 +00:00
if ( err ) {
return cb ( err ) ;
}
if ( ! user ) {
2016-06-07 14:48:28 +00:00
err = new Error ( g . f ( 'Email not found' ) ) ;
2015-10-30 21:59:31 +00:00
err . statusCode = 404 ;
err . code = 'EMAIL_NOT_FOUND' ;
return cb ( err ) ;
}
// create a short lived access token for temp login to change password
// TODO(ritch) - eventually this should only allow password change
2016-08-24 20:30:58 +00:00
if ( UserModel . settings . emailVerificationRequired && ! user . emailVerified ) {
err = new Error ( g . f ( 'Email has not been verified' ) ) ;
err . statusCode = 401 ;
err . code = 'RESET_FAILED_EMAIL_NOT_VERIFIED' ;
return cb ( err ) ;
}
2016-12-11 03:12:54 +00:00
user . createAccessToken ( ttl , function ( err , accessToken ) {
2015-10-30 21:59:31 +00:00
if ( err ) {
return cb ( err ) ;
}
cb ( ) ;
UserModel . emit ( 'resetPasswordRequest' , {
email : options . email ,
accessToken : accessToken ,
2016-04-01 09:14:26 +00:00
user : user ,
2016-11-30 11:44:30 +00:00
options : options ,
2015-10-30 21:59:31 +00:00
} ) ;
} ) ;
} ) ;
2015-07-01 11:43:25 +00:00
return cb . promise ;
2014-11-04 12:52:49 +00:00
} ;
2014-12-20 23:57:38 +00:00
/ * !
* Hash the plain password
* /
User . hashPassword = function ( plain ) {
this . validatePassword ( plain ) ;
var salt = bcrypt . genSaltSync ( this . settings . saltWorkFactor || SALT _WORK _FACTOR ) ;
return bcrypt . hashSync ( plain , salt ) ;
} ;
User . validatePassword = function ( plain ) {
2016-08-03 23:01:33 +00:00
var err ;
if ( plain && typeof plain === 'string' && plain . length <= MAX _PASSWORD _LENGTH ) {
2014-12-20 23:57:38 +00:00
return true ;
}
2016-08-03 23:01:33 +00:00
if ( plain . length > MAX _PASSWORD _LENGTH ) {
2017-01-06 11:12:35 +00:00
err = new Error ( g . f ( 'Password too long: %s' , plain ) ) ;
2016-08-03 23:01:33 +00:00
err . code = 'PASSWORD_TOO_LONG' ;
} else {
err = new Error ( g . f ( 'Invalid password: %s' , plain ) ) ;
err . code = 'INVALID_PASSWORD' ;
}
2014-12-20 23:57:38 +00:00
err . statusCode = 422 ;
throw err ;
} ;
2016-12-09 14:36:54 +00:00
2017-01-13 10:03:06 +00:00
User . _invalidateAccessTokensOfUsers = function ( userIds , options , cb ) {
if ( typeof options === 'function' && cb === undefined ) {
cb = options ;
options = { } ;
}
2016-12-09 14:36:54 +00:00
if ( ! Array . isArray ( userIds ) || ! userIds . length )
return process . nextTick ( cb ) ;
var accessTokenRelation = this . relations . accessTokens ;
if ( ! accessTokenRelation )
return process . nextTick ( cb ) ;
var AccessToken = accessTokenRelation . modelTo ;
2017-01-13 10:03:06 +00:00
var query = { userId : { inq : userIds } } ;
var tokenPK = AccessToken . definition . idName ( ) || 'id' ;
if ( options . accessToken && tokenPK in options . accessToken ) {
query [ tokenPK ] = { neq : options . accessToken [ tokenPK ] } ;
}
AccessToken . deleteAll ( query , options , cb ) ;
2016-12-09 14:36:54 +00:00
} ;
2014-11-04 12:52:49 +00:00
/ * !
* 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 ;
2014-12-28 08:02:37 +00:00
this . settings . ttl = this . settings . ttl || DEFAULT _TTL ;
2014-11-04 12:52:49 +00:00
2015-11-09 21:21:38 +00:00
UserModel . setter . email = function ( value ) {
if ( ! UserModel . settings . caseSensitiveEmail ) {
this . $email = value . toLowerCase ( ) ;
} else {
this . $email = value ;
}
} ;
2014-11-04 12:52:49 +00:00
UserModel . setter . password = function ( plain ) {
2015-06-16 21:44:37 +00:00
if ( typeof plain !== 'string' ) {
return ;
}
2015-02-25 00:36:51 +00:00
if ( plain . indexOf ( '$2a$' ) === 0 && plain . length === 60 ) {
// The password is already hashed. It can be the case
// when the instance is loaded from DB
this . $password = plain ;
} else {
this . $password = this . constructor . hashPassword ( plain ) ;
}
2014-11-04 12:52:49 +00:00
} ;
// 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 ;
2013-07-03 05:37:31 +00:00
}
2014-11-04 12:52:49 +00:00
next ( ) ;
2013-07-03 05:37:31 +00:00
} ) ;
2014-04-10 03:01:58 +00:00
2014-12-22 16:23:27 +00:00
UserModel . remoteMethod (
'login' ,
2014-11-04 12:52:49 +00:00
{
2016-08-15 09:06:05 +00:00
description : 'Login a user with username/email and password.' ,
2014-11-04 12:52:49 +00:00
accepts : [
2016-11-15 21:46:23 +00:00
{ arg : 'credentials' , type : 'object' , required : true , http : { source : 'body' } } ,
{ arg : 'include' , type : [ 'string' ] , http : { source : 'query' } ,
2016-08-15 09:06:05 +00:00
description : 'Related objects to include in the response. ' +
2016-11-15 21:46:23 +00:00
'See the description of return value for more details.' } ,
2014-11-04 12:52:49 +00:00
] ,
returns : {
arg : 'accessToken' , type : 'object' , root : true ,
description :
2016-06-07 14:48:28 +00:00
g . f ( 'The response body contains properties of the {{AccessToken}} created on login.\n' +
2014-11-04 12:52:49 +00:00
'Depending on the value of `include` parameter, the body may contain ' +
'additional properties:\n\n' +
2016-06-07 14:48:28 +00:00
' - `user` - `U+007BUserU+007D` - Data of the currently logged in user. ' +
'{{(`include=user`)}}\n\n' ) ,
2014-11-04 12:52:49 +00:00
} ,
2016-11-15 21:46:23 +00:00
http : { verb : 'post' } ,
2014-11-04 12:52:49 +00:00
}
) ;
2014-12-22 16:23:27 +00:00
UserModel . remoteMethod (
'logout' ,
2014-11-04 12:52:49 +00:00
{
2016-08-15 09:06:05 +00:00
description : 'Logout a user with access token.' ,
2014-11-04 12:52:49 +00:00
accepts : [
2015-07-01 19:13:09 +00:00
{ arg : 'access_token' , type : 'string' , http : function ( ctx ) {
2014-11-04 12:52:49 +00:00
var req = ctx && ctx . req ;
var accessToken = req && req . accessToken ;
2015-07-01 19:13:09 +00:00
var tokenID = accessToken ? accessToken . id : undefined ;
2014-11-04 12:52:49 +00:00
return tokenID ;
2016-08-15 09:06:05 +00:00
} , description : 'Do not supply this argument, it is automatically extracted ' +
'from request headers.' ,
2016-04-01 09:14:26 +00:00
} ,
2014-11-04 12:52:49 +00:00
] ,
2016-11-15 21:46:23 +00:00
http : { verb : 'all' } ,
2014-11-04 12:52:49 +00:00
}
) ;
2014-12-22 16:23:27 +00:00
UserModel . remoteMethod (
'confirm' ,
2014-11-04 12:52:49 +00:00
{
2016-08-15 09:06:05 +00:00
description : 'Confirm a user registration with email verification token.' ,
2014-11-04 12:52:49 +00:00
accepts : [
2016-11-15 21:46:23 +00:00
{ arg : 'uid' , type : 'string' , required : true } ,
{ arg : 'token' , type : 'string' , required : true } ,
{ arg : 'redirect' , type : 'string' } ,
2014-11-04 12:52:49 +00:00
] ,
2016-11-15 21:46:23 +00:00
http : { verb : 'get' , path : '/confirm' } ,
2014-11-04 12:52:49 +00:00
}
) ;
2014-12-22 16:23:27 +00:00
UserModel . remoteMethod (
'resetPassword' ,
2014-11-04 12:52:49 +00:00
{
2016-08-15 09:06:05 +00:00
description : 'Reset password for a user with email.' ,
2014-11-04 12:52:49 +00:00
accepts : [
2016-11-15 21:46:23 +00:00
{ arg : 'options' , type : 'object' , required : true , http : { source : 'body' } } ,
2014-11-04 12:52:49 +00:00
] ,
2016-11-15 21:46:23 +00:00
http : { verb : 'post' , path : '/reset' } ,
2014-11-04 12:52:49 +00:00
}
) ;
2014-10-10 09:53:22 +00:00
2015-04-03 08:04:05 +00:00
UserModel . afterRemote ( 'confirm' , function ( ctx , inst , next ) {
if ( ctx . args . redirect !== undefined ) {
if ( ! ctx . res ) {
2016-06-07 14:48:28 +00:00
return next ( new Error ( g . f ( 'The transport does not support HTTP redirects.' ) ) ) ;
2014-11-04 12:52:49 +00:00
}
2015-04-03 08:04:05 +00:00
ctx . res . location ( ctx . args . redirect ) ;
ctx . res . status ( 302 ) ;
}
next ( ) ;
2014-11-04 12:52:49 +00:00
} ) ;
2014-04-10 03:01:58 +00:00
2014-11-04 12:52:49 +00:00
// default models
assert ( loopback . Email , 'Email model must be defined before User model' ) ;
UserModel . email = loopback . Email ;
2014-04-10 03:01:58 +00:00
2014-11-04 12:52:49 +00:00
assert ( loopback . AccessToken , 'AccessToken model must be defined before User model' ) ;
UserModel . accessToken = loopback . AccessToken ;
2014-10-23 18:10:39 +00:00
2016-09-06 10:50:03 +00:00
UserModel . validate ( 'email' , emailValidator , {
message : g . f ( 'Must provide a valid email' ) ,
} ) ;
2014-04-10 03:01:58 +00:00
2016-08-18 21:52:04 +00:00
// Realm users validation
if ( UserModel . settings . realmRequired && UserModel . settings . realmDelimiter ) {
UserModel . validatesUniquenessOf ( 'email' , {
message : 'Email already exists' ,
scopedTo : [ 'realm' ] ,
} ) ;
UserModel . validatesUniquenessOf ( 'username' , {
message : 'User already exists' ,
scopedTo : [ 'realm' ] ,
} ) ;
} else {
// Regular(Non-realm) users validation
2016-11-15 21:46:23 +00:00
UserModel . validatesUniquenessOf ( 'email' , { message : 'Email already exists' } ) ;
UserModel . validatesUniquenessOf ( 'username' , { message : 'User already exists' } ) ;
2014-11-04 12:52:49 +00:00
}
2013-07-02 23:51:38 +00:00
2014-11-04 12:52:49 +00:00
return UserModel ;
} ;
/ * !
* Setup the base user .
* /
2013-07-03 05:37:31 +00:00
2014-11-04 12:52:49 +00:00
User . setup ( ) ;
2016-12-09 12:16:42 +00:00
// --- OPERATION HOOKS ---
//
// Important: Operation hooks are inherited by subclassed models,
// therefore they must be registered outside of setup() function
2016-12-09 13:28:24 +00:00
// Access token to normalize email credentials
User . observe ( 'access' , function normalizeEmailCase ( ctx , next ) {
if ( ! ctx . Model . settings . caseSensitiveEmail && ctx . query . where &&
ctx . query . where . email && typeof ( ctx . query . where . email ) === 'string' ) {
ctx . query . where . email = ctx . query . where . email . toLowerCase ( ) ;
}
next ( ) ;
} ) ;
2017-01-30 10:30:05 +00:00
User . observe ( 'before save' , function prepareForTokenInvalidation ( ctx , next ) {
2016-12-09 12:16:42 +00:00
if ( ctx . isNewInstance ) return next ( ) ;
if ( ! ctx . where && ! ctx . instance ) return next ( ) ;
2017-01-30 10:30:05 +00:00
2017-01-05 15:34:15 +00:00
var pkName = ctx . Model . definition . idName ( ) || 'id' ;
2017-01-31 12:44:32 +00:00
var where = ctx . where ;
if ( ! where ) {
where = { } ;
2017-01-05 15:34:15 +00:00
where [ pkName ] = ctx . instance [ pkName ] ;
}
2016-12-09 14:36:54 +00:00
2016-12-09 12:16:42 +00:00
ctx . Model . find ( { where : where } , function ( err , userInstances ) {
if ( err ) return next ( err ) ;
ctx . hookState . originalUserData = userInstances . map ( function ( u ) {
2017-01-05 15:34:15 +00:00
var user = { } ;
user [ pkName ] = u [ pkName ] ;
2017-01-30 10:30:05 +00:00
user . email = u . email ;
user . password = u . password ;
2017-01-05 15:34:15 +00:00
return user ;
2016-12-09 12:16:42 +00:00
} ) ;
2017-01-31 12:44:32 +00:00
var emailChanged ;
2016-12-09 12:16:42 +00:00
if ( ctx . instance ) {
2017-01-31 12:44:32 +00:00
emailChanged = ctx . instance . email !== ctx . hookState . originalUserData [ 0 ] . email ;
2016-12-09 12:16:42 +00:00
if ( emailChanged && ctx . Model . settings . emailVerificationRequired ) {
ctx . instance . emailVerified = false ;
}
2016-12-23 05:47:08 +00:00
} else if ( ctx . data . email ) {
2017-01-31 12:44:32 +00:00
emailChanged = ctx . hookState . originalUserData . some ( function ( data ) {
2016-12-23 05:47:08 +00:00
return data . email != ctx . data . email ;
2016-12-09 12:16:42 +00:00
} ) ;
if ( emailChanged && ctx . Model . settings . emailVerificationRequired ) {
ctx . data . emailVerified = false ;
}
}
2016-12-09 14:36:54 +00:00
2016-12-09 12:16:42 +00:00
next ( ) ;
} ) ;
} ) ;
2017-01-30 10:30:05 +00:00
User . observe ( 'after save' , function invalidateOtherTokens ( ctx , next ) {
2016-12-09 12:16:42 +00:00
if ( ! ctx . instance && ! ctx . data ) return next ( ) ;
if ( ! ctx . hookState . originalUserData ) return next ( ) ;
2016-12-09 14:36:54 +00:00
2017-01-05 15:34:15 +00:00
var pkName = ctx . Model . definition . idName ( ) || 'id' ;
2016-12-09 14:36:54 +00:00
var newEmail = ( ctx . instance || ctx . data ) . email ;
2017-01-30 10:30:05 +00:00
var newPassword = ( ctx . instance || ctx . data ) . password ;
2016-12-09 14:36:54 +00:00
2017-01-30 10:30:05 +00:00
if ( ! newEmail && ! newPassword ) return next ( ) ;
2016-12-09 14:36:54 +00:00
var userIdsToExpire = ctx . hookState . originalUserData . filter ( function ( u ) {
2017-01-30 10:30:05 +00:00
return ( newEmail && u . email !== newEmail ) ||
( newPassword && u . password !== newPassword ) ;
2016-12-09 12:16:42 +00:00
} ) . map ( function ( u ) {
2017-01-05 15:34:15 +00:00
return u [ pkName ] ;
2016-12-09 12:16:42 +00:00
} ) ;
2017-01-13 10:03:06 +00:00
ctx . Model . _invalidateAccessTokensOfUsers ( userIdsToExpire , ctx . options , next ) ;
2016-12-09 12:16:42 +00:00
} ) ;
2014-10-10 09:53:22 +00:00
} ;
2016-09-06 10:50:03 +00:00
function emailValidator ( err , done ) {
var value = this . email ;
if ( value == null )
return ;
if ( typeof value !== 'string' )
return err ( 'string' ) ;
if ( value === '' ) return ;
if ( ! isEmail ( value ) )
return err ( 'email' ) ;
}
2016-09-06 11:55:54 +00:00
function joinUrlPath ( args ) {
var result = arguments [ 0 ] ;
for ( var ix = 1 ; ix < arguments . length ; ix ++ ) {
var next = arguments [ ix ] ;
result += result [ result . length - 1 ] === '/' && next [ 0 ] === '/' ?
next . slice ( 1 ) : next ;
}
return result ;
}