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' ) ;
2017-02-06 11:39:21 +00:00
var qs = require ( 'querystring' ) ;
2014-11-04 12:52:49 +00:00
var SALT _WORK _FACTOR = 10 ;
var crypto = require ( 'crypto' ) ;
2017-08-10 22:34:09 +00:00
// bcrypt's max length is 72 bytes;
// See https://github.com/kelektiv/node.bcrypt.js/blob/45f498ef6dc6e8234e58e07834ce06a50ff16352/src/node_blf.h#L59
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
*
2017-04-10 12:07:41 +00:00
* Supported flavours :
*
* ` ` ` js
* createAccessToken ( ttl , cb )
* createAccessToken ( ttl , options , cb ) ;
* createAccessToken ( options , cb ) ;
* // recent addition:
* createAccessToken ( data , options , cb ) ;
* ` ` `
*
* @ options { Number | Object } [ ttl | data ] Either the requested ttl ,
* or an object with token properties to set ( see below ) .
* @ property { Number } [ ttl ] The requested ttl
* @ property { String [ ] } [ scopes ] The access scopes granted to the token .
* @ param { Object } [ options ] Additional options including remoting context
2015-03-02 22:48:08 +00:00
* @ 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
2017-04-10 12:07:41 +00:00
*
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 ( ) ;
2017-04-10 12:07:41 +00:00
let tokenData ;
if ( typeof ttl !== 'object' ) {
// createAccessToken(ttl[, options], cb)
tokenData = { ttl } ;
} else if ( options ) {
// createAccessToken(data, options, cb)
tokenData = ttl ;
} else {
// createAccessToken(options, cb);
tokenData = { } ;
2015-03-02 22:48:08 +00:00
}
2017-04-10 12:07:41 +00:00
var userSettings = this . constructor . settings ;
tokenData . ttl = Math . min ( tokenData . ttl || userSettings . ttl , userSettings . maxTTL ) ;
this . accessTokens . create ( tokenData , options , 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 ) {
2017-03-28 09:10:51 +00:00
* console . log ( token . id ) ;
* } ) ;
2014-11-04 12:52:49 +00:00
* ` ` `
*
2017-03-28 09:10:51 +00:00
* If the ` emailVerificationRequired ` flag is set for the inherited user model
* and the email has not yet been verified then the method will return a 401
* error that will contain the user ' s id . This id can be used to call the
* ` api/verify ` remote method to generate a new email verification token and
* send back the related email to the user .
*
2014-11-04 12:52:49 +00:00
* @ 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' ;
2017-03-28 09:10:51 +00:00
err . details = {
userId : user . id ,
} ;
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
} ;
2017-03-22 14:33:32 +00:00
/ * *
* Change this user ' s password .
*
* @ param { * } userId Id of the user changing the password
* @ param { string } oldPassword Current password , required in order
* to strongly verify the identity of the requesting user
* @ param { string } newPassword The new password to use .
* @ param { object } [ options ]
* @ callback { Function } callback
* @ param { Error } err Error object
* @ promise
* /
User . changePassword = function ( userId , oldPassword , newPassword , options , cb ) {
if ( cb === undefined && typeof options === 'function' ) {
cb = options ;
options = undefined ;
}
cb = cb || utils . createPromiseCallback ( ) ;
// Make sure to use the constructor of the (sub)class
// where the method is invoked from (`this` instead of `User`)
this . findById ( userId , options , ( err , inst ) => {
if ( err ) return cb ( err ) ;
if ( ! inst ) {
const err = new Error ( ` User ${ userId } not found ` ) ;
Object . assign ( err , {
code : 'USER_NOT_FOUND' ,
statusCode : 401 ,
} ) ;
return cb ( err ) ;
}
inst . changePassword ( oldPassword , newPassword , options , cb ) ;
} ) ;
return cb . promise ;
} ;
/ * *
* Change this user ' s password ( prototype / instance version ) .
*
* @ param { string } oldPassword Current password , required in order
* to strongly verify the identity of the requesting user
* @ param { string } newPassword The new password to use .
* @ param { object } [ options ]
* @ callback { Function } callback
* @ param { Error } err Error object
* @ promise
* /
User . prototype . changePassword = function ( oldPassword , newPassword , options , cb ) {
if ( cb === undefined && typeof options === 'function' ) {
cb = options ;
options = undefined ;
}
cb = cb || utils . createPromiseCallback ( ) ;
this . hasPassword ( oldPassword , ( err , isMatch ) => {
if ( err ) return cb ( err ) ;
if ( ! isMatch ) {
const err = new Error ( 'Invalid current password' ) ;
Object . assign ( err , {
code : 'INVALID_PASSWORD' ,
statusCode : 400 ,
} ) ;
return cb ( err ) ;
}
2017-04-10 10:20:40 +00:00
this . setPassword ( newPassword , options , cb ) ;
} ) ;
return cb . promise ;
} ;
/ * *
* Set this user ' s password after a password - reset request was made .
*
* @ param { * } userId Id of the user changing the password
* @ param { string } newPassword The new password to use .
* @ param { Object } [ options ] Additional options including remoting context
* @ callback { Function } callback
* @ param { Error } err Error object
* @ promise
* /
User . setPassword = function ( userId , newPassword , options , cb ) {
assert ( userId != null && userId !== '' , 'userId is a required argument' ) ;
assert ( ! ! newPassword , 'newPassword is a required argument' ) ;
if ( cb === undefined && typeof options === 'function' ) {
cb = options ;
options = undefined ;
}
cb = cb || utils . createPromiseCallback ( ) ;
// Make sure to use the constructor of the (sub)class
// where the method is invoked from (`this` instead of `User`)
this . findById ( userId , options , ( err , inst ) => {
if ( err ) return cb ( err ) ;
if ( ! inst ) {
const err = new Error ( ` User ${ userId } not found ` ) ;
Object . assign ( err , {
code : 'USER_NOT_FOUND' ,
statusCode : 401 ,
} ) ;
2017-03-27 09:26:48 +00:00
return cb ( err ) ;
}
2017-04-10 10:20:40 +00:00
inst . setPassword ( newPassword , options , cb ) ;
2017-03-22 14:33:32 +00:00
} ) ;
2017-04-10 10:20:40 +00:00
return cb . promise ;
} ;
/ * *
* Set this user ' s password . The callers of this method
* must ensure the client making the request is authorized
* to change the password , typically by providing the correct
* current password or a password - reset token .
*
* @ param { string } newPassword The new password to use .
* @ param { Object } [ options ] Additional options including remoting context
* @ callback { Function } callback
* @ param { Error } err Error object
* @ promise
* /
User . prototype . setPassword = function ( newPassword , options , cb ) {
assert ( ! ! newPassword , 'newPassword is a required argument' ) ;
if ( cb === undefined && typeof options === 'function' ) {
cb = options ;
options = undefined ;
}
cb = cb || utils . createPromiseCallback ( ) ;
try {
this . constructor . validatePassword ( newPassword ) ;
} catch ( err ) {
cb ( err ) ;
return cb . promise ;
}
2017-04-10 12:07:41 +00:00
// We need to modify options passed to patchAttributes, but we don't want
// to modify the original options object passed to us by setPassword caller
options = Object . assign ( { } , options ) ;
// patchAttributes() does not allow callers to modify the password property
// unless "options.setPassword" is set.
options . setPassword = true ;
2017-04-10 10:20:40 +00:00
const delta = { password : newPassword } ;
this . patchAttributes ( delta , options , ( err , updated ) => cb ( err ) ) ;
2017-03-22 14:33:32 +00:00
return cb . promise ;
} ;
2014-11-04 12:52:49 +00:00
/ * *
2017-03-28 09:10:51 +00:00
* Returns default verification options to use when calling User . prototype . verify ( )
* from remote method / user / : id / verify .
*
* NOTE : the User . getVerifyOptions ( ) method can also be used to ease the
* building of identity verification options .
*
* ` ` ` js
* var verifyOptions = MyUser . getVerifyOptions ( ) ;
* user . verify ( verifyOptions ) ;
* ` ` `
*
* This is the full list of possible params , with example values
*
* ` ` ` js
* {
* type : 'email' ,
* mailer : {
* send ( verifyOptions , options , cb ) {
* // send the email
* cb ( err , result ) ;
* }
* } ,
* to : 'test@email.com' ,
* from : 'noreply@email.com'
* subject : 'verification email subject' ,
* text : 'Please verify your email by opening this link in a web browser' ,
* headers : { 'Mime-Version' : '1.0' } ,
* template : 'path/to/template.ejs' ,
* templateFn : function ( verifyOptions , options , cb ) {
* cb ( null , 'some body template' ) ;
* }
* redirect : '/' ,
* verifyHref : 'http://localhost:3000/api/user/confirm' ,
* host : 'localhost'
* protocol : 'http'
* port : 3000 ,
* restApiRoot = '/api' ,
* generateVerificationToken : function ( user , options , cb ) {
* cb ( null , 'random-token' ) ;
* }
* }
* ` ` `
*
* NOTE : param ` to ` internally defaults to user ' s email but can be overriden for
* test purposes or advanced customization .
*
* Static default params can be modified in your custom user model json definition
* using ` settings.verifyOptions ` . Any default param can be programmatically modified
* like follows :
*
* ` ` ` js
* customUserModel . getVerifyOptions = function ( ) {
* const base = MyUser . base . getVerifyOptions ( ) ;
* return Object . assign ( { } , base , {
* // custom values
* } ) ;
* }
* ` ` `
*
* Usually you should only require to modify a subset of these params
* See ` User.verify() ` and ` User.prototype.verify() ` doc for params reference
* and their default values .
* /
User . getVerifyOptions = function ( ) {
2017-10-09 11:30:28 +00:00
const defaultOptions = {
2017-03-28 09:10:51 +00:00
type : 'email' ,
from : 'noreply@example.com' ,
} ;
2017-10-09 11:30:28 +00:00
return Object . assign ( { } , this . settings . verifyOptions || defaultOptions ) ;
2017-03-28 09:10:51 +00:00
} ;
/ * *
* Verify a user ' s identity by sending them a confirmation message .
* NOTE : Currently only email verification is supported
2014-11-04 12:52:49 +00:00
*
* ` ` ` js
2017-03-28 09:10:51 +00:00
* var verifyOptions = {
* type : 'email' ,
* from : 'noreply@example.com'
* template : 'verify.ejs' ,
* redirect : '/' ,
* generateVerificationToken : function ( user , options , cb ) {
* cb ( 'random-token' ) ;
* }
* } ;
*
* user . verify ( verifyOptions ) ;
* ` ` `
*
* NOTE : the User . getVerifyOptions ( ) method can also be used to ease the
* building of identity verification options .
2014-11-04 12:52:49 +00:00
*
2017-03-28 09:10:51 +00:00
* ` ` ` js
* var verifyOptions = MyUser . getVerifyOptions ( ) ;
* user . verify ( verifyOptions ) ;
2014-11-04 12:52:49 +00:00
* ` ` `
*
2017-04-07 18:31:11 +00:00
* @ options { Object } verifyOptions
2017-03-28 09:10:51 +00:00
* @ property { String } type Must be ` 'email' ` in the current implementation .
* @ property { Function } mailer A mailer function with a static ` .send() method.
* The ` .send() ` method must accept the verifyOptions object , the method ' s
* remoting context options object and a callback function with ` (err, email) `
* as parameters .
* Defaults to provided ` userModel.email ` function , or ultimately to LoopBack ' s
* own mailer function .
2014-11-04 12:52:49 +00:00
* @ property { String } to Email address to which verification email is sent .
2017-03-28 09:10:51 +00:00
* Defaults to user ' s email . Can also be overriden to a static value for test
* purposes .
* @ property { String } from Sender email address
* For example ` 'noreply@example.com' ` .
2014-11-04 12:52:49 +00:00
* @ property { String } subject Subject line text .
2017-03-28 09:10:51 +00:00
* Defaults to ` 'Thanks for Registering' ` or a local equivalent .
2014-11-04 12:52:49 +00:00
* @ property { String } text Text of email .
2017-03-28 09:10:51 +00:00
* Defaults to ` 'Please verify your email by opening this link in a web browser: `
* followed by the verify link .
* @ property { Object } headers Email headers . None provided by default .
* @ property { String } template Relative path of template that displays verification
* page . Defaults to ` '../../templates/verify.ejs' ` .
2016-11-11 13:28:49 +00:00
* @ property { Function } templateFn A function generating the email HTML body
2017-03-28 09:10:51 +00:00
* from ` verify() ` options object and generated attributes like ` options.verifyHref ` .
* It must accept the verifyOptions object , the method ' s remoting context options
* object and a callback function with ` (err, html) ` as parameters .
* A default templateFn function is provided , see ` createVerificationEmailBody() `
* for implementation details .
2014-11-04 12:52:49 +00:00
* @ property { String } redirect Page to which user will be redirected after
2017-03-28 09:10:51 +00:00
* they verify their email . Defaults to ` '/' ` .
* @ property { String } verifyHref The link to include in the user ' s verify message .
* Defaults to an url analog to :
* ` http://host:port/restApiRoot/userRestPath/confirm?uid=userId&redirect=/ ` `
* @ property { String } host The API host . Defaults to app ' s host or ` localhost ` .
* @ property { String } protocol The API protocol . Defaults to ` 'http' ` .
* @ property { Number } port The API port . Defaults to app ' s port or ` 3000 ` .
* @ property { String } restApiRoot The API root path . Defaults to app ' s restApiRoot
* or ` '/api' `
2015-03-12 14:56:43 +00:00
* @ property { Function } generateVerificationToken A function to be used to
2017-03-28 09:10:51 +00:00
* generate the verification token .
* It must accept the verifyOptions object , the method ' s remoting context options
* object and a callback function with ` (err, hexStringBuffer) ` as parameters .
* 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 .
* A default token generation function is provided , see ` generateVerificationToken() `
* for implementation details .
2017-04-07 18:31:11 +00:00
* @ callback { Function } cb Callback function .
2017-03-28 09:10:51 +00:00
* @ param { Object } options remote context options .
2016-02-04 20:31:39 +00:00
* @ param { Error } err Error object .
* @ param { Object } object Contains email , token , uid .
* @ promise
2014-11-04 12:52:49 +00:00
* /
2017-04-07 18:31:11 +00:00
User . prototype . verify = function ( verifyOptions , options , cb ) {
if ( cb === undefined && typeof options === 'function' ) {
cb = options ;
options = undefined ;
}
cb = cb || utils . createPromiseCallback ( ) ;
2015-07-01 11:43:25 +00:00
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-10-09 11:30:28 +00:00
verifyOptions = Object . assign ( { } , verifyOptions ) ;
2017-04-07 18:31:11 +00:00
// final assertion is performed once all options are assigned
assert ( typeof verifyOptions === 'object' ,
'verifyOptions object param required when calling user.verify()' ) ;
2017-10-09 11:30:28 +00:00
// Shallow-clone the options object so that we don't override
// the global default options object
verifyOptions = Object . assign ( { } , verifyOptions ) ;
2017-04-07 18:31:11 +00:00
// Set a default template generation function if none provided
verifyOptions . templateFn = verifyOptions . templateFn || createVerificationEmailBody ;
2017-03-28 09:10:51 +00:00
2017-04-07 18:31:11 +00:00
// Set a default token generation function if none provided
verifyOptions . generateVerificationToken = verifyOptions . generateVerificationToken ||
User . generateVerificationToken ;
2017-03-28 09:10:51 +00:00
2017-04-07 18:31:11 +00:00
// Set a default mailer function if none provided
verifyOptions . mailer = verifyOptions . mailer || userModel . email ||
registry . getModelByType ( loopback . Email ) ;
var pkName = userModel . definition . idName ( ) || 'id' ;
verifyOptions . redirect = verifyOptions . redirect || '/' ;
2016-04-01 09:14:26 +00:00
var defaultTemplate = path . join ( _ _dirname , '..' , '..' , 'templates' , 'verify.ejs' ) ;
2017-04-07 18:31:11 +00:00
verifyOptions . template = path . resolve ( verifyOptions . template || defaultTemplate ) ;
verifyOptions . user = user ;
verifyOptions . protocol = verifyOptions . protocol || 'http' ;
2014-11-04 12:52:49 +00:00
var app = userModel . app ;
2017-04-07 18:31:11 +00:00
verifyOptions . host = verifyOptions . host || ( app && app . get ( 'host' ) ) || 'localhost' ;
verifyOptions . port = verifyOptions . port || ( app && app . get ( 'port' ) ) || 3000 ;
verifyOptions . restApiRoot = verifyOptions . restApiRoot || ( app && app . get ( 'restApiRoot' ) ) || '/api' ;
2015-09-18 09:51:17 +00:00
var displayPort = (
2017-04-07 18:31:11 +00:00
( verifyOptions . protocol === 'http' && verifyOptions . port == '80' ) ||
( verifyOptions . protocol === 'https' && verifyOptions . port == '443' )
) ? '' : ':' + verifyOptions . port ;
2015-09-18 09:51:17 +00:00
2016-09-06 11:55:54 +00:00
var urlPath = joinUrlPath (
2017-04-07 18:31:11 +00:00
verifyOptions . restApiRoot ,
2016-09-06 11:55:54 +00:00
userModel . http . path ,
userModel . sharedClass . findMethodByName ( 'confirm' ) . http . path
) ;
2017-04-07 18:31:11 +00:00
verifyOptions . verifyHref = verifyOptions . verifyHref ||
verifyOptions . protocol +
2014-11-04 12:52:49 +00:00
'://' +
2017-04-07 18:31:11 +00:00
verifyOptions . host +
2015-09-18 09:51:17 +00:00
displayPort +
2016-09-06 11:55:54 +00:00
urlPath +
2017-02-06 11:39:21 +00:00
'?' + qs . stringify ( {
2017-12-12 08:33:15 +00:00
uid : '' + verifyOptions . user [ pkName ] ,
redirect : verifyOptions . redirect ,
} ) ;
2014-11-04 12:52:49 +00:00
2017-04-07 18:31:11 +00:00
verifyOptions . to = verifyOptions . to || user . email ;
verifyOptions . subject = verifyOptions . subject || g . f ( 'Thanks for Registering' ) ;
verifyOptions . headers = verifyOptions . headers || { } ;
2014-11-04 12:52:49 +00:00
2017-04-07 18:31:11 +00:00
// assert the verifyOptions params that might have been badly defined
assertVerifyOptions ( verifyOptions ) ;
2015-03-12 14:56:43 +00:00
2017-04-07 18:31:11 +00:00
// argument "options" is passed depending on verifyOptions.generateVerificationToken function requirements
var tokenGenerator = verifyOptions . generateVerificationToken ;
if ( tokenGenerator . length == 3 ) {
tokenGenerator ( user , options , addTokenToUserAndSave ) ;
} else {
tokenGenerator ( user , addTokenToUserAndSave ) ;
}
2015-03-12 14:56:43 +00:00
2017-04-07 18:31:11 +00:00
function addTokenToUserAndSave ( err , token ) {
if ( err ) return cb ( err ) ;
2015-03-12 14:56:43 +00:00
user . verificationToken = token ;
2017-04-07 18:31:11 +00:00
user . save ( options , function ( err ) {
if ( err ) return cb ( err ) ;
sendEmail ( user ) ;
2015-03-12 14:56:43 +00:00
} ) ;
2017-04-07 18:31:11 +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 ) {
2017-04-07 18:31:11 +00:00
verifyOptions . verifyHref += '&token=' + user . verificationToken ;
verifyOptions . verificationToken = user . verificationToken ;
verifyOptions . text = verifyOptions . text || g . f ( 'Please verify your email by opening ' +
'this link in a web browser:\n\t%s' , verifyOptions . verifyHref ) ;
verifyOptions . text = verifyOptions . text . replace ( /\{href\}/g , verifyOptions . verifyHref ) ;
2013-12-20 01:49:47 +00:00
2017-04-07 18:31:11 +00:00
// argument "options" is passed depending on templateFn function requirements
var templateFn = verifyOptions . templateFn ;
if ( templateFn . length == 3 ) {
templateFn ( verifyOptions , options , setHtmlContentAndSend ) ;
} else {
templateFn ( verifyOptions , setHtmlContentAndSend ) ;
}
2013-11-20 18:59:29 +00:00
2017-04-07 18:31:11 +00:00
function setHtmlContentAndSend ( err , html ) {
if ( err ) return cb ( err ) ;
2015-05-06 13:52:07 +00:00
2017-04-07 18:31:11 +00:00
verifyOptions . html = html ;
2016-06-07 14:48:28 +00:00
2017-04-07 18:31:11 +00:00
// Remove verifyOptions.template to prevent rejection by certain
// nodemailer transport plugins.
delete verifyOptions . template ;
2015-05-06 13:52:07 +00:00
2017-04-07 18:31:11 +00:00
// argument "options" is passed depending on Email.send function requirements
var Email = verifyOptions . mailer ;
if ( Email . send . length == 3 ) {
Email . send ( verifyOptions , options , handleAfterSend ) ;
2014-11-04 12:52:49 +00:00
} else {
2017-04-07 18:31:11 +00:00
Email . send ( verifyOptions , handleAfterSend ) ;
2014-11-04 12:52:49 +00:00
}
2016-11-11 13:28:49 +00:00
2017-04-07 18:31:11 +00:00
function handleAfterSend ( err , email ) {
if ( err ) return cb ( err ) ;
cb ( 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
}
2017-04-07 18:31:11 +00:00
return cb . promise ;
2014-11-04 12:52:49 +00:00
} ;
2017-04-07 18:31:11 +00:00
function assertVerifyOptions ( verifyOptions ) {
assert ( verifyOptions . type , 'You must supply a verification type (verifyOptions.type)' ) ;
assert ( verifyOptions . type === 'email' , 'Unsupported verification type' ) ;
assert ( verifyOptions . to , 'Must include verifyOptions.to when calling user.verify() ' +
'or the user must have an email property' ) ;
assert ( verifyOptions . from , 'Must include verifyOptions.from when calling user.verify()' ) ;
assert ( typeof verifyOptions . templateFn === 'function' ,
'templateFn must be a function' ) ;
assert ( typeof verifyOptions . generateVerificationToken === 'function' ,
'generateVerificationToken must be a function' ) ;
assert ( verifyOptions . mailer , 'A mailer function must be provided' ) ;
assert ( typeof verifyOptions . mailer . send === 'function' , 'mailer.send must be a function ' ) ;
}
function createVerificationEmailBody ( verifyOptions , options , cb ) {
var template = loopback . template ( verifyOptions . template ) ;
var body = template ( verifyOptions ) ;
2016-11-11 13:28:49 +00:00
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 .
2017-04-07 18:31:11 +00:00
* @ param { object } options remote context options .
* @ param { Function } cb The generator must pass back the new token with this function call .
2015-03-12 14:56:43 +00:00
* /
2017-04-07 18:31:11 +00:00
User . generateVerificationToken = function ( user , options , cb ) {
2015-03-12 14:56:43 +00:00
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 ) ;
}
2017-04-10 12:07:41 +00:00
if ( UserModel . settings . restrictResetPasswordTokenScope ) {
const tokenData = {
ttl : ttl ,
scopes : [ 'reset-password' ] ,
} ;
user . createAccessToken ( tokenData , options , onTokenCreated ) ;
} else {
// We need to preserve backwards-compatibility with
// user-supplied implementations of "createAccessToken"
// that may not support "options" argument (we have such
// examples in our test suite).
user . createAccessToken ( ttl , onTokenCreated ) ;
}
function onTokenCreated ( 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
} ) ;
2017-04-10 12:07:41 +00:00
}
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 ;
2017-08-10 22:34:09 +00:00
if ( ! plain || typeof plain !== 'string' ) {
err = new Error ( g . f ( 'Invalid password.' ) ) ;
err . code = 'INVALID_PASSWORD' ;
err . statusCode = 422 ;
throw err ;
2014-12-20 23:57:38 +00:00
}
2017-08-10 22:34:09 +00:00
// Bcrypt only supports up to 72 bytes; the rest is silently dropped.
var len = Buffer . byteLength ( plain , 'utf8' ) ;
if ( len > MAX _PASSWORD _LENGTH ) {
err = new Error ( g . f ( 'The password entered was too long. Max length is %d (entered %d)' ,
MAX _PASSWORD _LENGTH , len ) ) ;
2016-08-03 23:01:33 +00:00
err . code = 'PASSWORD_TOO_LONG' ;
2017-08-10 22:34:09 +00:00
err . statusCode = 422 ;
throw err ;
2016-08-03 23:01:33 +00:00
}
2014-12-20 23:57:38 +00:00
} ;
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 ] } ;
}
2016-11-21 20:51:43 +00:00
// add principalType in AccessToken.query if using polymorphic relations
// between AccessToken and User
var relatedUser = AccessToken . relations . user ;
2017-02-23 11:56:13 +00:00
var isRelationPolymorphic = relatedUser && relatedUser . polymorphic &&
! relatedUser . modelTo ;
2016-11-21 20:51:43 +00:00
if ( isRelationPolymorphic ) {
query . principalType = this . modelName ;
}
2017-01-13 10:03:06 +00:00
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
}
) ;
2017-03-28 09:10:51 +00:00
UserModel . remoteMethod (
'prototype.verify' ,
{
description : 'Trigger user\'s identity verification with configured verifyOptions' ,
accepts : [
{ arg : 'verifyOptions' , type : 'object' , http : ctx => this . getVerifyOptions ( ) } ,
{ arg : 'options' , type : 'object' , http : 'optionsFromRequest' } ,
] ,
http : { verb : 'post' } ,
}
) ;
2014-12-22 16:23:27 +00:00
UserModel . remoteMethod (
'confirm' ,
2014-11-04 12:52:49 +00:00
{
2017-03-28 09:10:51 +00:00
description : 'Confirm a user registration with identity 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
2017-03-22 14:33:32 +00:00
UserModel . remoteMethod (
'changePassword' ,
{
description : 'Change a user\'s password.' ,
accepts : [
2017-10-27 07:47:07 +00:00
{ arg : 'id' , type : 'any' , http : getUserIdFromRequestContext } ,
2017-03-22 14:33:32 +00:00
{ arg : 'oldPassword' , type : 'string' , required : true , http : { source : 'form' } } ,
{ arg : 'newPassword' , type : 'string' , required : true , http : { source : 'form' } } ,
{ arg : 'options' , type : 'object' , http : 'optionsFromRequest' } ,
] ,
http : { verb : 'POST' , path : '/change-password' } ,
}
) ;
2017-04-10 12:07:41 +00:00
const setPasswordScopes = UserModel . settings . restrictResetPasswordTokenScope ?
[ 'reset-password' ] : undefined ;
2017-04-10 10:20:40 +00:00
UserModel . remoteMethod (
'setPassword' ,
{
description : 'Reset user\'s password via a password-reset token.' ,
accepts : [
2017-10-19 11:29:08 +00:00
{ arg : 'id' , type : 'any' , http : getUserIdFromRequestContext } ,
2017-04-10 10:20:40 +00:00
{ arg : 'newPassword' , type : 'string' , required : true , http : { source : 'form' } } ,
{ arg : 'options' , type : 'object' , http : 'optionsFromRequest' } ,
] ,
2017-04-10 12:07:41 +00:00
accessScopes : setPasswordScopes ,
2017-04-10 10:20:40 +00:00
http : { verb : 'POST' , path : '/reset-password' } ,
}
) ;
2017-10-19 11:29:08 +00:00
function getUserIdFromRequestContext ( ctx ) {
const token = ctx . req . accessToken ;
if ( ! token ) return ;
const hasPrincipalType = 'principalType' in token ;
if ( hasPrincipalType && token . principalType !== UserModel . modelName ) {
// We have multiple user models related to the same access token model
// and the token used to authorize reset-password request was created
// for a different user model.
const err = new Error ( g . f ( 'Access Denied' ) ) ;
err . statusCode = 403 ;
throw err ;
}
return token . userId ;
}
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-04-10 12:07:41 +00:00
User . observe ( 'before save' , function rejectInsecurePasswordChange ( ctx , next ) {
const UserModel = ctx . Model ;
if ( ! UserModel . settings . rejectPasswordChangesViaPatchOrReplace ) {
// In legacy password flow, any DAO method can change the password
return next ( ) ;
}
if ( ctx . isNewInstance ) {
// The password can be always set when creating a new User instance
return next ( ) ;
}
const data = ctx . data || ctx . instance ;
const isPasswordChange = 'password' in data ;
// This is the option set by `setPassword()` API
// when calling `this.patchAttritubes()` to change user's password
if ( ctx . options . setPassword ) {
// Verify that only the password is changed and nothing more or less.
if ( Object . keys ( data ) . length > 1 || ! isPasswordChange ) {
// This is a programmer's error, use the default status code 500
return next ( new Error (
'Invalid use of "options.setPassword". Only "password" can be ' +
'changed when using this option.' ) ) ;
}
return next ( ) ;
}
if ( ! isPasswordChange ) {
return next ( ) ;
}
const err = new Error (
'Changing user password via patch/replace API is not allowed. ' +
'Use changePassword() or setPassword() instead.' ) ;
err . statusCode = 401 ;
err . code = 'PASSWORD_CHANGE_NOT_ALLOWED' ;
next ( err ) ;
} ) ;
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
2017-03-24 13:57:41 +00:00
ctx . Model . find ( { where : where } , ctx . options , function ( err , userInstances ) {
2016-12-09 12:16:42 +00:00
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 ;
2017-03-03 14:53:49 +00:00
if ( ! isEmail . validate ( value ) )
2016-09-06 10:50:03 +00:00
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 ;
}