periodicjs.ext.login
Version:
An authentication extension for periodicjs that uses passport to authenticate user sessions.
679 lines (638 loc) • 22 kB
JavaScript
;
var async = require('async'),
appSettings,
appenvironment,
bcrypt = require('bcrypt'),
CoreController,
CoreUtilities,
CoreMailer,
jwt = require('jsonwebtoken'),
moment = require('moment'),
loginExtSettings,
logger,
merge = require('utils-merge'),
mongoose,
User,
path = require('path'),
passport;
var periodic;
const capitalize = require('capitalize');
// Utility Functions
var waterfall = function (array, cb) {
async.waterfall(array, cb);
};
var encode = function (data) {
return jwt.sign(data, loginExtSettings.token.secret);
};
var decode = function (data, cb) {
logger.debug('jwt decode data', data);
jwt.verify(data, loginExtSettings.token.secret, {}, function (err, decoded_token) {
if (err) {
logger.error('Error from JWT.verify', err);
cb(err);
} else {
cb(null, decoded_token);
}
});
};
var hasExpired = function (token_expires_millis) {
var now = new Date();
var diff = (now.getTime() - token_expires_millis);
return diff > 0;
};
var invalidateUserToken = function (req, res, next, cb) {
let token = req.controllerData.token;
let UserModel = Object.assign({}, User);
if (req.body.entitytype) {
UserModel = mongoose.model(capitalize(req.body.entitytype));
}
try {
if (!token){
// console.log({ token }, 'req.body', req.body);
UserModel.findOne({
'attributes.reset_token_link': req.body.token || req.body.query,
}, function (err, usr) {
// console.log({ err, usr });
if (err) {
logger.error('error finding the user for invalidate token fn');
cb(err, null);
} else if (!usr){
cb(new Error('Invalid token'));
} else {
usr.attributes = Object.assign({}, usr.attributes);
usr.attributes.reset_token = '';
usr.attributes.reset_token_link = '';
usr.attributes.reset_token_expires_millis = 0;
cb(false, req, res, next, usr);
}
});
} else {
UserModel.findOne({
'attributes.reset_token': token,
}, function (err, usr) {
// console.log({ err, usr });
if (err) {
logger.error('error finding the user for invalidate token fn');
cb(err, null);
} else if (!usr){
cb(new Error('Invalid token'));
} else {
usr.attributes = Object.assign({}, usr.attributes);
usr.attributes.reset_token = '';
usr.attributes.reset_token_link = '';
usr.attributes.reset_token_expires_millis = 0;
cb(false, req, res, next, usr);
}
});
}
} catch (e) {
cb(e);
}
};
var resetPassword = function (req, res, next, user, cb) {
var err;
// console.log('loginExtSettings', loginExtSettings);
// console.log('req.body', req.body);
if (req.body.password) {
var validate = User.checkValidation({
newuser: req.body,
checkusername: false,
checkemail: false,
checkpassword: true,
useComplexity: loginExtSettings.complexitySettings.useComplexity,
complexity: loginExtSettings.complexitySettings.settings.medium,
});
if (req.body.password !== req.body.passwordconfirm) {
err = new Error('Passwords do not match in token');
req.flash('error', err);
cb(err, null);
} else if (validate !== null) {
req.flash('error', validate);
cb(validate, null);
} else {
var salt = bcrypt.genSaltSync(10),
hash = bcrypt.hashSync(req.body.password, salt);
user.password = hash;
cb(null, user, req);
}
} else {
cb(new Error('Invalid Empty Password'), null);
}
};
/**
* description The save user function has two special fn calls on the model to mark the properties on it as changed/modified this gets around some werid edge cases when its being updated in memory but not save in mongo
*
*/
function saveUser(user, req, cb) {
user.markModified('attributes');
user.markModified('password');
if (user.extensionattributes && user.extensionattributes.login && user.extensionattributes.login.flagged) {
user.extensionattributes.login.flagged = false;
user.extensionattributes.login.attempts = 0;
user.extensionattributes.login.timestamp = moment().subtract(loginExtSettings.timeout.attempt_interval.time, loginExtSettings.timeout.attempt_interval.unit);
user.markModified('extensionattributes');
}
user.save(function (err, usr) {
if (err) {
cb(err, null);
}
cb(null, usr, req);
});
}
var getUser = function (req, res, next, cb) {
if (req.body.entitytype) {
User = mongoose.model(capitalize(req.body.entitytype));
}
User.findOne({
email: new RegExp('^' + req.body.email + '$', 'i'),
}, function (err, user) {
if (err) {
cb(err, null);
} else if (user) {
cb(false, user, req);
} else {
req.flash('error', 'No user with that email found!');
cb(new Error('No user with that email found.'), null);
}
});
};
var generateToken = function (user, req, cb) {
//Generate reset token and URL link; also, create expiry for reset token
//make sure attributes exists || create it via merge
var salt = bcrypt.genSaltSync(10);
var now = new Date();
var expires = new Date(now.getTime() + (loginExtSettings.token.resetTokenExpiresMinutes * 60 * 1000)).getTime();
user.attributes = user.attributes || {};
user.attributes.reset_token = encode({
email: user.email,
apikey: user.apikey,
});
user.attributes.reset_token_link = CoreUtilities.makeNiceName(bcrypt.hashSync(user.attributes.reset_token, salt));
user.attributes.reset_token_expires_millis = expires;
//TODO: Look into why mongoose properties
//are not being saved during async fn calls
user.markModified('attributes');
user.save(function (err) {
if (err) {
cb(err, null);
}
cb(null, user, req);
});
};
// create a func for the mail options
var emailForgotPasswordLink = function (user, req, cb) {
CoreController.getPluginViewDefaultTemplate({
viewname: 'email/user/forgot_password_link',
themefileext: appSettings.templatefileextension,
},
function (err, templatepath) {
if (err) {
cb(err);
} else {
// console.log('emailForgotPasswordLink templatepath', templatepath);
if (templatepath === 'email/user/forgot_password_link') {
templatepath = path.resolve(process.cwd(), 'node_modules/periodicjs.ext.login/views', templatepath + '.' + appSettings.templatefileextension);
}
// console.log('user for forgot password', user);
var coreMailerOptions = {
appenvironment: appenvironment,
to: user.email,
from: appSettings.fromemail || appSettings.adminnotificationemail,
replyTo: appSettings.fromemail || appSettings.adminnotificationemail,
subject: loginExtSettings.settings.forgotPasswordEmailSubject || appSettings.name + ' - Reset your password',
emailtemplatefilepath: templatepath,
emailtemplatedata: {
user: user,
appname: appSettings.name,
hostname: req.headers.host,
filename: templatepath,
adminPostRoute: req.adminPostRoute,
},
};
if (loginExtSettings.settings.adminbccemail || appSettings.adminbccemail) {
coreMailerOptions.bcc = loginExtSettings.settings.adminbccemail || appSettings.adminbccemail;
}
CoreMailer.sendEmail(coreMailerOptions, cb);
}
}
);
// cb(null, options);
};
var emailResetPasswordNotification = function (user, req, cb) {
CoreController.getPluginViewDefaultTemplate({
viewname: 'email/user/reset_password_notification',
themefileext: appSettings.templatefileextension,
},
function (err, templatepath) {
if (err) {
cb(err);
} else {
// console.log('user for forgot password', user);
if (templatepath === 'email/user/reset_password_notification') {
templatepath = path.resolve(process.cwd(), 'node_modules/periodicjs.ext.login/views', templatepath + '.' + appSettings.templatefileextension);
}
var coreMailerOptions = {
appenvironment: appenvironment,
to: user.email,
from: appSettings.fromemail || appSettings.adminnotificationemail,
replyTo: appSettings.fromemail || appSettings.adminnotificationemail,
subject: loginExtSettings.settings.forgotPasswordEmailNotificationSubject || appSettings.name + ' - Password reset notification',
emailtemplatefilepath: templatepath,
emailtemplatedata: {
user: user,
appname: appSettings.name,
hostname: req.headers.host,
filename: templatepath,
},
};
if (loginExtSettings.settings.adminbccemail || appSettings.adminbccemail) {
coreMailerOptions.bcc = loginExtSettings.settings.adminbccemail || appSettings.adminbccemail;
}
CoreMailer.sendEmail(coreMailerOptions, cb);
}
}
);
// cb(null, options);
};
//Post to auth/forgot with the users email
var forgot = function (req, res, next) {
let adminPostRoute = res.locals.adminPostRoute || 'auth';
req.adminPostRoute = res.locals.adminPostRoute || 'auth';
// console.log('res.locals.adminPostRoute',res.locals.adminPostRoute);
// console.log('req.adminPostRoute',req.adminPostRoute);
var arr = [
function (cb) {
cb(null, req, res, next);
},
getUser,
generateToken,
emailForgotPasswordLink,
];
waterfall(arr,
function (err, results) {
if (periodic.app.controller.extension.reactadmin) {
if (err) {
logger.debug('could not send reset email', err);
}
return res.status(200).send({
result:'success',
data: 'Password reset instructions were sent to your email address',
});
} else {
CoreController.respondInKind({
req: req,
res: res,
err: err,
responseData: results,
callback: function (req, res /*,responseData*/) {
if (err) {
req.flash('error', err.message);
res.redirect('/auth/forgot');
} else {
req.flash('info', 'Password reset instructions were sent to your email address');
if (req.controllerData && req.controllerData.sendemailstatus) {
req.controllerData.password_reset_emailstatus = results;
next();
} else {
res.redirect(loginExtSettings.settings.authLoginPath);
}
}
},
});
}
});
};
var asyncreset = function (req, res) {
let UserModel = Object.assign({}, User);
if (req.query.entitytype) {
UserModel = mongoose.model(capitalize(req.query.entitytype));
}
UserModel.findOne({
'attributes.reset_token_link': req.params.token,
}, function (err, user_with_token) {
if (err || !user_with_token) {
res.status(400).send({
result: 'error',
data: {
error: (err) ? err.toString() : 'Invalid token',
},
});
} else {
res.status(200).send({
result: 'success',
data: {
user: {
email: user_with_token.email,
username: user_with_token.username,
firstname: user_with_token.firstname,
lastname: user_with_token.lastname,
token: req.params.token,
},
},
});
}
});
};
var get_token = function (req, res, next) {
req.controllerData = (req.controllerData) ? req.controllerData : {};
if (periodic.app.controller.extension.reactadmin) {
let reactadmin = periodic.app.controller.extension.reactadmin;
next();
} else {
User.findOne({
'attributes.reset_token_link': req.params.token,
}, function (err, user_with_token) {
if (err) {
req.flash('error', err.message);
res.redirect(loginExtSettings.settings.authLoginPath);
} else if (!user_with_token || !user_with_token.attributes.reset_token) {
req.flash('error', 'Invalid reset token');
res.redirect(loginExtSettings.settings.authLoginPath);
} else if (hasExpired(user_with_token.attributes.reset_token_expires_millis)) {
req.flash('error', 'Password reset token is has expired.');
res.redirect(loginExtSettings.settings.authLoginPath);
} else {
req.controllerData.token = user_with_token.attributes.reset_token;
next();
}
});
}
};
//GET if the user token is vaild show the change password page
var reset = function (req, res, next) {
if (periodic.app.controller.extension.reactadmin) {
let reactadmin = periodic.app.controller.extension.reactadmin;
// console.log({ reactadmin });
// console.log('ensureAuthenticated req.session', req.session);
// console.log('ensureAuthenticated req.user', req.user);
next();
} else {
let adminPostRoute = res.locals.adminPostRoute || 'auth';
var token = req.controllerData.token,
// current_user,
decode_token;
decode(token, function (err, decode) {
if (err) {
CoreController.handleDocumentQueryErrorResponse({
err: err,
res: res,
req: req,
errorflash: err.message,
});
} else {
decode_token = decode;
//Find the User by their token
User.findOne({
'attributes.reset_token': token,
}, function (err, found_user) {
if (err || !found_user) {
req.flash('error', 'Password reset token is invalid.');
res.redirect(loginExtSettings.settings.authLoginPath);
}
// current_user = found_user;
//Check to make sure token hasn't expired
//Check to make sure token is valid and sign by us
else if (found_user.email !== decode_token.email && found_user.api_key !== decode_token.api_key) {
req.flash('error', 'This token is not valid please try again');
res.redirect(loginExtSettings.settings.authLoginPath);
} else {
CoreController.getPluginViewDefaultTemplate({
viewname: 'user/reset',
themefileext: appSettings.templatefileextension,
extname: 'periodicjs.ext.login',
},
function (err, templatepath) {
CoreController.handleDocumentQueryRender({
res: res,
req: req,
renderView: templatepath,
responseData: {
pagedata: {
title: 'Reset Password',
current_user: found_user,
},
user: req.user,
adminPostRoute: adminPostRoute,
},
});
});
}
});
}
});
}
};
//POST change the users old password to the new password in the form
var token = function (req, res, next) {
// console.log('req.body', req.body);
// console.log('req.params', req.params);
var user_token = req.params.token || req.body.token; //req.controllerData.token;
waterfall([
function (cb) {
cb(null, req, res, next);
},
invalidateUserToken,
resetPassword,
saveUser,
emailResetPasswordNotification,
],
function (err, results) {
if (periodic.app.controller.extension.reactadmin) {
let reactadmin = periodic.app.controller.extension.reactadmin;
if (err) {
res.status(400).send({
result: 'error',
data: {
error:err.toString(),
},
});
} else {
res.status(200).send({
result: 'success',
data:'Password successfully changed',
});
}
} else {
// console.log('These are the err', err);
// console.log('These are the results', results);
CoreController.respondInKind({
req: req,
res: res,
err: err,
responseData: results || {},
callback: function (req, res /*,responseData*/ ) {
// console.log('err',err,'/auth/reset/' + user_token);
if (err) {
// console.log('return to reset');
req.flash('error', err.message);
res.redirect('/auth/reset/' + user_token);
} else {
// console.log('no return to x');
req.flash('success', 'Password Sucessfully Changed!');
res.redirect(loginExtSettings.settings.authLoginPath);
}
},
});
}
});
};
var getTokenExpiresTime = function () {
var now = new Date();
return new Date(now.getTime() + (loginExtSettings.token.resetTokenExpiresMinutes * 60 * 1000)).getTime();
};
var update_user_activation_token = function (req, res, next) {
try {
if (!req.body.attributes.user_activation_token) {
throw Error('invalid user activation');
} else if (!req.isAuthenticated()) {
throw Error('must be logged in');
}
req.user.attributes = merge(req.user.attributes, req.body.attributes);
User.findOne({
'_id': req.user._id,
}, function (err, user_to_update) {
if (err) {
throw err;
} else if (!user_to_update || !user_to_update) {
throw Error('invalid user activation token');
} else {
user_to_update.attributes = req.user.attributes;
user_to_update.markModified('attributes');
user_to_update.save(function (err /*, usr */ ) {
if (err) {
next(err);
} else {
next();
}
});
}
});
} catch (e) {
CoreController.handleDocumentQueryErrorResponse({
err: e,
res: res,
req: req,
errorflash: e.message,
});
}
};
var get_user_activation_token = function (req, res, next) {
req.controllerData = (req.controllerData) ? req.controllerData : {};
User.findOne({
'attributes.user_activation_token_link': req.params.token,
}, function (err, user_with_activation_token) {
// console.log('user_with_activation_token', user_with_activation_token);
if (err) {
req.flash('error', err.message);
res.redirect(loginExtSettings.settings.authLoginPath);
} else if (!user_with_activation_token || !user_with_activation_token.attributes.user_activation_token) {
req.flash('error', 'Invalid validation token');
res.redirect(loginExtSettings.settings.authLoginPath);
} else if (hasExpired(user_with_activation_token.attributes.reset_activation_expires_millis)) {
req.flash('error', 'Activation token has expired.');
res.redirect(loginExtSettings.settings.authLoginPath);
} else {
req.controllerData.user_activation_token = user_with_activation_token.attributes.user_activation_token;
next();
}
});
};
var generate_activation_attributes = function (options, callback) {
var activationData = options.activationData;
try {
if (!activationData.email) {
throw new Error('you must provide an email address to activate an account');
}
var salt = bcrypt.genSaltSync(10),
expires = getTokenExpiresTime(),
user_activation_token = encode({
email: activationData.email,
});
activationData.attributes = activationData.attributes || {};
activationData.attributes.user_activation_token = user_activation_token;
activationData.attributes.user_activation_token_link = CoreUtilities.makeNiceName(bcrypt.hashSync(activationData.attributes.user_activation_token, salt));
activationData.attributes.reset_activation_expires_millis = expires;
callback(null, activationData);
} catch (e) {
callback(e, null);
}
};
var update_activation_attributes = function (options, callback) {
try {
var updatedActivationData = options.updatedActivationData;
User.findOne({
'_id': updatedActivationData._id,
}, function (err, user_to_update) {
if (err) {
callback(err);
} else if (!user_to_update || !user_to_update) {
callback(Error('Invalid user activation token'));
} else {
user_to_update.attributes = merge(user_to_update.attributes, updatedActivationData.attributes);
user_to_update.markModified('attributes');
user_to_update.save(function (err /*, usr */ ) {
if (err) {
callback(err);
} else {
callback(null, user_to_update);
}
});
}
});
} catch (e) {
callback(e);
}
};
// auth/user/new
var create_user_activation_token = function (req, res, next) {
try {
if (!req.body.email && !req.isAuthenticated()) {
throw new Error('you must be logged in, to activate your account');
}
var userdata = CoreUtilities.removeEmptyObjectValues(req.body),
salt = bcrypt.genSaltSync(10),
expires = getTokenExpiresTime(),
user_activation_token = encode({
email: userdata.email || req.user.email,
});
userdata.attributes = {};
userdata.attributes.user_activation_token = user_activation_token;
userdata.attributes.user_activation_token_link = CoreUtilities.makeNiceName(bcrypt.hashSync(userdata.attributes.user_activation_token, salt));
userdata.attributes.reset_activation_expires_millis = expires;
req.body = userdata;
next();
} catch (e) {
CoreController.handleDocumentQueryErrorResponse({
err: e,
res: res,
req: req,
errorflash: e.message,
});
}
};
var tokenController = function (resources, passportResources, UserModel) {
periodic = resources;
appSettings = resources.settings;
CoreController = resources.core.controller;
CoreUtilities = resources.core.utilities;
CoreMailer = resources.core.mailer;
loginExtSettings = passportResources.loginExtSettings;
logger = resources.logger;
mongoose = resources.mongoose;
passport = passportResources.passport;
User = UserModel || mongoose.model('User');
appenvironment = appSettings.application.environment;
return {
forgot: forgot,
reset: reset,
generate_activation_attributes: generate_activation_attributes,
update_activation_attributes: update_activation_attributes,
get_user_activation_token: get_user_activation_token,
get_token: get_token,
create_user_activation_token: create_user_activation_token,
update_user_activation_token: update_user_activation_token,
token: token,
asyncreset,
};
};
module.exports = tokenController;