Permalink
Browse files

init

  • Loading branch information...
1 parent b2e5c66 commit f37000778c85f69ae04f770d93196065d4c6427e @zemirco committed Oct 3, 2013
View
1 .npmignore
@@ -0,0 +1 @@
+test/
View
8 .travis.yml
@@ -0,0 +1,8 @@
+language: node_js
+node_js:
+ - "0.10"
+services: couchdb
+before_script:
+ - npm install -g grunt-cli
+ - curl -X PUT localhost:5984/test
+ - node ./test/createViews.js
View
32 Gruntfile.js
@@ -0,0 +1,32 @@
+
+module.exports = function(grunt) {
+
+ // Project configuration.
+ grunt.initConfig({
+ pkg: grunt.file.readJSON('package.json'),
+ jshint: {
+ options: {
+ expr: true
+ },
+ files: ['Gruntfile.js', 'index.js', 'test/*.js']
+ },
+ mochaTest: {
+ test: {
+ options: {
+ reporter: 'spec'
+ },
+ src: ['test/test.js']
+ }
+ }
+ });
+
+ // load tasks
+ grunt.loadNpmTasks('grunt-contrib-jshint');
+ grunt.loadNpmTasks('grunt-mocha-test');
+
+ // register tasks
+ grunt.registerTask('hint', ['jshint']);
+ grunt.registerTask('mocha', ['mochaTest']);
+ grunt.registerTask('default', ['jshint', 'mochaTest']);
+
+};
View
0 History.md
No changes.
View
218 index.js
@@ -0,0 +1,218 @@
+
+var path = require('path');
+var uuid = require('node-uuid');
+var bcrypt = require('bcrypt');
+
+module.exports = function(app, config) {
+
+ var adapter = require('lockit-' + config.db + '-adapter')(config);
+ var sendmail = require('lockit-sendmail')(config);
+
+ // set default route
+ var route = config.forgotPasswordRoute || '/forgot-password';
+
+ // GET /forgot-password
+ app.get(route, function(req, res) {
+ res.render(path.join(__dirname, 'views', 'get-forgot-password'), {
+ title: 'Forgot password'
+ });
+ });
+
+ // POST /forgot-password
+ app.post(route, function(req, response) {
+
+ var email = req.body.email;
+
+ var error = null;
+ // regexp from https://github.com/angular/angular.js/blob/master/src/ng/directive/input.js#L4
+ var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}$/;
+
+ // check for valid input
+ if (!email || !email.match(EMAIL_REGEXP)) {
+ response.status(403);
+ response.render(path.join(__dirname, 'views', 'get-forgot-password'), {
+ title: 'Forgot password',
+ error: 'Email is invalid'
+ });
+ return;
+ }
+
+ // looks like given email address has the correct format
+
+ // look for user in db
+ adapter.find('email', email, function(err, user) {
+ if (err) console.log(err);
+
+ // no user found -> pretend we sent an email
+ if (!user) {
+ response.render(path.join(__dirname, 'views', 'post-forgot-password'), {
+ title: 'Forgot password'
+ });
+ return;
+ }
+
+ // user found in db
+ // set old pw and hash to null
+ // send link with setting new password page
+ var token = uuid.v4();
+ delete user.hash;
+ user.pwdResetToken = token;
+
+ // set expiration date for password reset token
+ var now = new Date();
+ var tomorrow = now.setTime(now.getTime() + config.forgotPasswordTokenExpiration);
+
+ user.pwdResetTokenExpires = new Date(tomorrow);
+
+ // update user in db
+ adapter.update(user, function(err, res) {
+ if (err) console.log(err);
+
+ // send email with forgot password link
+ sendmail.forgotPassword(user.username, user.email, token, function(err, res) {
+ if (err) console.log(err);
+
+ // render success message
+ response.render(path.join(__dirname, 'views', 'post-forgot-password'), {
+ title: 'Forgot password'
+ });
+
+ });
+
+ });
+
+ });
+
+ });
+
+ // GET /forgot-password/:token
+ app.get(route + '/:token', function(req, res, next) {
+
+ // get token from url
+ var token = req.params.token;
+
+ // verify format of token
+ var re = new RegExp('[0-9a-f]{22}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', 'i');
+
+ // if format is wrong no need to query the database
+ if (!re.test(token)) return next();
+
+ // check if we have a user with that token
+ adapter.find('pwdResetToken', token, function(err, user) {
+ if (err) console.log(err);
+
+ // if no user is found forward to error handling middleware
+ if (!user) return next();
+
+ // check if token has expired
+ if (new Date(user.pwdResetTokenExpires) < new Date()) {
+
+ // make old token invalid
+ delete user.pwdResetToken;
+ delete user.pwdResetTokenExpires;
+
+ // update user in db
+ adapter.update(user, function(err, user) {
+ if (err) console.log(err);
+
+ // tell user that link has expired
+ res.render(path.join(__dirname, 'views', 'link-expired'), {
+ title: 'Forgot password - Link expired'
+ });
+
+ });
+
+ return;
+ }
+
+ // send token as local variable for POST request to right url
+ res.render(path.join(__dirname, 'views', 'get-new-password'), {
+ token: token,
+ title: 'Choose a new password'
+ });
+
+ });
+
+ });
+
+ // POST /forgot-password/:token
+ app.post(route + '/:token', function(req, res, next) {
+
+ var password = req.body.password;
+ var token = req.params.token;
+
+ // verify format of token
+ var re = new RegExp('[0-9a-f]{22}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', 'i');
+
+ // if format is wrong no need to query the database
+ if (!re.test(token)) return next();
+
+ // check for valid input
+ if (!password) {
+ res.status(403);
+ res.render(path.join(__dirname, 'views', 'get-forgot-password'), {
+ title: 'Choose a new password',
+ error: 'Please enter a password',
+ token: token
+ });
+ return;
+ }
+
+ // check for token in db
+ adapter.find('pwdResetToken', token, function(err, user) {
+ if (err) console.log(err);
+
+ // if no token is found forward to error handling middleware
+ if (!user) return next();
+
+ // check if token has expired
+ if (new Date(user.pwdResetTokenExpires) < new Date()) {
+
+ // make old token invalid
+ delete user.pwdResetToken;
+ delete user.pwdResetTokenExpires;
+
+ // update user in db
+ adapter.update(user, function(err, user) {
+ if (err) console.log(err);
+
+ // tell user that link has expired
+ res.render(path.join(__dirname, 'views', 'link-expired'), {
+ title: 'Forgot password - Link expired'
+ });
+
+ });
+
+ return;
+ }
+
+ // create hash for new password
+ bcrypt.hash(password, 10, function(err, hash) {
+ if (err) console.log(err);
+
+ // update user's credentials
+ user.hash = hash;
+
+ // remove helper properties
+ delete user.pwdResetToken;
+ delete user.pwdResetTokenExpires;
+
+ // update user in db
+ adapter.update(user, function(err, user) {
+ if (err) console.log(err);
+
+ // render success message
+ res.render(path.join(__dirname, 'views', 'change-password-success'), {
+ title: 'Password changed'
+ });
+
+ });
+
+ });
+
+
+ });
+
+ });
+
+};
View
39 package.json
@@ -0,0 +1,39 @@
+{
+ "name": "lockit-forgot-password",
+ "version": "0.0.1",
+ "description": "forgot password middleware for lockit",
+ "main": "index.js",
+ "scripts": {
+ "test": "grunt"
+ },
+ "author": {
+ "name": "Mirco Zeiss",
+ "email": "mirco.zeiss@gmail.com",
+ "twitter": "zeMirco"
+ },
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/zeMirco/lockit-forgot-account"
+ },
+ "keywords": [
+ "lockit",
+ "forgot",
+ "password"
+ ],
+ "dependencies": {
+ "node-uuid": "~1.4.1",
+ "bcrypt": "~0.7.7"
+ },
+ "devDependencies": {
+ "supertest": "~0.7.1",
+ "should": "~1.2.2",
+ "grunt": "~0.4.1",
+ "grunt-contrib-jshint": "~0.6.4",
+ "grunt-mocha-test": "~0.7.0",
+ "lockit-couchdb-adapter": "0.0.2",
+ "nano": "~4.1.1",
+ "express": "3.3.5",
+ "jade": "*"
+ }
+}
View
73 test/app.js
@@ -0,0 +1,73 @@
+
+/**
+ * Module dependencies.
+ */
+
+var express = require('express');
+var routes = require('./routes');
+var user = require('./routes/user');
+var http = require('http');
+var path = require('path');
+var fs = require('fs');
+
+// require delete account middleware
+//var config = require('./config.js');
+var forgotPassword = require('../index.js');
+
+function start(config) {
+
+ config = config || require('./config.js');
+
+ var app = express();
+
+// set basedir so views can properly extend layout.jade
+ app.locals.basedir = __dirname + '/views'; // comment out and error returns
+
+// all environments
+ app.set('port', process.env.PORT || 3000);
+ app.set('views', __dirname + '/views');
+ app.set('view engine', 'jade');
+ app.use(express.favicon());
+ app.use(express.bodyParser());
+ app.use(express.methodOverride());
+ app.use(express.cookieParser('your secret here'));
+ app.use(express.cookieSession());
+
+// set a dummy session for testing purpose
+ app.use(function(req, res, next) {
+ req.session.username = 'john';
+ next();
+ });
+
+// use forgot password middleware with testing options
+ forgotPassword(app, config);
+
+ app.use(app.router);
+ app.use(express.static(path.join(__dirname, 'public')));
+
+// development only
+ if ('development' == app.get('env')) {
+ app.use(express.errorHandler());
+ }
+
+ app.get('/', routes.index);
+ app.get('/users', user.list);
+
+ http.createServer(app).listen(app.get('port'), function(){
+ console.log('Express server listening on port ' + app.get('port'));
+ });
+
+ return app;
+
+}
+
+// export app for testing
+if(require.main === module){
+ // called directly
+ start();
+} else {
+ // required as a module -> from test file
+ module.exports = function(config) {
+ return start(config);
+ };
+}
View
43 test/config.js
@@ -0,0 +1,43 @@
+exports.appname = 'beachbapp';
+exports.url = 'http://localhost:3000';
+exports.port = 3000; // Todo: use port instead full url
+
+// email settings
+exports.emailType = 'Stub';
+exports.emailSettings = {
+ service: 'none',
+ auth: {
+ user: 'none',
+ pass: 'none'
+ }
+};
+
+// signup settings
+exports.signupRoute = '/signup';
+exports.signupTokenExpiration = 24 * 60 * 60 * 1000;
+
+// forgot password settings
+exports.forgotPasswordRoute = '/forgot-password';
+exports.forgotPasswordTokenExpiration = 24 * 60 * 60 * 1000;
+
+// settings for test
+exports.db = 'couchdb';
+exports.dbUrl = 'http://127.0.0.1:5984/test';
+
+// signup process -> resend email with verification link
+exports.emailResendVerification = {
+ title: 'Complete your registration at <%- appname %>',
+ text:
+ '<h2>Hello <%- username %></h2>' +
+ 'here is the link again. <%- link %> to complete your registration at <%- appname %>.' +
+ '<p>The <%- appname %> Team</p>'
+};
+
+// forgot password
+exports.emailForgotPassword = {
+ title: 'Reset your password',
+ text:
+ '<h2>Hey <%- username %></h2>' +
+ '<%- link %> to reset your password.' +
+ '<p>The <%- appname %> Team</p>'
+};
View
41 test/createViews.js
@@ -0,0 +1,41 @@
+
+var config = require('./config.js');
+
+// create couchdb templates
+var db = require('nano')({
+ url: config.dbUrl,
+ request_defaults: config.request_defaults
+});
+
+// couchdb views we need to make the app work
+var users = {
+ _id: '_design/users',
+ views: {
+ username: {
+ map: function(doc) {
+ emit(doc.username, doc);
+ }
+ },
+ email: {
+ map: function(doc) {
+ emit(doc.email, doc);
+ }
+ },
+ signupToken: {
+ map: function(doc) {
+ emit(doc.signupToken, doc);
+ }
+ },
+ pwdResetToken: {
+ map: function(doc) {
+ emit(doc.pwdResetToken, doc);
+ }
+ }
+ }
+};
+
+// save views to db
+db.insert(users, function(err, body) {
+ if (err) console.log(err);
+ console.log('done');
+});
View
12 test/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "application-name",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "start": "node app.js"
+ },
+ "dependencies": {
+ "express": "3.3.5",
+ "jade": "*"
+ }
+}
View
8 test/public/stylesheets/style.css
@@ -0,0 +1,8 @@
+body {
+ padding: 50px;
+ font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
+}
+
+a {
+ color: #00B7FF;
+}
View
8 test/routes/index.js
@@ -0,0 +1,8 @@
+
+/*
+ * GET home page.
+ */
+
+exports.index = function(req, res){
+ res.render('index', { title: 'Express' });
+};
View
8 test/routes/user.js
@@ -0,0 +1,8 @@
+
+/*
+ * GET users listing.
+ */
+
+exports.list = function(req, res){
+ res.send("respond with a resource");
+};
View
258 test/test.js
@@ -0,0 +1,258 @@
+
+var request = require('supertest');
+var should = require('should');
+var uuid = require('node-uuid');
+
+var config = require('./config.js');
+var app = require('./app.js')(config);
+
+// clone config object
+var configAlt = JSON.parse(JSON.stringify(config));
+// set some custom properties
+configAlt.port = 4000;
+configAlt.forgotPasswordTokenExpiration = 10;
+var appAlt = require('./app.js')(configAlt);
+
+var adapter = require('lockit-' + config.db + '-adapter')(config);
+
+// add a dummy user to db
+before(function(done) {
+ adapter.save('john', 'john@email.com', 'password', function(err, user) {
+ if (err) console.log(err);
+ adapter.save('steve', 'steve@email.com', 'password', function(err, user) {
+ if (err) console.log(err);
+ done();
+ });
+
+ });
+});
+
+// start the test
+describe('forgot-password', function() {
+
+ describe('GET /forgot-password', function() {
+
+ it('should use the default route when none is specified', function(done) {
+ request(app)
+ .get('/forgot-password')
+ .end(function(err, res) {
+ res.statusCode.should.equal(200);
+ res.text.should.include('Enter your email address here and we\'ll send you an email with a link');
+ res.text.should.include('<title>Forgot password</title>');
+ done();
+ });
+ });
+
+ });
+
+ describe('POST /forgot-password', function() {
+
+ it('should return an error when email has invalid format', function(done) {
+ request(app)
+ .post('/forgot-password')
+ .send({email: 'johnwayne.com'})
+ .end(function(error, res) {
+ res.statusCode.should.equal(403);
+ res.text.should.include('Email is invalid');
+ done();
+ });
+ });
+
+ it('should render a success message when no user was found', function(done) {
+ request(app)
+ .post('/forgot-password')
+ .send({email: 'jim@wayne.com'})
+ .end(function(error, res) {
+ res.statusCode.should.equal(200);
+ res.text.should.include('Email with link for password reset sent.');
+ res.text.should.include('<title>Forgot password</title>');
+ done();
+ });
+ });
+
+ it('should render a success message when email was sent', function(done) {
+ request(app)
+ .post('/forgot-password')
+ .send({email: 'john@email.com'})
+ .end(function(error, res) {
+ res.statusCode.should.equal(200);
+ res.text.should.include('Email with link for password reset sent.');
+ res.text.should.include('<title>Forgot password</title>');
+ done();
+ });
+ });
+
+ });
+
+ describe('GET /forgot-password/:token', function() {
+
+ it('should forward to error handling middleware when token has invalid format', function(done) {
+ request(app)
+ .get('/forgot-password/some-test-token-123')
+ .end(function(err, res) {
+ res.statusCode.should.equal(404);
+ res.text.should.include('Cannot GET /forgot-password/some-test-token-123');
+ done();
+ });
+ });
+
+ it('should forward to error handling middleware when no user for token is found', function(done) {
+ var token = uuid.v4();
+ request(app)
+ .get('/forgot-password/' + token)
+ .end(function(err, res) {
+ res.statusCode.should.equal(404);
+ res.text.should.include('Cannot GET /forgot-password/' + token);
+ done();
+ });
+ });
+
+ it('should render the link expired template when token has expired', function(done) {
+
+ // create token
+ request(appAlt)
+ .post('/forgot-password')
+ .send({email: 'steve@email.com'})
+ .end(function(error, res) {
+
+ // get token from db
+ adapter.find('username', 'steve', function(err, user) {
+ if (err) console.log(err);
+
+ // use GET request
+ request(app)
+ .get('/forgot-password/' + user.pwdResetToken)
+ .end(function(err, res) {
+ res.statusCode.should.equal(200);
+ res.text.should.include('This link has expired');
+ res.text.should.include('<title>Forgot password - Link expired</title>');
+ done();
+ });
+ });
+
+ });
+
+ });
+
+ it('should render a form to enter the new password', function(done) {
+
+ // get valid token from db
+ adapter.find('username', 'john', function(err, user) {
+ if (err) console.log(err);
+
+ // make request with valid token
+ request(app)
+ .get('/forgot-password/' + user.pwdResetToken)
+ .end(function(err, res) {
+ res.statusCode.should.equal(200);
+ res.text.should.include('Create a new password');
+ res.text.should.include('<title>Choose a new password</title>');
+ done();
+ });
+ });
+
+ });
+
+ });
+
+ describe('POST /forgot-password/:token', function() {
+
+ it('should return with an error message when password is empty', function(done) {
+ var token = uuid.v4();
+ request(app)
+ .post('/forgot-password/' + token)
+ .send({password: ''})
+ .end(function(err, res) {
+ res.statusCode.should.equal(403);
+ res.text.should.include('Please enter a password');
+ res.text.should.include('<title>Choose a new password</title>');
+ done();
+ });
+ });
+
+ it('should forward to error handling middleware when token has invalid format', function(done) {
+ request(app)
+ .post('/forgot-password/some-test-token-123')
+ .send({password: 'new Password'})
+ .end(function(err, res) {
+ res.statusCode.should.equal(404);
+ res.text.should.include('Cannot POST /forgot-password/some-test-token-123');
+ done();
+ });
+ });
+
+ it('should forward to error handling middleware when no user for token is found', function(done) {
+ var token = uuid.v4();
+ request(app)
+ .post('/forgot-password/' + token)
+ .send({password: 'new Password'})
+ .end(function(err, res) {
+ res.statusCode.should.equal(404);
+ res.text.should.include('Cannot POST /forgot-password/' + token);
+ done();
+ });
+ });
+
+ it('should render the link expired template when token has expired', function(done) {
+
+ // create token
+ request(appAlt)
+ .post('/forgot-password')
+ .send({email: 'steve@email.com'})
+ .end(function(error, res) {
+
+ // get token from db
+ adapter.find('username', 'steve', function(err, user) {
+ if (err) console.log(err);
+
+ // use token from db for POST request
+ request(app)
+ .post('/forgot-password/' + user.pwdResetToken)
+ .send({password: 'something'})
+ .end(function(err, res) {
+ res.statusCode.should.equal(200);
+ res.text.should.include('This link has expired');
+ res.text.should.include('<title>Forgot password - Link expired</title>');
+ done();
+ });
+ });
+
+ });
+
+ });
+
+ it('should render a success message when everything is fine', function(done) {
+
+ // get token from db
+ adapter.find('username', 'john', function(err, user) {
+ if (err) console.log(err);
+
+ // use token to make proper request
+ request(app)
+ .post('/forgot-password/' + user.pwdResetToken)
+ .send({password: 'new Password'})
+ .end(function(err, res) {
+ res.statusCode.should.equal(200);
+ res.text.should.include('You have successfully changed your password');
+ res.text.should.include('<title>Password changed</title>');
+ done();
+ });
+ });
+ });
+
+ });
+
+});
+
+// remove user from db
+after(function(done) {
+
+ adapter.delete('username', 'john', function(err) {
+ if (err) console.log(err);
+ adapter.delete('username', 'steve', function(err) {
+ if (err) console.log(err);
+ done();
+ });
+ });
+
+});
View
5 test/views/index.jade
@@ -0,0 +1,5 @@
+extends layout
+
+block content
+ h1= title
+ p Welcome to #{title}
View
7 test/views/layout.jade
@@ -0,0 +1,7 @@
+doctype 5
+html
+ head
+ title= title
+ link(rel='stylesheet', href='/stylesheets/style.css')
+ body
+ block content
View
6 views/change-password-success.jade
@@ -0,0 +1,6 @@
+extend /layout
+
+block content
+ div.panel.panel-success
+ div.panel-heading Password changed
+ div.panel-body You have successfully changed your password. Use your new credentials to login.
View
14 views/get-forgot-password.jade
@@ -0,0 +1,14 @@
+extend /layout
+
+block content
+ div.panel.panel-default
+ div.panel-heading Forgot password
+ div.panel-body
+ p Enter your email address here and we'll send you an email with a link that will let you reset your password.
+ form(action="/forgot-password", method="POST")
+ div.form-group
+ label(for="email") Email
+ input.form-control(type="email", id="email", name="email", placeholder="Your email")
+ if error
+ div.alert.alert-warning #{error}
+ input(type="submit", class="btn btn-primary", value="Submit")
View
13 views/get-new-password.jade
@@ -0,0 +1,13 @@
+extend /layout
+
+block content
+ div.panel.panel-default
+ div.panel-heading Create a new password
+ div.panel-body
+ form(action="/forgot-password/#{token}", method="POST")
+ div.form-group
+ label(for="password") Password
+ input.form-control(type="password", id="password", name="password", placeholder="New password")
+ if error
+ div.alert.alert-warning #{error}
+ input(type="submit", class="btn btn-primary", value="Submit")
View
8 views/link-expired.jade
@@ -0,0 +1,8 @@
+extend /layout
+
+block content
+ div.panel.panel-default
+ div.panel-heading Forgot password
+ div.panel-body This link has expired. If you forgot your password click
+ a(href="/forgot-password") here
+ | .
View
6 views/post-forgot-password.jade
@@ -0,0 +1,6 @@
+extend /layout
+
+block content
+ div.panel.panel-default
+ div.panel-heading Forgot password
+ div.panel-body Email with link for password reset sent. Please check your inbox.

0 comments on commit f370007

Please sign in to comment.