UNPKG

decentralized-internet

Version:

An NPM library of programs to create decentralized web and distributed computing projects

459 lines (376 loc) 11 kB
module.exports = function (server, conf) { var Promise = require('bluebird'); var jwt = require('jsonwebtoken'); var bcrypt = require('bcrypt'); const saltRounds = conf.saltRounds; var Boom = require('@hapi/boom'); var _ = require('underscore'); var nodemailer = require('nodemailer'); var couchprovider = require('couch-provider').couchProvider; couchprovider.setConfiguration(conf.userdb); var couchUpdateViews = require('couch-update-views'); var path = require('path'); var transporter; if(conf.mailer.nodemailer === 'nodemailer-stub-transport'){ transporter = nodemailer.createTransport(require(conf.mailer.nodemailer)()); }else{ transporter = nodemailer.createTransport(conf.mailer.nodemailer); } transporter.verify(function(error, success) { if (error) { console.log(error); } }); couchUpdateViews.migrateUp(couchprovider.getCouchDBServer(), path.join(__dirname, 'views'), true) .catch(function(err){ return couchUpdateViews.migrateUp(couchprovider.getCouchDBServer(), path.join(__dirname, 'views')); }) .then(function(){ return couchprovider.getView("_design/user/_view/scopes"); }) .then(function(res){ if(res && res.length === 0){ console.log("Generating default scopes..."); return couchprovider.uploadDocuments({ type: 'scopes', scopes:['default','admin'] }) .then(function(){ console.log("Done!"); }); } }) .catch(function(err){ console.error(err); }); var handler = {}; handler.validationFunctions = []; const addValidationFunction = function(valFunc){ handler.validationFunctions.push(valFunc); } server.method({ name: 'jwtauth.addValidationFunction', method: addValidationFunction, options: {} }); const validate = async function (req, decodedToken) { return handler.validateUser(req, decodedToken) .then(function(res){ return res; }) .catch(function(err){ return Promise.any(_.map(handler.validationFunctions, function(valFunc){ return valFunc(req, decodedToken); })) .then(function(res){ return res; }) .catch(Promise.AggregateError, function(err) { return Promise.reject(Boom.unauthorized(err)); }); }); } if(conf.algorithms){ console.log("Please modify your configuration. Instead of field 'algorithms' change to 'verifyOptions'"); server.auth.strategy('token', 'jwt', { key: conf.privateKey, validateFunc: validate, verifyOptions: conf.algorithms // only allow HS256 algorithm }); }else{ server.auth.strategy('token', 'jwt', { key: conf.privateKey, validateFunc: validate, verifyOptions: conf.verifyOptions // only allow HS256 algorithm }); } const sign = function(data, algorithm){ var token = {}; var algo; if(algorithm){ algo = algorithm; }else{ algo = conf.algorithm; } token.token = jwt.sign(data, conf.privateKey, algo ); return token; } server.method({ name: 'jwtauth.sign', method: sign, options: {} }); const verify = function(token){ try{ var decodedToken = jwt.verify(token, conf.privateKey); return validateUser(decodedToken); }catch(e){ throw Boom.unauthorized(e); } } server.method({ name: 'jwtauth.verify', method: verify, options: {} }); server.method({ name: 'jwt.verify', method: function(token){ return jwt.verify(token, conf.privateKey); }, options: {} }); const validateUser = function(decodedToken){ return couchprovider.getView('_design/user/_view/info?key=' + JSON.stringify(decodedToken.email)) .then(function(info){ var info = _.pluck(info, "value"); if(info.length > 1){ throw Boom.unauthorized("More than 1 user with same email found in DB!"); }else if(info.length === 0){ throw Boom.unauthorized("User not found in db") } return info[0]; }); } handler.validateUser = function(req, decodedToken){ return validateUser(decodedToken); } const bcryptHash = function(password){ return new Promise(function(resolve, reject){ bcrypt.hash(password, conf.saltRounds, function(err, hash) { if(err){ reject(err); }else{ resolve(hash); } }); }); } handler.createUser = function(req, rep){ var email = req.payload.email; var password = req.payload.password; return couchprovider.getView('_design/user/_view/info?key=' + JSON.stringify(email)) .then(function(info){ var info = _.pluck(info, "value"); if(info.length === 0){ return bcryptHash(password); }else{ throw Boom.conflict("User already exists"); } }) .then(function(hash){ var user = req.payload; user.password = hash; user.type = 'user'; user.scope = ['default']; return couchprovider.uploadDocuments(user) .then(function(res){ res = res[0]; if(res.ok){ return server.methods.jwtauth.sign({ email: user.email }); }else{ throw Boom.badData(res); } }); }) .catch(function(err){ return err; }); } handler.getUser = function(req, rep){ var credentials = req.auth.credentials; return credentials; } handler.getUsers = function(req, rep){ return couchprovider.getView('_design/user/_view/info') .then(function(info){ return _.pluck(info, 'value'); }) .catch(function(err){ return Boom.badImplementation(err); }) } handler.updateUser = function(req, rep){ var user = req.auth.credentials; var updateinfo = req.payload; if(user.email !== updateinfo.email && user.scope.indexOf('admin') === -1){ return (Boom.unauthorized('You cannot modify the user information')); }else{ return couchprovider.getDocument(updateinfo._id) .then(function(userindb){ updateinfo.password = userindb.password; if(updateinfo.scope === undefined){ updateinfo.scope = userindb.scope; } return couchprovider.uploadDocuments(updateinfo) .then(function(res){ res = res[0]; if(res.ok){ return res; }else{ throw res; } }) }) .catch(function(err){ return Boom.badImplementation(err); }); } } handler.updateUserBasic = function(req, rep){ var user = req.auth.credentials; var updateinfo = req.payload; if(user.email !== updateinfo.email || user._id !== updateinfo._id){ return (Boom.unauthorized('You cannot modify the user information')); }else{ return couchprovider.getDocument(updateinfo._id) .then(function(userindb){ updateinfo.password = userindb.password; updateinfo.scope = userindb.scope; return couchprovider.uploadDocuments(updateinfo) .then(function(res){ res = res[0]; if(res.ok){ return res; }else{ throw res; } }) }) .catch(function(err){ return Boom.badImplementation(err); }); } } handler.login = function(req, rep){ var email = req.payload.email; var password = req.payload.password; return couchprovider.getView('_design/user/_view/hash?key=' + JSON.stringify(email)) .then(function(hash){ return _.pluck(hash, "value")[0]; }) .then(function(hash){ return new Promise(function(resolve, reject){ bcrypt.compare(password, hash, function(err, res) { if(res){ resolve(server.methods.jwtauth.sign({ email: email })); }else{ reject(err); } }); }); }) .catch(function(err){ return (Boom.unauthorized(err)); }); } handler.loginUpdate = function(req, rep){ var credentials = req.auth.credentials; var email = credentials.email; var password = req.payload.password; return couchprovider.getDocument(credentials._id) .then(function(user){ return bcryptHash(password) .then(function(hash){ user.password = hash; return couchprovider.uploadDocuments(user) .then(function(res){ res = res[0]; if(res.ok){ return server.methods.jwtauth.sign({ email: user.email }); }else{ return Boom.badImplementation(res); } }); }) }) .catch(function(err){ return (Boom.unauthorized(err)); }); } handler.deleteUser = function(req, rep){ var credentials = req.auth.credentials; var user = req.auth.credentials; if(req.payload){ user = req.payload; } if(user.email !== credentials.email && credentials.scope.indexOf('admin') === -1){ return (Boom.unauthorized('You cannot delete the user')); }else{ return couchprovider.deleteDocument(user) .then(rep) .catch(function(err){ return (Boom.conflict(err)); }); } } handler.resetPassword = function(req, rep){ var email = req.payload.email; return couchprovider.getView('_design/user/_view/info?key=' + JSON.stringify(email)) .then(function(info){ var info = _.pluck(info, "value"); if(info.length === 0){ throw Boom.unauthorized("I don't know who you are, you need to create an account first!"); } info = info[0]; var algorithm = _.clone(conf.algorithm); algorithm.expiresIn = "30m"; var token = server.methods.jwtauth.sign({ email: info.email }, algorithm); var message = "Hello @USERNAME@,<br>Somebody asked me to send you a link to reset your password, hopefully it was you.<br>Follow this <a href='@SERVER@/public/#/login/reset?token=@TOKEN@'>link</a> to reset your password.<br>The link will expire in 30 minutes.<br>Bye."; if(conf.mailer.message){ message = conf.mailer.message; } var uri = server.info.uri; if(conf.mailer.uri){ uri = conf.mailer.uri; } message = message.replace("@USERNAME@", info.name); message = message.replace("@SERVER@", uri); message = message.replace("@TOKEN@", token.token); var mailOptions = { from: conf.mailer.from, to: email, subject: 'Password reset', // Subject line html: message }; return new Promise(function(resolve, reject){ transporter.sendMail(mailOptions, function(error, info){ if(error){ reject(Boom.badImplementation(error)); }else{ resolve("An email has been sent to recover your password."); } }); }) }) .catch(function(err){ console.log(err) return (Boom.wrap(err)); }); } handler.getScopes = function(req, rep){ return couchprovider.getView('_design/user/_view/scopes?include_docs=true') .then(function(info){ return (_.pluck(info, 'doc')); }) .catch(function(err){ return (Boom.badImplementation(err)); }) } handler.updateScopes = function(req, rep){ var user = req.auth.credentials; var updateinfo = req.payload; if(user.scope.indexOf('admin') === -1){ return (Boom.unauthorized('You cannot modify the scopes')); }else{ return couchprovider.uploadDocuments(req.payload) .then(function(res){ return (res[0]); }) .catch(function(e){ return (Boom.wrap(e)); }); } } return handler; }