Skip to content

Commit

Permalink
Updates the password reset code. The flows need to be re-evaluated.
Browse files Browse the repository at this point in the history
I don't think they make much sense but the code has been updated so that when the flows are changed it should be easy.
  • Loading branch information
brianhyder committed Jul 4, 2016
1 parent 38d5fee commit 98e30a6
Show file tree
Hide file tree
Showing 10 changed files with 533 additions and 185 deletions.
26 changes: 24 additions & 2 deletions include/dao/indices.js
@@ -1,3 +1,20 @@
/*
Copyright (C) 2016 PencilBlue, LLC
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';

/**
* Ascending index value
Expand Down Expand Up @@ -119,7 +136,12 @@ module.exports = function IndicesModule(multisite) {
//password reset
{
collection: 'password_reset',
spec: {verification_code: ASC},
spec: {verificationCode: ASC},
options: {unique: true}
},
{
collection: 'password_reset',
spec: {userId: ASC},
options: {unique: true}
},

Expand Down Expand Up @@ -336,4 +358,4 @@ module.exports = function IndicesModule(multisite) {
options: {expireAfterSeconds: 25920000}
}
];
};
};
2 changes: 2 additions & 0 deletions include/requirements.js
Expand Up @@ -282,5 +282,7 @@ module.exports = function PB(config) {
pb.PluginServiceLoader = require(path.join(config.docRoot, '/include/service/entities/plugins/loaders/plugin_service_loader.js'))(pb);
pb.PluginLocalizationLoader = require(path.join(config.docRoot, '/include/service/entities/plugins/loaders/plugin_localization_loader.js'))(pb);

pb.PasswordResetService = require(path.join(config.docRoot, '/include/service/entities/password_reset_service.js'))(pb);

return pb;
};
3 changes: 2 additions & 1 deletion include/service/base_object_service.js
Expand Up @@ -637,7 +637,8 @@ module.exports = function(pb) {
/**
* Deletes a single item based on the specified query in the options
* @method deleteSingle
* @param {Object} options
* @param {Object} [options] See BaseObjectService#getSingle
* @param {object} [options.where]
* @param {Function} cb
*/
BaseObjectService.prototype.deleteSingle = function(options, cb) {
Expand Down
271 changes: 271 additions & 0 deletions include/service/entities/password_reset_service.js
@@ -0,0 +1,271 @@
/*
Copyright (C) 2016 PencilBlue, LLC
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';

//dependencies
var util = require('../../util.js');
var async = require('async');

module.exports = function(pb) {

//pb dependencies
var DAO = pb.DAO;
var BaseObjectService = pb.BaseObjectService;
var ValidationService = pb.ValidationService;
var UrlService = pb.UrlService;
var SiteService = pb.SiteService;

/**
* @private
* @static
* @readonly
* @property TYPE
* @type {String}
*/
var TYPE = 'password_reset';

/**
* Provides interactions with topics
* @class PasswordResetService
* @extends BaseObjectService
* @constructor
* @param {Object} context
* @param {string} context.site
* @param {boolean} context.onlyThisSite
* @param {UserService} context.userService
* @param {SiteService} context.siteService
* @param {EmailService} context.emailService
*/
function PasswordResetService(context) {
if (!util.isObject(context)) {
context = {};
}

/**
* @property userService
* @type {UserService}
*/
this.userService = context.userService;

/**
* @property siteService
* @type {SiteService}
*/
this.siteService = context.siteService;

/**
* @property emailService
* @type {EmailService}
*/
this.emailService = context.emailService;

context.type = TYPE;
PasswordResetService.super_.call(this, context);
}
util.inherits(PasswordResetService, BaseObjectService);

/**
* @method addIfNotExists
* @param {string} usernameOrEmail
* @param {function} cb (Error, {created: boolean, data: {}})
*/
PasswordResetService.prototype.addIfNotExists = function(usernameOrEmail, cb) {
var self = this;

//retrieve the user
this.userService.getByUsernameOrEmail(usernameOrEmail, {}, function(err, userObj) {
if (util.isError(err)) {
return cb(err);
}
if (util.isNullOrUndefined(userObj)) {
return cb(BaseObjectService.notFound());
}

//attempt to retrieve any existing reset
self.getSingle({where: {userId: userObj.id}}, function(err, passwordResetObj) {
if (util.isError(err)) {
return cb(err);
}

//need to know if we should create the DTO or not
var created = !passwordResetObj;
if (created) {
passwordResetObj = {userId: userObj.id};
}

//now persist it back
self.save(passwordResetObj, function(err, persistedPasswordReset) {
if (util.isError(err)) {
return cb(err);
}

//now attempt to send the notification
self.sendPasswordResetEmail(userObj, persistedPasswordReset, function(err) {
cb(err, {
created: created,
data: persistedPasswordReset
});
});
});
});
});
};

/**
* Sends a password reset email to a user
* @method sendPasswordResetEmail
* @param {object} user A user object
* @param {object} passwordReset A password reset object containing the verification code
* @param {function} cb (Error)
*/
PasswordResetService.prototype.sendPasswordResetEmail = function(user, passwordReset, cb) {
var self = this;

var tasks = [
util.wrapTask(this.siteService, this.siteService.getByUid, [this.context.site]),
function(siteInfo, callback) {
var root = SiteService.getHostWithProtocol(siteInfo.hostname);
var verificationUrl = UrlService.urlJoin(root, '/actions/user/reset_password') +
util.format('?email=%s&code=%s', encodeURIComponent(user.email), encodeURIComponent(passwordReset.verificationCode));

var options = {
to: user.email,
subject: siteInfo.displayName + ' Password Reset',
template: 'admin/elements/password_reset_email',
replacements: {
'verification_url': verificationUrl,
'first_name': user.first_name,
'last_name': user.last_name
}
};
self.emailService.sendFromTemplate(options, callback);
}
];
async.waterfall(tasks, cb);
};

/**
* @method validate
* @param context
* @param {object} context.data
* @param {Array} context.validationErrors
* @param {function} cb
* @returns {*}
*/
PasswordResetService.prototype.validate = function(context, cb) {
var tasks = [
util.wrapTask(this, this.validateUserId, [context]),
util.wrapTask(this, this.validateVerificationCode, [context])
];
async.parallel(tasks, cb);
};

PasswordResetService.prototype.validateUserId = function(context, cb) {
var obj = context.data;
var errors = context.validationErrors;

if (!ValidationService.isIdStr(obj.userId, true)) {
errors.push(BaseObjectService.validationFailure('userId', 'The userId must be a valid string'));
return cb();
}

var options = {
where: DAO.getIdWhere(obj.userId)
};
this.userService.count(options, function(err, count) {
if (count !== 1) {
errors.push(BaseObjectService.validationFailure('userId', 'The userId must reference an existing user'));
}
cb(err);
});
};

PasswordResetService.prototype.validateVerificationCode = function(context, cb) {
var obj = context.data;
var errors = context.validationErrors;

if (!ValidationService.isNonEmptyStr(obj.verificationCode, true)) {
errors.push(BaseObjectService.validationFailure('verificationCode', 'The verificationCode must be a non-empty string'));
return cb();
}

var where = {
verificationCode: obj.verificationCode
};
this.dao.unique(TYPE, where, obj[DAO.getIdField()], function(err, isUnique) {
if (!isUnique) {
errors.push(BaseObjectService.validationFailure('verificationCode', 'The verificationCode must be unique'));
}
cb(err);
});
};

/**
*
* @static
* @method
* @param {Object} context
* @param {PasswordResetService} context.service An instance of the service that triggered
* the event that called this handler
* @param {Function} cb A callback that takes a single parameter: an error if occurred
*/
PasswordResetService.onFormat = function(context, cb) {
var dto = context.data;
dto.userId = BaseObjectService.sanitize(dto.userId);
cb();
};

/**
*
* @static
* @method
* @param {Object} context
* @param {PasswordResetService} context.service An instance of the service that triggered
* the event that called this handler
* @param {Function} cb A callback that takes a single parameter: an error if occurred
*/
PasswordResetService.onMerge = function(context, cb) {
context.object.userId = context.data.userId;

//the verification code is overwritten each time the object is saved. This makes it a singleton and prevents
//multiple requests for a password reset, per user, floating around
context.object.verificationCode = util.uniqueId();
cb();
};

/**
*
* @static
* @method validate
* @param {Object} context
* @param {Object} context.data The DTO that was provided for persistence
* @param {PasswordResetService} context.service An instance of the service that triggered
* the event that called this handler
* @param {Function} cb A callback that takes a single parameter: an error if occurred
*/
PasswordResetService.onValidate = function(context, cb) {
context.service.validate(context, cb);
};

//Event Registries
BaseObjectService.on(TYPE + '.' + BaseObjectService.FORMAT, PasswordResetService.onFormat);
BaseObjectService.on(TYPE + '.' + BaseObjectService.MERGE, PasswordResetService.onMerge);
BaseObjectService.on(TYPE + '.' + BaseObjectService.VALIDATE, PasswordResetService.onValidate);

//exports
return PasswordResetService;
};
10 changes: 5 additions & 5 deletions include/service/entities/topic_service.js
Expand Up @@ -52,24 +52,24 @@ module.exports = function(pb) {
/**
*
* @static
* @method
* @method format
* @param {Object} context
* @param {TopicService} service An instance of the service that triggered
* @param {TopicService} context.service An instance of the service that triggered
* the event that called this handler
* @param {Function} cb A callback that takes a single parameter: an error if occurred
*/
TopicService.format = function(context, cb) {
var dto = context.data;
dto.name = pb.BaseController.sanitize(dto.name);
dto.name = BaseObjectService.sanitize(dto.name);
cb(null);
};

/**
*
* @static
* @method
* @method merge
* @param {Object} context
* @param {TopicService} service An instance of the service that triggered
* @param {TopicService} context.service An instance of the service that triggered
* the event that called this handler
* @param {Function} cb A callback that takes a single parameter: an error if occurred
*/
Expand Down

0 comments on commit 98e30a6

Please sign in to comment.