UNPKG

activator

Version:

simple user activation and password reset for nodejs

327 lines (321 loc) 9.23 kB
/*jslint node:true, nomen:true */ /*jshint unused:vars */ // with defaults var async = require('async'), smtp = require('./mailer'), _ = require('lodash'), mailer, attachments, rparam = require('./params'), jwt = require('jsonwebtoken'), signkey, getObjectProperty = function(object, property) { var propertyPath = property.split('.'), current = object; while (propertyPath.length > 0) { current = current[propertyPath.shift()]; if (!current) {return;} } return current; }, DEFAULTS = { model: {find: function(user,cb){cb("uninitialized");}, save: function(id,data,cb){cb("uninitialized");}, generate: null }, transport: "smtp://localhost:465/activator.net/", resetExpire: 60, proto: "https://", emailProperty: "email", from: "help@activator.net", styliner: false, attachments: {}, idProperty: "id", sendPasswordResetComplete: false, mailHeaders: function(type, lang) { return null; } }, model = DEFAULTS.model, transport, from, templates, emailProperty, idProperty, sendPasswordResetComplete, resetExpire, proto, getAuthCode = function (req) { // first check for Authorization header var ret, header = req.headers.Authorization || req.headers.authorization, lparam = req.param("authorization"), uparam = req.param("Authorization"), bearer; if (header) { bearer = (header.match(/^Bearer\s+(\S+)$/) || [])[1]; } ret = bearer || lparam || uparam; return ret; }, createActivate = function (req,done) { // add the activation code using JSON Web Tokens var id = (req.activator?req.activator.id:null) || (req.user?req.user.id:null); if (!id) { done(500,"uninitialized"); } else { async.waterfall([ function(cb) { model.find(id,cb); }, function(res,cb){ if (!res) { cb(404); } else { var email = getObjectProperty(res, emailProperty), id = getObjectProperty(res,idProperty), code = jwt.sign({sub:email,purpose:"activation"},signkey,{algorithm:"HS256"}); if (!email) { cb("missingemail"); } else if (!id) { cb(404); } else { mailer("activate",req.lang||"en_US",{code:code,authentication:code,email:email,id:id,request:req},from,email,attachments.activate,cb); } } } ],function (err) { var code = 400; if (err) { if (err === 404) { code = 404; } else if (err === "uninitialized") { code = 500; } done(code,err); } else { done(201,req.activator?req.activator.body:undefined); } }); } }, completeActivate = function (req,done) { var code = getAuthCode(req), id = req.param("user"); async.waterfall([ function (cb) {model.find(id,cb);}, function (res,cb) { if (!res) { cb(404); } else { try { var decoded = jwt.verify(code,signkey,{algorithms:"HS256"}); if (decoded.purpose !== "activation") { throw new Error("invalid purpose"); } if (decoded.sub !== getObjectProperty(res, emailProperty)) { throw new Error("invalidactivationcode"); } model.activate(idProperty?getObjectProperty(res, idProperty):id,cb); } catch (e) { cb("invalidcode"); } } } ],function (err) { var code = 400; if (err) { if (err === 404) { code = 404; } else if (err === "uninitialized") { code = 500; } done(code,err); } else { done(200); } }); }, createPasswordReset = function (req,done) { /* * process: * 1) get the user by email * 2) create a random reset code * 3) save it * 4) send an email */ async.waterfall([ function (cb) {model.find(req.param("user"),cb);}, function (res,cb) { if (!res || res.length < 1) { cb(404); } else { var email = getObjectProperty(res, emailProperty), id = idProperty?getObjectProperty(res, idProperty):res.id, reset_code = jwt.sign({sub:email,purpose:"resetpassword"},signkey,{algorithm:"HS256"}); mailer("passwordreset",req.lang||"en_US",{code:reset_code,authorization:reset_code,email:email,id:id,request:req},from,email,attachments.passwordreset,cb); } } ],function (err) { var code = 400; if (err) { if (typeof(err) === 'number') { code = err; } else if (err === "uninitialized" || err === "baddb") { code = 500; } done(code,err); } else { done(201); } }); }, completePasswordReset = function (req,done) { var reset_code = getAuthCode(req), id = req.param("user"), user, now = Math.floor(new Date().getTime()/1000), newpass; async.waterfall([ function (cb) {model.find(id,cb);}, function (res,cb) { var password; if (!res) { cb(404); } else { user = res; /* * Generate a password for the given user if function provided */ password = typeof(model.generate) === "function" ? model.generate() : req.param("password"); if (!password) { cb("missingpassword"); } else { cb(null,res,password); } } }, function (res,password,cb) { if (typeof(model.validatePassword) === "function") { // validate the password var args = model.validatePassword.length; // first determine if this is synchronous or async if (args === 1 && model.validatePassword(password) !== true) { /* Password fails validation */ cb("badpassword"); } else if (args === 2) { model.validatePassword(password,function (err,data) { if (err) { cb(err); } else if (data !== true) { cb("badpassword"); } else { cb(null,res,password); } }); } else { cb(null,res,password); } } else { cb(null,res,password); } }, function (res,password,cb) { try { var decoded = jwt.verify(reset_code,signkey,{algorithms:"HS256"}); if (decoded.purpose !== "resetpassword" || decoded.sub !== getObjectProperty(res, emailProperty)) { throw new Error("invalidresetcode"); } else if (decoded.iat < now - resetExpire*60) { throw new Error("expiredresetcode"); } newpass = password; model.setPassword(idProperty?getObjectProperty(res, idProperty):id,password,cb); } catch (e) { cb(e); } }, function (d,cb) { if (sendPasswordResetComplete) { mailer("passwordresetcomplete",req.lang||"en_US",{email:user.email,id:id,password:newpass,request:req},from,user.email,attachments.passwordresetcomplete,cb); } else { cb(null); } } ],function (err, result) { var code = 400; if (err) { if (err === 404) { code = 404; } else if (err === "uninitialized") { code = 500; } done(code,err); } else { done(200); } }); }; module.exports = { init: function (config) { model = config.user || DEFAULTS.model; transport = config.transport || DEFAULTS.transport; templates = config.templates || function(type,lang,callback){callback(null);}; mailHeaders = config.mailHeaders || DEFAULTS.mailHeaders; resetExpire = config.resetExpire || DEFAULTS.resetExpire; proto = config.protocol || DEFAULTS.proto; mailer = smtp(transport, mailHeaders, templates, config.styliner || DEFAULTS.styliner); attachments = config.attachments || DEFAULTS.attachments; emailProperty = config.emailProperty || DEFAULTS.emailProperty; from = config.from || DEFAULTS.from; idProperty = config.id || DEFAULTS.idProperty; signkey = config.signkey; sendPasswordResetComplete = config.sendPasswordResetComplete || DEFAULTS.sendPasswordResetComplete; }, createPasswordReset: function (req,res,next) { rparam(req); createPasswordReset(req,function (code,message) { if (message === null || message === undefined || (typeof(message) === "number" && message === code)) { res.sendStatus(code); } else { res.status(code).send(message); } }); }, createPasswordResetNext: function (req,res,next) { rparam(req); createPasswordReset(req,function (code,message) { req.activator = req.activator || {}; _.extend(req.activator,{code:code,message:message}); next(); }); }, completePasswordReset: function (req,res,next) { rparam(req); completePasswordReset(req,function (code,message) { res.status(code).send(message); }); }, completePasswordResetNext: function (req,res,next) { rparam(req); completePasswordReset(req,function (code,message) { req.activator = req.activator || {}; _.extend(req.activator,{code:code,message:message}); next(); }); }, createActivate: function (req,res,next) { rparam(req); createActivate(req,function (code,message) { if (message === null || message === undefined || (typeof(message) === "number" && message === code)) { res.sendStatus(code); } else { res.status(code).send(message); } }); }, createActivateNext: function (req,res,next) { rparam(req); createActivate(req,function (code,message) { req.activator = req.activator || {}; _.extend(req.activator,{code:code,message:message}); next(); }); }, completeActivate: function (req,res,next) { rparam(req); completeActivate(req,function (code,message) { res.status(code).send(message); }); }, completeActivateNext: function (req,res,next) { rparam(req); completeActivate(req,function (code,message) { req.activator = req.activator || {}; _.extend(req.activator,{code:code,message:message}); next(); }); }, templates: { file: require('./filesdriver') } };