2014-04-14 21:49:29 +00:00
|
|
|
/*!
|
2013-07-01 23:50:03 +00:00
|
|
|
* Module Dependencies.
|
|
|
|
*/
|
|
|
|
|
2014-10-10 09:53:22 +00:00
|
|
|
var loopback = require('../../lib/loopback')
|
2013-07-03 05:37:31 +00:00
|
|
|
, path = require('path')
|
2013-07-15 21:07:17 +00:00
|
|
|
, SALT_WORK_FACTOR = 10
|
2013-07-03 05:37:31 +00:00
|
|
|
, crypto = require('crypto')
|
2013-09-12 19:09:43 +00:00
|
|
|
, bcrypt = require('bcryptjs')
|
2013-11-15 02:34:51 +00:00
|
|
|
, DEFAULT_TTL = 1209600 // 2 weeks in seconds
|
2013-11-20 18:59:29 +00:00
|
|
|
, DEFAULT_RESET_PW_TTL = 15 * 60 // 15 mins in seconds
|
2013-12-11 03:43:59 +00:00
|
|
|
, DEFAULT_MAX_TTL = 31556926 // 1 year in seconds
|
2014-01-10 18:07:39 +00:00
|
|
|
, 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`
|
|
|
|
* - ALLOW EVERYONE `findById`
|
|
|
|
* - ALLOW OWNER `updateAttributes`
|
|
|
|
*
|
2014-10-01 17:33:36 +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
|
|
|
|
*
|
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) {
|
2013-07-01 23:50:03 +00:00
|
|
|
|
2014-06-06 21:30:18 +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
|
|
|
|
* @callack {Function} cb The callback function
|
|
|
|
* @param {String|Error} err The error string or object
|
|
|
|
* @param {AccessToken} token The generated access token object
|
|
|
|
*/
|
|
|
|
User.prototype.createAccessToken = function(ttl, cb) {
|
2014-06-09 22:00:15 +00:00
|
|
|
var userModel = this.constructor;
|
|
|
|
ttl = Math.min(ttl || userModel.settings.ttl, userModel.settings.maxTTL);
|
2014-06-06 21:30:18 +00:00
|
|
|
this.accessTokens.create({
|
|
|
|
ttl: ttl
|
|
|
|
}, cb);
|
|
|
|
};
|
|
|
|
|
2014-10-23 18:10:39 +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);
|
|
|
|
}
|
|
|
|
return parts;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (credentials.realm) {
|
|
|
|
query.realm = credentials.realm;
|
|
|
|
}
|
|
|
|
var parts;
|
|
|
|
if (credentials.email) {
|
|
|
|
parts = splitPrincipal(credentials.email, realmDelimiter);
|
|
|
|
query.email = parts[1];
|
|
|
|
if (parts[0]) {
|
|
|
|
query.realm = parts[0];
|
|
|
|
}
|
|
|
|
} else if (credentials.username) {
|
|
|
|
parts = splitPrincipal(credentials.username, realmDelimiter);
|
|
|
|
query.username = parts[1];
|
|
|
|
if (parts[0]) {
|
|
|
|
query.realm = parts[0];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return query;
|
|
|
|
}
|
|
|
|
|
2013-07-02 23:51:38 +00:00
|
|
|
/**
|
|
|
|
* Login a user by with the given `credentials`.
|
|
|
|
*
|
2013-12-20 01:49:47 +00:00
|
|
|
* ```js
|
2013-11-13 19:49:08 +00:00
|
|
|
* User.login({username: 'foo', password: 'bar'}, function (err, token) {
|
2014-10-10 09:53:22 +00:00
|
|
|
* console.log(token.id);
|
|
|
|
* });
|
2013-12-20 01:49:47 +00:00
|
|
|
* ```
|
2013-07-02 23:51:38 +00:00
|
|
|
*
|
2014-11-03 22:07:19 +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
|
2013-07-02 23:51:38 +00:00
|
|
|
*/
|
|
|
|
|
2014-10-10 09:53:22 +00:00
|
|
|
User.login = function(credentials, include, fn) {
|
2014-06-06 21:30:18 +00:00
|
|
|
var self = this;
|
2014-01-30 13:33:45 +00:00
|
|
|
if (typeof include === 'function') {
|
|
|
|
fn = include;
|
|
|
|
include = undefined;
|
|
|
|
}
|
|
|
|
|
2014-09-03 05:58:49 +00:00
|
|
|
include = (include || '');
|
|
|
|
if (Array.isArray(include)) {
|
2014-10-10 09:53:22 +00:00
|
|
|
include = include.map(function(val) {
|
2014-09-03 05:58:49 +00:00
|
|
|
return val.toLowerCase();
|
|
|
|
});
|
2014-10-10 09:53:22 +00:00
|
|
|
} else {
|
2014-09-03 05:58:49 +00:00
|
|
|
include = include.toLowerCase();
|
|
|
|
}
|
2014-10-01 17:33:36 +00:00
|
|
|
|
2014-10-23 18:10:39 +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);
|
2014-09-03 05:58:49 +00:00
|
|
|
|
2014-10-23 18:10:39 +00:00
|
|
|
if(realmRequired && !query.realm) {
|
|
|
|
var err1 = new Error('realm is required');
|
|
|
|
err1.statusCode = 400;
|
|
|
|
return fn(err1);
|
|
|
|
}
|
|
|
|
if (!query.email && !query.username) {
|
|
|
|
var err2 = new Error('username or email is required');
|
|
|
|
err2.statusCode = 400;
|
|
|
|
return fn(err2);
|
2013-07-16 01:22:33 +00:00
|
|
|
}
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2014-06-06 21:30:18 +00:00
|
|
|
self.findOne({where: query}, function(err, user) {
|
2013-07-02 23:51:38 +00:00
|
|
|
var defaultError = new Error('login failed');
|
2014-02-28 21:19:52 +00:00
|
|
|
defaultError.statusCode = 401;
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2014-10-10 09:53:22 +00:00
|
|
|
if (err) {
|
2014-01-27 22:47:48 +00:00
|
|
|
debug('An error is reported from User.findOne: %j', err);
|
2013-07-02 23:51:38 +00:00
|
|
|
fn(defaultError);
|
2014-10-10 09:53:22 +00:00
|
|
|
} else if (user) {
|
2014-07-07 21:09:45 +00:00
|
|
|
if (self.settings.emailVerificationRequired) {
|
|
|
|
if (!user.emailVerified) {
|
|
|
|
// Fail to log in if email verification is not done yet
|
|
|
|
debug('User email has not been verified');
|
|
|
|
err = new Error('login failed as the email has not been verified');
|
|
|
|
err.statusCode = 401;
|
|
|
|
return fn(err);
|
|
|
|
}
|
|
|
|
}
|
2013-07-02 23:51:38 +00:00
|
|
|
user.hasPassword(credentials.password, function(err, isMatch) {
|
2014-10-10 09:53:22 +00:00
|
|
|
if (err) {
|
2014-01-27 22:47:48 +00:00
|
|
|
debug('An error is reported from User.hasPassword: %j', err);
|
2013-07-02 23:51:38 +00:00
|
|
|
fn(defaultError);
|
2014-10-10 09:53:22 +00:00
|
|
|
} else if (isMatch) {
|
2014-06-06 21:30:18 +00:00
|
|
|
user.createAccessToken(credentials.ttl, function(err, token) {
|
2014-01-30 13:33:45 +00:00
|
|
|
if (err) return fn(err);
|
2014-09-03 05:58:49 +00:00
|
|
|
if (Array.isArray(include) ? include.indexOf('user') !== -1 : include === 'user') {
|
2014-01-30 13:33:45 +00:00
|
|
|
// 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);
|
|
|
|
});
|
2013-07-02 23:51:38 +00:00
|
|
|
} else {
|
2014-01-27 22:47:48 +00:00
|
|
|
debug('The password is invalid for user %s', query.email || query.username);
|
2013-07-02 23:51:38 +00:00
|
|
|
fn(defaultError);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
2014-01-27 22:47:48 +00:00
|
|
|
debug('No matching record is found for user %s', query.email || query.username);
|
2013-07-02 23:51:38 +00:00
|
|
|
fn(defaultError);
|
|
|
|
}
|
|
|
|
});
|
2014-02-14 18:31:30 +00:00
|
|
|
};
|
2013-07-02 23:51:38 +00:00
|
|
|
|
2013-07-03 05:37:31 +00:00
|
|
|
/**
|
2013-11-13 19:49:08 +00:00
|
|
|
* Logout a user with the given accessToken id.
|
2013-07-03 05:37:31 +00:00
|
|
|
*
|
2013-12-20 01:49:47 +00:00
|
|
|
* ```js
|
2013-07-03 05:37:31 +00:00
|
|
|
* User.logout('asd0a9f8dsj9s0s3223mk', function (err) {
|
2014-10-10 09:53:22 +00:00
|
|
|
* console.log(err || 'Logged out');
|
|
|
|
* });
|
2013-12-20 01:49:47 +00:00
|
|
|
* ```
|
2013-07-03 05:37:31 +00:00
|
|
|
*
|
2013-11-13 19:49:08 +00:00
|
|
|
* @param {String} accessTokenID
|
2013-12-20 01:49:47 +00:00
|
|
|
* @callback {Function} callback
|
|
|
|
* @param {Error} err
|
2013-07-03 05:37:31 +00:00
|
|
|
*/
|
|
|
|
|
2014-10-10 09:53:22 +00:00
|
|
|
User.logout = function(tokenId, fn) {
|
|
|
|
this.relations.accessTokens.modelTo.findById(tokenId, function(err, accessToken) {
|
|
|
|
if (err) {
|
2013-07-03 05:37:31 +00:00
|
|
|
fn(err);
|
2014-10-10 09:53:22 +00:00
|
|
|
} else if (accessToken) {
|
2013-11-13 19:49:08 +00:00
|
|
|
accessToken.destroy(fn);
|
2013-07-03 05:37:31 +00:00
|
|
|
} else {
|
2013-11-13 19:49:08 +00:00
|
|
|
fn(new Error('could not find accessToken'));
|
2013-07-03 05:37:31 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2013-07-02 23:51:38 +00:00
|
|
|
/**
|
|
|
|
* Compare the given `password` with the users hashed password.
|
|
|
|
*
|
|
|
|
* @param {String} password The plain text password
|
|
|
|
* @returns {Boolean}
|
|
|
|
*/
|
|
|
|
|
2014-10-10 09:53:22 +00:00
|
|
|
User.prototype.hasPassword = function(plain, fn) {
|
|
|
|
if (this.password && plain) {
|
2013-07-15 21:07:17 +00:00
|
|
|
bcrypt.compare(plain, this.password, function(err, isMatch) {
|
2014-10-10 09:53:22 +00:00
|
|
|
if (err) return fn(err);
|
2013-09-12 19:09:43 +00:00
|
|
|
fn(null, isMatch);
|
|
|
|
});
|
2013-07-15 21:07:17 +00:00
|
|
|
} else {
|
|
|
|
fn(null, false);
|
|
|
|
}
|
2013-07-02 23:51:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2013-12-20 01:49:47 +00:00
|
|
|
* Verify a user's identity by sending them a confirmation email.
|
2013-07-03 05:37:31 +00:00
|
|
|
*
|
2013-12-20 01:49:47 +00:00
|
|
|
* ```js
|
2013-07-03 05:37:31 +00:00
|
|
|
* var options = {
|
2014-10-10 09:53:22 +00:00
|
|
|
* type: 'email',
|
|
|
|
* to: user.email,
|
|
|
|
* template: 'verify.ejs',
|
|
|
|
* redirect: '/'
|
|
|
|
* };
|
2013-07-03 05:37:31 +00:00
|
|
|
*
|
|
|
|
* user.verify(options, next);
|
2013-12-20 01:49:47 +00:00
|
|
|
* ```
|
2013-07-03 05:37:31 +00:00
|
|
|
*
|
2014-10-15 07:07:30 +00:00
|
|
|
* @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'.
|
|
|
|
* @property {String} redirect Page to which user will be redirected after
|
|
|
|
* they verify their email, for example `'/'` for root URI.
|
2013-07-02 23:51:38 +00:00
|
|
|
*/
|
|
|
|
|
2014-10-10 09:53:22 +00:00
|
|
|
User.prototype.verify = function(options, fn) {
|
2013-07-03 05:37:31 +00:00
|
|
|
var user = this;
|
2014-07-27 05:39:42 +00:00
|
|
|
var userModel = this.constructor;
|
2013-07-03 05:37:31 +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');
|
|
|
|
assert(options.to || this.email, 'Must include options.to when calling user.verify() or the user must have an email property');
|
|
|
|
assert(options.from, 'Must include options.from when calling user.verify() or the user must have an email property');
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2013-07-03 05:37:31 +00:00
|
|
|
options.redirect = options.redirect || '/';
|
|
|
|
options.template = path.resolve(options.template || path.join(__dirname, '..', '..', 'templates', 'verify.ejs'));
|
|
|
|
options.user = this;
|
|
|
|
options.protocol = options.protocol || 'http';
|
2014-07-27 05:39:42 +00:00
|
|
|
|
2014-08-08 04:45:18 +00:00
|
|
|
var app = userModel.app;
|
2014-07-27 05:39:42 +00:00
|
|
|
options.host = options.host || (app && app.get('host')) || 'localhost';
|
|
|
|
options.port = options.port || (app && app.get('port')) || 3000;
|
2014-10-10 09:53:22 +00:00
|
|
|
options.restApiRoot = options.restApiRoot || (app && app.get('restApiRoot')) || '/api';
|
2013-07-03 05:37:31 +00:00
|
|
|
options.verifyHref = options.verifyHref ||
|
2014-10-10 09:53:22 +00:00
|
|
|
options.protocol
|
|
|
|
+ '://'
|
|
|
|
+ options.host
|
|
|
|
+ ':'
|
|
|
|
+ options.port
|
|
|
|
+ options.restApiRoot
|
|
|
|
+ userModel.http.path
|
|
|
|
+ userModel.confirm.http.path
|
|
|
|
+ '?uid='
|
|
|
|
+ options.user.id
|
|
|
|
+ '&redirect='
|
|
|
|
+ options.redirect;
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2013-07-03 05:37:31 +00:00
|
|
|
|
|
|
|
// Email model
|
2014-01-23 22:40:27 +00:00
|
|
|
var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email);
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2013-07-03 05:37:31 +00:00
|
|
|
crypto.randomBytes(64, function(err, buf) {
|
2014-10-10 09:53:22 +00:00
|
|
|
if (err) {
|
2013-07-03 05:37:31 +00:00
|
|
|
fn(err);
|
|
|
|
} else {
|
2014-03-21 19:02:11 +00:00
|
|
|
user.verificationToken = buf.toString('hex');
|
2014-10-10 09:53:22 +00:00
|
|
|
user.save(function(err) {
|
|
|
|
if (err) {
|
2013-07-03 05:37:31 +00:00
|
|
|
fn(err);
|
|
|
|
} else {
|
2014-04-10 03:01:58 +00:00
|
|
|
sendEmail(user);
|
2013-07-03 05:37:31 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2013-07-03 05:37:31 +00:00
|
|
|
// TODO - support more verification types
|
|
|
|
function sendEmail(user) {
|
2014-04-10 03:01:58 +00:00
|
|
|
options.verifyHref += '&token=' + user.verificationToken;
|
|
|
|
|
2013-07-03 05:37:31 +00:00
|
|
|
options.text = options.text || 'Please verify your email by opening this link in a web browser:\n\t{href}';
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2013-07-03 05:37:31 +00:00
|
|
|
options.text = options.text.replace('{href}', options.verifyHref);
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2013-07-16 17:49:25 +00:00
|
|
|
var template = loopback.template(options.template);
|
2013-07-03 05:37:31 +00:00
|
|
|
Email.send({
|
|
|
|
to: options.to || user.email,
|
2014-02-24 02:08:13 +00:00
|
|
|
from: options.from,
|
2013-07-03 05:37:31 +00:00
|
|
|
subject: options.subject || 'Thanks for Registering',
|
|
|
|
text: options.text,
|
2014-10-24 05:46:23 +00:00
|
|
|
html: template(options),
|
|
|
|
headers: options.headers || {}
|
|
|
|
}, function (err, email) {
|
|
|
|
if(err) {
|
2013-07-03 05:37:31 +00:00
|
|
|
fn(err);
|
|
|
|
} else {
|
|
|
|
fn(null, {email: email, token: user.verificationToken, uid: user.id});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2013-07-02 23:51:38 +00:00
|
|
|
}
|
|
|
|
|
2013-12-20 01:49:47 +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
|
|
|
|
*/
|
2014-10-10 09:53:22 +00:00
|
|
|
User.confirm = function(uid, token, redirect, fn) {
|
|
|
|
this.findById(uid, function(err, user) {
|
|
|
|
if (err) {
|
2013-07-03 05:37:31 +00:00
|
|
|
fn(err);
|
|
|
|
} else {
|
2014-10-10 09:53:22 +00:00
|
|
|
if (user && user.verificationToken === token) {
|
2013-07-03 05:37:31 +00:00
|
|
|
user.verificationToken = undefined;
|
|
|
|
user.emailVerified = true;
|
2014-10-10 09:53:22 +00:00
|
|
|
user.save(function(err) {
|
|
|
|
if (err) {
|
2014-07-17 05:42:05 +00:00
|
|
|
fn(err);
|
2013-07-03 05:37:31 +00:00
|
|
|
} else {
|
|
|
|
fn();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
2014-07-17 05:42:05 +00:00
|
|
|
if (user) {
|
|
|
|
err = new Error('Invalid token: ' + token);
|
|
|
|
err.statusCode = 400;
|
|
|
|
} else {
|
|
|
|
err = new Error('User not found: ' + uid);
|
|
|
|
err.statusCode = 404;
|
|
|
|
}
|
|
|
|
fn(err);
|
2013-07-03 05:37:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2013-11-20 18:59:29 +00:00
|
|
|
|
2013-12-20 01:49:47 +00:00
|
|
|
/**
|
|
|
|
* Create a short lived acess token for temporary login. Allows users
|
|
|
|
* to change passwords if forgotten.
|
|
|
|
*
|
|
|
|
* @options {Object} options
|
|
|
|
* @prop {String} email The user's email address
|
|
|
|
* @callback {Function} callback
|
|
|
|
* @param {Error} err
|
|
|
|
*/
|
|
|
|
|
2013-11-20 18:59:29 +00:00
|
|
|
User.resetPassword = function(options, cb) {
|
|
|
|
var UserModel = this;
|
|
|
|
var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL;
|
|
|
|
|
|
|
|
options = options || {};
|
2014-10-10 09:53:22 +00:00
|
|
|
if (typeof options.email === 'string') {
|
2014-05-10 07:43:01 +00:00
|
|
|
UserModel.findOne({ where: {email: options.email} }, function(err, user) {
|
2014-10-10 09:53:22 +00:00
|
|
|
if (err) {
|
2013-11-20 18:59:29 +00:00
|
|
|
cb(err);
|
2014-10-10 09:53:22 +00:00
|
|
|
} else if (user) {
|
2013-11-20 18:59:29 +00:00
|
|
|
// create a short lived access token for temp login to change password
|
|
|
|
// TODO(ritch) - eventually this should only allow password change
|
|
|
|
user.accessTokens.create({ttl: ttl}, function(err, accessToken) {
|
2014-10-10 09:53:22 +00:00
|
|
|
if (err) {
|
2013-11-20 18:59:29 +00:00
|
|
|
cb(err);
|
|
|
|
} else {
|
|
|
|
cb();
|
|
|
|
UserModel.emit('resetPasswordRequest', {
|
|
|
|
email: options.email,
|
2014-05-10 07:43:01 +00:00
|
|
|
accessToken: accessToken,
|
|
|
|
user: user
|
2013-11-20 18:59:29 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
cb();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
var err = new Error('email is required');
|
|
|
|
err.statusCode = 400;
|
|
|
|
|
|
|
|
cb(err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-12-20 01:49:47 +00:00
|
|
|
/*!
|
2013-07-12 19:40:36 +00:00
|
|
|
* Setup an extended user model.
|
2013-07-03 05:37:31 +00:00
|
|
|
*/
|
|
|
|
|
2014-10-10 09:53:22 +00:00
|
|
|
User.setup = function() {
|
2013-08-15 23:59:16 +00:00
|
|
|
// We need to call the base class's setup method
|
2014-10-10 09:53:22 +00:00
|
|
|
User.base.setup.call(this);
|
2013-07-03 05:37:31 +00:00
|
|
|
var UserModel = this;
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2013-11-15 02:34:51 +00:00
|
|
|
// max ttl
|
|
|
|
this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL;
|
|
|
|
this.settings.ttl = DEFAULT_TTL;
|
|
|
|
|
2014-10-10 09:53:22 +00:00
|
|
|
UserModel.setter.password = function(plain) {
|
2013-07-16 01:22:33 +00:00
|
|
|
var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR);
|
2013-08-30 23:52:27 +00:00
|
|
|
this.$password = bcrypt.hashSync(plain, salt);
|
2013-07-16 01:22:33 +00:00
|
|
|
}
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2014-07-07 21:09:45 +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;
|
|
|
|
}
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
|
2013-07-16 17:49:25 +00:00
|
|
|
loopback.remoteMethod(
|
2013-07-02 23:51:38 +00:00
|
|
|
UserModel.login,
|
|
|
|
{
|
2014-07-19 05:48:07 +00:00
|
|
|
description: 'Login a user with username/email and password',
|
2013-07-02 23:51:38 +00:00
|
|
|
accepts: [
|
2014-01-30 13:33:45 +00:00
|
|
|
{arg: 'credentials', type: 'object', required: true, http: {source: 'body'}},
|
2014-10-10 09:53:22 +00:00
|
|
|
{arg: 'include', type: 'string', http: {source: 'query' }, description: 'Related objects to include in the response. ' +
|
|
|
|
'See the description of return value for more details.'}
|
2013-07-02 23:51:38 +00:00
|
|
|
],
|
2014-01-30 13:33:45 +00:00
|
|
|
returns: {
|
2014-10-10 09:53:22 +00:00
|
|
|
arg: 'accessToken', type: 'object', root: true, description: 'The response body contains properties of the AccessToken created on login.\n' +
|
|
|
|
'Depending on the value of `include` parameter, the body may contain ' +
|
|
|
|
'additional properties:\n\n' +
|
|
|
|
' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n'
|
2014-01-30 13:33:45 +00:00
|
|
|
},
|
2013-07-02 23:51:38 +00:00
|
|
|
http: {verb: 'post'}
|
|
|
|
}
|
|
|
|
);
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2013-07-16 17:49:25 +00:00
|
|
|
loopback.remoteMethod(
|
2013-07-03 05:37:31 +00:00
|
|
|
UserModel.logout,
|
|
|
|
{
|
2014-07-19 05:48:07 +00:00
|
|
|
description: 'Logout a user with access token',
|
2013-07-03 05:37:31 +00:00
|
|
|
accepts: [
|
2013-12-18 05:22:05 +00:00
|
|
|
{arg: 'access_token', type: 'string', required: true, http: function(ctx) {
|
|
|
|
var req = ctx && ctx.req;
|
|
|
|
var accessToken = req && req.accessToken;
|
|
|
|
var tokenID = accessToken && accessToken.id;
|
|
|
|
|
|
|
|
return tokenID;
|
2014-10-10 09:53:22 +00:00
|
|
|
}, description: 'Do not supply this argument, it is automatically extracted ' +
|
|
|
|
'from request headers.'
|
2014-01-30 13:35:49 +00:00
|
|
|
}
|
2013-07-03 05:37:31 +00:00
|
|
|
],
|
|
|
|
http: {verb: 'all'}
|
|
|
|
}
|
|
|
|
);
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2013-07-16 17:49:25 +00:00
|
|
|
loopback.remoteMethod(
|
2013-07-03 05:37:31 +00:00
|
|
|
UserModel.confirm,
|
|
|
|
{
|
2014-07-19 05:48:07 +00:00
|
|
|
description: 'Confirm a user registration with email verification token',
|
2013-07-03 05:37:31 +00:00
|
|
|
accepts: [
|
|
|
|
{arg: 'uid', type: 'string', required: true},
|
|
|
|
{arg: 'token', type: 'string', required: true},
|
|
|
|
{arg: 'redirect', type: 'string', required: true}
|
|
|
|
],
|
|
|
|
http: {verb: 'get', path: '/confirm'}
|
|
|
|
}
|
|
|
|
);
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2013-11-20 18:59:29 +00:00
|
|
|
loopback.remoteMethod(
|
|
|
|
UserModel.resetPassword,
|
|
|
|
{
|
2014-07-19 05:48:07 +00:00
|
|
|
description: 'Reset password for a user with email',
|
2013-11-20 18:59:29 +00:00
|
|
|
accepts: [
|
|
|
|
{arg: 'options', type: 'object', required: true, http: {source: 'body'}}
|
|
|
|
],
|
|
|
|
http: {verb: 'post', path: '/reset'}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2014-10-10 09:53:22 +00:00
|
|
|
UserModel.on('attached', function() {
|
|
|
|
UserModel.afterRemote('confirm', function(ctx, inst, next) {
|
|
|
|
if (ctx.req) {
|
2013-07-03 05:37:31 +00:00
|
|
|
ctx.res.redirect(ctx.req.param('redirect'));
|
|
|
|
} else {
|
2014-10-15 14:42:46 +00:00
|
|
|
next(new Error('transport unsupported'));
|
2013-07-03 05:37:31 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2013-07-12 19:40:36 +00:00
|
|
|
// default models
|
2014-10-10 09:53:22 +00:00
|
|
|
assert(loopback.Email, 'Email model must be defined before User model');
|
|
|
|
UserModel.email = loopback.Email;
|
|
|
|
|
|
|
|
assert(loopback.AccessToken, 'AccessToken model must be defined before User model');
|
|
|
|
UserModel.accessToken = loopback.AccessToken;
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2014-07-19 12:18:21 +00:00
|
|
|
// email validation regex
|
2013-07-13 00:03:13 +00:00
|
|
|
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2014-10-23 18:10:39 +00:00
|
|
|
|
2013-07-13 00:03:13 +00:00
|
|
|
UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'});
|
2014-10-23 18:10:39 +00:00
|
|
|
|
|
|
|
// FIXME: We need to add support for uniqueness of composite keys in juggler
|
|
|
|
if (!(UserModel.settings.realmRequired || UserModel.settings.realmDelimiter)) {
|
|
|
|
UserModel.validatesUniquenessOf('email', {message: 'Email already exists'});
|
|
|
|
UserModel.validatesUniquenessOf('username', {message: 'User already exists'});
|
|
|
|
}
|
2014-04-10 03:01:58 +00:00
|
|
|
|
2013-07-02 23:51:38 +00:00
|
|
|
return UserModel;
|
|
|
|
}
|
|
|
|
|
2013-07-03 05:37:31 +00:00
|
|
|
/*!
|
|
|
|
* Setup the base user.
|
|
|
|
*/
|
|
|
|
|
2013-07-17 21:08:14 +00:00
|
|
|
User.setup();
|
2014-10-10 09:53:22 +00:00
|
|
|
|
|
|
|
};
|