├── .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 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/example/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | password reset example
4 |
5 |
6 |
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 |
--------------------------------------------------------------------------------