├── .travis.yml ├── README.markdown ├── example └── web │ ├── forgot.html │ ├── index.html │ └── server.js ├── index.js ├── package.json └── test └── stack.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.4 4 | - 0.6 5 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | password-reset 2 | ============== 3 | 4 | middleware for managing password reset emails 5 | 6 | TODO: Update this README and example 7 | 8 | example 9 | ------- 10 | 11 | ``` js 12 | var fs = require('fs'); 13 | var express = require('express'); 14 | var app = express.createServer(); 15 | 16 | app.use(express.static(__dirname)); 17 | app.use(require('sesame')()); // for sessions 18 | 19 | // example nodemailer config here 20 | var forgot = require('../../')({ 21 | uri: 'http://localhost:8080/password_reset', 22 | from: 'password-robot@localhost', 23 | transportType: 'SMTP', 24 | transportOptions: { 25 | service: "Gmail", 26 | auth: { 27 | user: "youmailaccount@gmail.com", 28 | pass: "password" 29 | } 30 | } 31 | }); 32 | 33 | 34 | app.use(forgot.middleware); 35 | 36 | app.post('/forgot', express.bodyParser(), function(req, res) { 37 | var email = req.body.email; 38 | 39 | var callback = { 40 | error: function(err) { 41 | res.end('Error sending message: ' + err); 42 | }, 43 | success: function(success) { 44 | res.end('Check your inbox for a password reset message.'); 45 | } 46 | }; 47 | var reset = forgot(email, callback); 48 | 49 | reset.on('request', function(req_, res_) { 50 | req_.session.reset = { 51 | email: email, 52 | id: reset.id 53 | }; 54 | fs.createReadStream(__dirname + '/forgot.html').pipe(res_); 55 | }); 56 | }); 57 | 58 | app.post('/reset', express.bodyParser(), function(req, res) { 59 | if (!req.session.reset) return res.end('reset token not set'); 60 | 61 | var password = req.body.password; 62 | var confirm = req.body.confirm; 63 | if (password !== confirm) return res.end('passwords do not match'); 64 | 65 | // update the user db here 66 | 67 | forgot.expire(req.session.reset.id); 68 | delete req.session.reset; 69 | res.end('password reset'); 70 | }); 71 | 72 | app.listen(8080); 73 | console.log('Listening on :8080'); 74 | ``` 75 | 76 | methods 77 | ======= 78 | 79 | var forgot = require('password-reset')(opts) 80 | -------------------------------------------- 81 | 82 | Create a new password reset session `forgot` with some options `opts`. 83 | 84 | `opts.uri` must be the location of the password reset route, such as 85 | `'http://localhost:8080/_password_reset'`. A query string is appended to 86 | `opts.uri` with a unique one-time hash. 87 | 88 | `opts.body(uri)` can be a function that takes the password reset link `uri` and 89 | returns the email body as a string. 90 | 91 | The options `transportType` and `transportOptions` are passed directly to 92 | [nodemailer](https://github.com/andris9/Nodemailer). 93 | 94 | When the user clicks on the uri link `forgot` emits a `"request", req, res` 95 | event. 96 | 97 | var reset = forgot(email, cb) 98 | ----------------------------- 99 | 100 | Send a password reset email to the `email` address. 101 | `cb.error(error)` fires when the email sent got some error. 102 | `cb.success(success)` fires when the email has been sent. 103 | 104 | forgot.middleware(req, res, next) 105 | --------------------------------- 106 | 107 | Use this middleware function to intercept requests on the `opts.uri`. 108 | 109 | forgot.expire(id) 110 | ----------------- 111 | 112 | Prevent a session from being used again. Call this after you have successfully 113 | reset the password. 114 | 115 | attributes 116 | ========== 117 | 118 | reset.id 119 | -------- 120 | 121 | Pass this value to `forgot.expire(id)`. 122 | 123 | events 124 | ====== 125 | 126 | reset.on('request', function (req, res) { ... }) 127 | ------------------------------------------------ 128 | 129 | Emitted when the user clicks on the password link from the email. 130 | 131 | reset.on('failure', function (err) { ... }) 132 | ------------------------------------------- 133 | 134 | Emitted when an error occurs sending email. You can also listen for this event 135 | in `forgot()`'s callback. 136 | 137 | reset.on('success', function () {}) 138 | ----------------------------------- 139 | 140 | Emitted when an email is successfully sent. 141 | 142 | install 143 | ======= 144 | 145 | With [npm](http://npmjs.org) do: 146 | 147 | ``` 148 | npm install password-reset 149 | ``` 150 | 151 | license 152 | ======= 153 | 154 | MIT/X11 155 | 156 | credits to 157 | ========== 158 | Substack for the original module 159 | 160 | test 161 | ==== 162 | 163 | With [npm](http://npmjs.org), do: 164 | 165 | npm test 166 | -------------------------------------------------------------------------------- /example/web/forgot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | reset password 4 | 5 | 6 |
7 |
8 | new password 9 |
10 |
11 | 12 |
13 | 14 |
15 | confirm password 16 |
17 |
18 | 19 |
20 | 21 |
22 | 23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | password reset example 4 | 5 | 6 |
7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /example/web/server.js: -------------------------------------------------------------------------------- 1 | //TODO: Update example 2 | 3 | var fs = require('fs'); 4 | var express = require('express'); 5 | var app = express.createServer(); 6 | 7 | app.use(express.static(__dirname)); 8 | app.use(require('sesame')()); // for sessions 9 | 10 | // example nodemailer config here 11 | var forgot = require('../../')({ 12 | uri: 'http://localhost:8080/password_reset', 13 | from: 'password-robot@localhost', 14 | transportType: 'SMTP', 15 | transportOptions: { 16 | service: "Gmail", 17 | auth: { 18 | user: "youmailaccount@gmail.com", 19 | pass: "password" 20 | } 21 | } 22 | }); 23 | 24 | 25 | app.use(forgot.middleware); 26 | 27 | app.post('/forgot', express.bodyParser(), function(req, res) { 28 | var email = req.body.email; 29 | 30 | var callback = { 31 | error: function(err) { 32 | res.end('Error sending message: ' + err); 33 | }, 34 | success: function(success) { 35 | res.end('Check your inbox for a password reset message.'); 36 | } 37 | }; 38 | var reset = forgot(email, callback); 39 | 40 | reset.on('request', function(req_, res_) { 41 | req_.session.reset = { 42 | email: email, 43 | id: reset.id 44 | }; 45 | fs.createReadStream(__dirname + '/forgot.html').pipe(res_); 46 | }); 47 | }); 48 | 49 | app.post('/reset', express.bodyParser(), function(req, res) { 50 | if (!req.session.reset) return res.end('reset token not set'); 51 | 52 | var password = req.body.password; 53 | var confirm = req.body.confirm; 54 | if (password !== confirm) return res.end('passwords do not match'); 55 | 56 | // update the user db here 57 | 58 | forgot.expire(req.session.reset.id); 59 | delete req.session.reset; 60 | res.end('password reset'); 61 | }); 62 | 63 | app.listen(8080); 64 | console.log('Listening on :8080'); 65 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var url = require('url'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var mailer = require('nodemailer'); 4 | var ent = require('ent'); 5 | 6 | module.exports = function (opts) { 7 | if (typeof opts === 'string') { 8 | opts = { uri : opts }; 9 | } 10 | 11 | var transport; 12 | if (opts.transportType && opts.transportOptions) { 13 | transport = mailer.createTransport(opts.transportType, opts.transportOptions); 14 | } else { 15 | console.log("No transport type specified!"); 16 | } 17 | 18 | var reset = new Forgot(opts); 19 | 20 | var self = function (email, cb) { 21 | var session = reset.generate(); 22 | if (!session) return; 23 | 24 | var uri = session.uri = opts.uri + '?' + session.id; 25 | 26 | transport.sendMail({ 27 | sender : opts.from || 'nodepasswordreset@localhost', 28 | to : email, 29 | subject : opts.subject || 'Password reset request', 30 | text : opts.text || "", 31 | html : opts.html || [ 32 | 'Click this link to reset your password:\r\n', 33 | '
', 34 | '', 35 | ent.encode(uri), 36 | '', 37 | '' 38 | ].join('\r\n') 39 | }, function (error, success) { 40 | if (error) { 41 | if (cb.error) cb.error(error); 42 | delete reset.sessions[session.id]; 43 | } else { 44 | if(cb.success) cb.success(success) 45 | } 46 | }); 47 | 48 | return session; 49 | }; 50 | 51 | self.middleware = reset.middleware.bind(reset); 52 | 53 | self.expire = function (id) { 54 | delete reset.sessions[id]; 55 | }; 56 | 57 | return self; 58 | }; 59 | 60 | function Forgot (opts) { 61 | this.sessions = opts.sessions || {}; 62 | this.mount = url.parse(opts.uri); 63 | this.mount.port = this.mount.port || 80; 64 | } 65 | 66 | Forgot.prototype.generate = function () { 67 | var buf = new Buffer(16); 68 | for (var i = 0; i < buf.length; i++) { 69 | buf[i] = Math.floor(Math.random() * 256); 70 | } 71 | var id = buf.toString('base64'); 72 | 73 | var session = this.sessions[id] = new EventEmitter; 74 | session.id = id; 75 | return session; 76 | }; 77 | 78 | Forgot.prototype.middleware = function (req, res, next) { 79 | if (!next) next = function (err) { 80 | if (err) res.end(err) 81 | } 82 | 83 | var u = url.parse('http://' + req.headers.host + req.url); 84 | u.port = u.port || 80; 85 | var id = u.query; 86 | 87 | if (u.hostname !== this.mount.hostname 88 | || parseInt(u.port, 10) !== parseInt(this.mount.port, 10) 89 | || u.pathname !== this.mount.pathname) { 90 | next() 91 | } 92 | else if (!id) { 93 | res.statusCode = 400; 94 | next('No auth token specified.'); 95 | } 96 | else if (!this.sessions[id]) { 97 | res.statusCode = 410; 98 | next('auth token expired'); 99 | } 100 | else { 101 | this.sessions[id].emit('request', req, res); 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "password-reset-nodemailer", 3 | "description" : "Middleware for password reset emails using nodemailer", 4 | "version" : "0.0.1", 5 | "repository" : { 6 | "type" : "git", 7 | "url" : "git://github.com/sampepose/node-password-reset.git" 8 | }, 9 | "main" : "index.js", 10 | "keywords" : [ 11 | "middleware", 12 | "web", 13 | "password", 14 | "reset" 15 | ], 16 | "directories" : { 17 | "lib" : ".", 18 | "example" : "example" 19 | }, 20 | "dependencies" : { 21 | "nodemailer":"0.3.x", 22 | "ent" : "0.0.x" 23 | }, 24 | "devDependencies" : { 25 | "tap" : "0.1.x", 26 | "express" : "2.5.x", 27 | "sesame" : "0.1.x", 28 | "smtp-protocol" : "0.1.x", 29 | "request" : "2.9.x" 30 | }, 31 | "engines" : { 32 | "node" : ">=0.4.0" 33 | }, 34 | "scripts" : { 35 | "test" : "tap test/*.js" 36 | }, 37 | "license" : "MIT", 38 | "author" : { 39 | "name" : "Sam Pepose", 40 | "email" : "sampepose@gmail.com", 41 | "url" : "" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/stack.js: -------------------------------------------------------------------------------- 1 | var tap = require('tap'); 2 | var test = tap.test; 3 | 4 | var express = require('express'); 5 | var request = require('request'); 6 | var smtp = require('smtp-protocol'); 7 | var passwordReset = require('../'); 8 | 9 | var ports = { 10 | smtp : Math.floor(Math.random() * 5e5 + 1e5), 11 | http : Math.floor(Math.random() * 5e5 + 1e5), 12 | }; 13 | 14 | var server = smtp.createServer(function (req) { 15 | req.on('message', function (stream, ack) { 16 | var data = ''; 17 | stream.on('data', function (buf) { data += buf }); 18 | stream.on('end', function () { 19 | server.emit(req.to, data.match(/(http:\/\/[^"]+)/)[1]); 20 | }); 21 | ack.accept(); 22 | }); 23 | }); 24 | server.listen(ports.smtp); 25 | 26 | var app = express.createServer(); 27 | app.listen(ports.http); 28 | app.use(require('sesame')()); 29 | 30 | var forgot = passwordReset({ 31 | uri : 'http://localhost:' + ports.http + '/password_reset', 32 | from : 'password-robot@localhost', 33 | host : 'localhost', port : ports.smtp, 34 | }); 35 | app.use(forgot.middleware); 36 | 37 | app.post('/forgot', express.bodyParser(), function (req, res) { 38 | var email = req.body.email; 39 | 40 | var reset = forgot(email, function (err) { 41 | if (err) { 42 | res.statusCode = 500; 43 | res.end(err.toString()); 44 | } 45 | else res.end('sent') 46 | }); 47 | 48 | reset.on('request', function (req_, res_) { 49 | req_.session.reset = { email : email, id : reset.id }; 50 | res_.end('forgot'); 51 | }); 52 | }); 53 | 54 | app.post('/reset', express.bodyParser(), function (req, res) { 55 | res.statusCode = 401; 56 | if (!req.session.reset) return res.end('reset token not set'); 57 | 58 | var password = req.body.password; 59 | var confirm = req.body.confirm; 60 | if (password !== confirm) return res.end('passwords do not match'); 61 | 62 | // update the user db here 63 | 64 | forgot.expire(req.session.reset.id); 65 | delete req.session.reset; 66 | res.statusCode = 200; 67 | res.end('password reset'); 68 | }); 69 | 70 | test('reset success', function (t) { 71 | var uri = 'http://localhost:' + ports.http; 72 | var to = t.conf.name.replace(/ /g, '.') + '@localhost'; 73 | 74 | var opts = { 75 | uri : uri + '/forgot', 76 | headers : { 77 | 'content-type' : 'application/x-www-form-urlencoded', 78 | }, 79 | }; 80 | var cookie; 81 | var req = request.post(opts, function (e, r, b) { 82 | if (e) t.fail(e); 83 | 84 | cookie = r.headers['set-cookie']; 85 | t.equal(b, 'sent'); 86 | }); 87 | req.end('email=' + to); 88 | 89 | server.on(to, function (link) { 90 | request.get( 91 | { uri : link, headers : { cookie : cookie } }, 92 | function (e, r, b) { 93 | t.equal(b, 'forgot'); 94 | reset(function (e, b) { 95 | if (e) t.fail(e); 96 | t.equal(b, 'password reset'); 97 | reset(function (e, b) { 98 | if (e) t.fail(e); 99 | t.equal(b, 'reset token not set', 'expiry check'); 100 | t.end(); 101 | }); 102 | }); 103 | } 104 | ); 105 | }); 106 | 107 | function reset (cb) { 108 | request.post( 109 | { uri : uri + '/reset', headers : { cookie : cookie } }, 110 | function (e, r, b) { cb(e, b) } 111 | ); 112 | } 113 | }); 114 | 115 | test('invalid token', function (t) { 116 | var opts = { 117 | uri : 'http://localhost:' + ports.http + '/password_reset?beepboop==', 118 | } 119 | request(opts, function (e, r, b) { 120 | t.equal(b, 'auth token expired'); 121 | t.end(); 122 | }); 123 | }); 124 | 125 | tap.on('end', function () { 126 | app.close(); 127 | server.close(); 128 | }); 129 | --------------------------------------------------------------------------------