]> git.example.dev Git - binbsis50.git/commitdiff
added ability for registered users to reset their forgotten password
authorLuigi Pinca <luigipinca@gmail.com>
Sun, 1 Jul 2012 08:00:28 +0000 (10:00 +0200)
committerLuigi Pinca <luigipinca@gmail.com>
Sun, 1 Jul 2012 08:00:28 +0000 (10:00 +0200)
13 files changed:
app.js
config.json
lib/email/mailer.js [new file with mode: 0644]
lib/email/template.jade [new file with mode: 0644]
lib/email/template.txt [new file with mode: 0644]
package.json
public/static/css/style.css
routes/site.js
routes/user.js
views/changepasswd.jade
views/login.jade
views/recoverpasswd.jade [new file with mode: 0644]
views/resetpasswd.jade [new file with mode: 0644]

diff --git a/app.js b/app.js
index e092453213fdc86da6572fa764ce95acb6861d7d..48fec1c47e7bb8ec20958ae00ec459008b959edd 100644 (file)
--- a/app.js
+++ b/app.js
@@ -14,8 +14,8 @@ var config = require('./config')
  * Setting up redis.
  */
 
-var songsdb = redisurl.createClient(config.songsdburl);
-var usersdb = redisurl.createClient(config.usersdburl);
+var songsdb = redisurl.createClient(config.songsdburl)
+    , usersdb = redisurl.createClient(config.usersdburl);
 
 songsdb.on('error', function(err) {
     console.log(err.message);
@@ -56,7 +56,7 @@ app.dynamicHelpers({
 
 // Routes
 site.use({db:songsdb,rooms:config.rooms});
-user.use({db:usersdb,rooms:config.rooms});
+user.use({db:usersdb,rooms:config.rooms,sendgrid:config.sendgrid});
 
 app.get('/', site.index);
 app.get('/artworks', site.artworks);
@@ -68,6 +68,10 @@ app.post('/login', user.validateLogin, user.checkUser, user.authenticate);
 app.get('/logout', user.logout);
 app.get('/signup', site.signup);
 app.post('/signup', user.validateSignUp, user.userExists, user.emailExists, user.createAccount);
+app.get('/recoverpasswd', site.recoverPasswd);
+app.post('/recoverpasswd', user.validateRecoverPasswd, user.sendEmail);
+app.get('/resetpasswd', site.resetPasswd);
+app.post('/resetpasswd', user.resetPasswd);
 app.get('/:room', site.room);
 app.get('/user/*', user.profile);
 
@@ -135,7 +139,7 @@ io.sockets.on('connection', function(socket) {
         }
     });
     socket.on('joinanonymously', function(nickname, roomname) {
-        if (!socket.nickname && typeof nickname === 'string' && nickname !== '' && 
+        if (!socket.nickname && typeof nickname === 'string' && nickname !== '' &&
             typeof roomname === 'string' && config.rooms.indexOf(roomname) !== -1) {
             rooms[roomname].setNickName(socket, nickname);
         }
index 3d08ff19b267f34237b03c7897fb0cd0e85c4878..827a4691ca3b2b8dae3015e9e0af268b96bec528 100644 (file)
@@ -3,6 +3,10 @@
   "songsdburl": "",
   "usersdburl": "",
   "sessionsecret": "",
+  "sendgrid": {
+    "user": "",
+    "pass": ""
+  },
   "songsinarun": 15,
   "gameswithnorepeats": 3,
   "allowederrors": 2,
diff --git a/lib/email/mailer.js b/lib/email/mailer.js
new file mode 100644 (file)
index 0000000..516ef30
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ * Module dependencies.
+ */
+
+var fs = require('fs')
+    , jade = require('jade')
+    , nodemailer = require('nodemailer')
+    , jadetemplate = fs.readFileSync(__dirname + '/template.jade')
+    , texttemplate = fs.readFileSync(__dirname + '/template.txt', 'utf-8')
+    , transport;
+    
+/**
+ * Generate the HTML version of the message.
+ */
+
+var HTMLMessage = jade.compile(jadetemplate);
+
+/**
+ * Generate the plaintext version of the message.
+ */
+
+var plaintextMessage = function(token) {
+    return texttemplate.replace(/<token>/, token);
+};
+
+/**
+ * Send the reset password email.
+ */
+
+exports.sendEmail = function(to, token, callback) {
+    transport.sendMail({
+        from: 'binb <no-reply@binb.nodejitsu.com>',
+        to: to,
+        subject: 'binb password recovery',
+        html: HTMLMessage({token:token}),
+        text: plaintextMessage(token)
+    }, function(err, response) {
+        if(err) {
+            return callback(err);
+        }
+        callback(null, response);
+    });
+};
+
+/**
+ * Create a reusable transport method.
+ */
+
+exports.setTransport = function(sendgrid) {
+    transport = nodemailer.createTransport ('SMTP', {
+        service: 'SendGrid',
+        auth: {
+            user: sendgrid.user,
+            pass: sendgrid.pass
+        }
+    });
+};
diff --git a/lib/email/template.jade b/lib/email/template.jade
new file mode 100644 (file)
index 0000000..99041e0
--- /dev/null
@@ -0,0 +1,22 @@
+doctype html
+html
+    head
+        meta(charset="UTF-8")
+        title binb password recovery
+    body(style="font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;")
+        div(style="width:600px;margin:0 auto;border:3px solid #CCC;border-radius:5px;background-color:#F0F0F0;")
+            div(style="padding:16px 16px 0;font-size:15px;color:#505050;")
+                img(alt="logo",src="https://dl.dropbox.com/u/58444696/binb-logo.png")
+                h2 Password recovery
+                p To initiate the password reset process for your binb account,
+                    | click the link below:
+                a(style="color:#0088CC;text-decoration:none;",
+                    href="http://binb.nodejitsu.com/resetpasswd?token=#{token}")
+                    | http://bind.nodejitsu.com/resetpasswd?token=#{token}
+                p If you can't click it, please copy and paste the URL in a new tab/window.
+                p If you haven't requested a password reset, you can disregard this message, 
+                    | because it's  likely that another user entered your email address 
+                    | by mistake while trying to reset a password.
+                p.note(style="font-size:13px;")
+                    b Note:
+                    |  This email cannot accept replies.
diff --git a/lib/email/template.txt b/lib/email/template.txt
new file mode 100644 (file)
index 0000000..5b73c2f
--- /dev/null
@@ -0,0 +1,25 @@
+
+
+============================================================
+binb password recovery
+============================================================
+
+To initiate the password reset process for your binb
+account, click the link below:
+
+
+http://binb.nodejitsu.com/resetpasswd?token=<token>
+
+
+If you can't click it, please copy and paste the URL in a 
+new tab or window.
+
+If you haven't requested a password reset, you can 
+disregard this message, because it's  likely that another 
+user enteredyour email address by mistake while trying to 
+reset a password.
+
+============================================================
+============================================================
+
+Note: This email cannot accept replies.
index e9ac03f092a310e918ca749cef8a6823d69ac65b..9f044e596ec50cc8e2e9059c2511b534035aff6f 100644 (file)
@@ -7,6 +7,7 @@
     "connect-redis": "1.4.x",
     "express": "2.5.x",
     "jade": "0.26.x",
+    "nodemailer": "0.3.x",
     "redis-url": "0.1.x",
     "socket.io": "0.9.x"
   },
@@ -17,5 +18,5 @@
   "engines": {
     "node": "0.6.x"
   },
-  "version": "0.3.1-7"
-}
\ No newline at end of file
+  "version": "0.3.2"
+}
index aec9cb97ea8d0ccb87e0f0082949b8b25212c58f..18d5adfdbade2ff3e931491e826f6043a90f2789 100644 (file)
@@ -59,6 +59,11 @@ form .clearfix {
     margin-left: 120px;
     margin-top: 9px;
 }
+.forgot-passwd {
+    display: inline-block;
+    vertical-align: top;
+    margin: 14px 0 0 15px;
+}
 #captcha-input {
     width: 126px;
 }
@@ -83,7 +88,7 @@ input {
     float: right;
 }
 .thumbnails {
-    margin-bottom: 0px;
+    margin-bottom: 0;
 }
 .thumbnails > li {
     margin: 0 0 18px 80px;
@@ -230,7 +235,7 @@ input {
     width: 24px;
     height:24px;
     top: 49px;
-    background: url('/static/images/sprites.png') no-repeat 0px -32px;
+    background: url('/static/images/sprites.png') no-repeat 0 -32px;
 }
 #wheel-left {
     left:51px;
@@ -260,9 +265,9 @@ input {
     -webkit-border-radius:4px;
     -moz-border-radius:4px;
     border-radius:4px;
-    -webkit-box-shadow: inset 0px 1px 2px 0px #333;
-     -moz-box-shadow: inset 0px 1px 2px 0px #333;
-    box-shadow: inset 0px 1px 2px 0px #333;
+    -webkit-box-shadow: inset 0 1px 2px 0 #333;
+     -moz-box-shadow: inset 0 1px 2px 0 #333;
+    box-shadow: inset 0 1px 2px 0 #333;
 }
 #progress-bar {
     position:absolute;
@@ -274,7 +279,7 @@ input {
 }
 #progress {
     background-color: #6184b7;
-    width:0px;
+    width:0;
 }
 #touch-backdrop {
     width:220px;
@@ -319,15 +324,15 @@ input {
 }
 #volume-button .button:hover {
     border-color:#999999;
-    -webkit-box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.25);
-    -moz-box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.25);
-    box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.25);
+    -webkit-box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.25);
+    -moz-box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.25);
+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.25);
 }
 #volume-button .button:active {
     border-color:#999999 #AAAAAA #CCCCCC;
-    -webkit-box-shadow: 0px 1px 2px 0px #aaa inset;
-    -moz-box-shadow: 0px 1px 2px 0px #aaa inset;
-    box-shadow: 0px 1px 2px 0px #aaa inset;
+    -webkit-box-shadow: 0 1px 2px 0 #aaa inset;
+    -moz-box-shadow: 0 1px 2px 0 #aaa inset;
+    box-shadow: 0 1px 2px 0 #aaa inset;
     filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#e6e6e6,EndColorStr=#dcdcdc);
     background-image:-moz-linear-gradient(top,#e6e6e6 0,#dcdcdc 100%);
     background-image:-ms-linear-gradient(top,#e6e6e6 0,#dcdcdc 100%);
@@ -376,14 +381,14 @@ input {
     border-radius: 2px;
 }
 #volume-total {
-    -webkit-box-shadow: inset 0px 0px 3px 0px #333;
-    -moz-box-shadow: inset 0px 0px 3px 0px #333;
-    box-shadow: inset 0px 0px 3px 0px #333;
+    -webkit-box-shadow: inset 0 0 3px 0 #333;
+    -moz-box-shadow: inset 0 0 3px 0 #333;
+    box-shadow: inset 0 0 3px 0 #333;
 }
 #volume-current {
-    -webkit-box-shadow: inset 0px 0px 1px 0px #000;
-    -moz-box-shadow: inset 0px 0px 1px 0px #000;
-    box-shadow: inset 0px 0px 1px 0px #000;
+    -webkit-box-shadow: inset 0 0 1px 0 #000;
+    -moz-box-shadow: inset 0 0 1px 0 #000;
+    box-shadow: inset 0 0 1px 0 #000;
     background-color: #6184b7;
 }
 #volume-handle {
@@ -432,10 +437,10 @@ input {
 .registered, .round-rank {
     height: 16px;
     width: 16px;
-    margin: 1px 2px 0px 0px;
+    margin: 1px 2px 0 0;
 }
 .registered {
-    background: url('/static/images/sprites.png') no-repeat 0px -16px;
+    background: url('/static/images/sprites.png') no-repeat 0 -16px;
 }
 .registered:hover {
     background: url('/static/images/sprites.png') no-repeat -16px -16px;
index 5f8b8d86be081bb98f36eb8333346509520e7a99..19abf3dd22bf893152400014a5677afebbe99488 100644 (file)
@@ -52,7 +52,7 @@ exports.changePasswd = function(req, res) {
     if (!req.session.user) {
         return res.redirect('/login?followup=/changepasswd');
     }
-    res.render('changepasswd', {followup:req.query['followup'],loggedin:req.session.user});
+    res.render('changepasswd', {followup:req.query.followup,loggedin:req.session.user});
 };
 
 exports.index = function(req, res) {
@@ -60,7 +60,17 @@ exports.index = function(req, res) {
 };
 
 exports.login = function(req, res) {
-    res.render('login', {followup:req.query['followup']});
+    res.render('login', {followup:req.query.followup});
+};
+
+exports.recoverPasswd = function(req, res) {
+    var captcha = new Captcha();
+    req.session.captchacode = captcha.getCode();
+    res.render('recoverpasswd', {captchaurl:captcha.toDataURL(),followup:req.query.followup});
+};
+
+exports.resetPasswd = function(req, res) {
+    res.render('resetpasswd', {token:req.query.token});
 };
 
 exports.room = function(req, res) {
@@ -75,5 +85,5 @@ exports.room = function(req, res) {
 exports.signup = function(req, res) {
     var captcha = new Captcha();
     req.session.captchacode = captcha.getCode();
-    res.render('signup', {captchaurl:captcha.toDataURL(),followup:req.query['followup']});
+    res.render('signup', {captchaurl:captcha.toDataURL(),followup:req.query.followup});
 };
index e26cfc68e11e2f8ec00f942b7ed3962f8859f750..42da22090167f413dd06efbf5d207077a23e4edf 100644 (file)
@@ -5,6 +5,7 @@
 var crypto = require('crypto')
     , db
     , followupurls = []
+    , mailer = require('../lib/email/mailer')
     , User = require('../lib/user');
     
 /**
@@ -56,7 +57,7 @@ var buildLeaderboards = function(pointsresults, timesresults) {
     var obj = {
         pointsleaderboard: [],
         timesleaderboard: []
-    }
+    };
     for (var i=0; i<pointsresults.length; i+=2) {
         obj.pointsleaderboard.push({
             username: pointsresults[i],
@@ -77,6 +78,7 @@ var buildLeaderboards = function(pointsresults, timesresults) {
 exports.use = function(options) {
     db = options.db;
     rooms = options.rooms;
+    mailer.setTransport(options.sendgrid);
     // Populate the whitelist of follow-up URLs
     followupurls.push('/');
     followupurls.push('/changepasswd');
@@ -103,14 +105,14 @@ exports.leaderboards = function(req, res) {
  */
  
 exports.validateChangePasswd = function(req, res, next) {
-    if (!req.session.user || !req.body.oldpassword || !req.body.newpassword) {
-        return res.send('Missing data');
+    if (!req.session.user || req.body.oldpassword === undefined ||
+        req.body.newpassword === undefined) {
+        return res.send(412);
     }
     
     var errors = {};
     
     req.body.oldpassword = req.body.oldpassword.trim();
-    req.body.newpassword = req.body.newpassword.trim();
     if (req.body.oldpassword === '') {
         errors.oldpassword = "can't be empty";
     }
@@ -142,7 +144,7 @@ exports.checkOldPasswd = function(req, res, next) {
 };
 
 exports.changePasswd = function(req, res) {
-    var followup = (safeFollowup(req.query['followup'])) ? req.query['followup'] : '/'
+    var followup = (safeFollowup(req.query.followup)) ? req.query.followup : '/'
         , user = req.session.user
         , key = 'user:'+user
         , salt = crypto.randomBytes(6).toString('base64')
@@ -162,8 +164,8 @@ exports.changePasswd = function(req, res) {
  */
 
 exports.validateLogin = function(req, res, next) {
-    if (!req.body.username || !req.body.password) {
-        return res.send('Missing data');
+    if (req.body.username === undefined || req.body.password === undefined) {
+        return res.send(412);
     }
 
     var errors = {};
@@ -202,7 +204,7 @@ exports.authenticate = function(req, res) {
     db.hmget(key, 'salt', 'password', function(err, data) {
         var hash = crypto.createHash('sha256').update(data[0]+req.body.password).digest('hex');
         if (hash === data[1]) {
-            var followup = (safeFollowup(req.query['followup'])) ? req.query['followup'] : '/';
+            var followup = (safeFollowup(req.query.followup)) ? req.query.followup : '/';
             // Authentication succeeded, regenerate the session
             req.session.regenerate(function() {
                 req.session.cookie.maxAge = 604800000; // One week
@@ -232,8 +234,9 @@ exports.logout = function(req, res) {
  */
 
 exports.validateSignUp = function(req, res, next) {
-    if (!req.body.username || !req.body.email || !req.body.password || !req.body.captcha) {
-        return res.send('Missing data');
+    if (req.body.username === undefined || req.body.email === undefined ||
+        req.body.password === undefined || req.body.captcha === undefined) {
+        return res.send(412);
     }
 
     var errors = {};
@@ -272,7 +275,7 @@ exports.validateSignUp = function(req, res, next) {
 exports.userExists = function(req, res, next) {
     var key = 'user:'+req.body.username;
     db.exists(key, function(err, data) {
-        if (data === 1) { 
+        if (data === 1) {
             // User already exists
             req.session.errors = {alert: 'A user with that name already exists.'};
             return res.redirect(req.url);
@@ -284,7 +287,7 @@ exports.userExists = function(req, res, next) {
 exports.emailExists = function(req, res, next) {
     var key = 'email:'+req.body.email;
     db.exists(key, function(err, data) {
-        if (data === 1) { 
+        if (data === 1) {
             // Email already exists
             req.session.errors = {alert: 'A user with that email already exists.'};
             return res.redirect(req.url);
@@ -319,7 +322,100 @@ exports.createAccount = function(req, res) {
     // Delete old fields values (we don't want these to be available in login view)
     delete req.session.oldvalues;
     var msg = 'You successfully created your account. You are now ready to login.';
-    res.render('login', {followup:req.query['followup'],success:msg});
+    res.render('login', {followup:req.query.followup,success:msg});
+};
+
+/**
+ * Recover password middlewares.
+ */
+exports.validateRecoverPasswd = function(req, res, next) {
+    if (req.body.email === undefined || req.body.captcha === undefined) {
+        return res.send(412);
+    }
+
+    var errors = {};
+    
+    if (!req.body.email.isEmail()) {
+        errors.email = 'is not an email address';
+    }
+    if (req.body.captcha !== req.session.captchacode) {
+        errors.captcha = 'no match';
+    }
+    
+    req.session.oldvalues = {email: req.body.email};
+    
+    if (errors.email || errors.captcha) {
+        req.session.errors = errors;
+        return res.redirect(req.url);
+    }
+    
+    next();
+};
+
+exports.sendEmail = function(req, res) {
+    var key = 'email:'+req.body.email;
+    db.get(key, function(err, data) {
+        if (data !== null) {
+            // Email exists, generate a secure random token
+            delete req.session.captchacode;
+            var token = crypto.randomBytes(48).toString('hex');
+            // Token expires after 4 hours
+            db.setex('token:'+token, 14400, data, function(err, reply) {
+                mailer.sendEmail(req.body.email, token, function(err, response) {
+                    if (err) {
+                        console.log('reset password error: '+err.message);
+                    }
+                });
+            });
+            return res.render('recoverpasswd', {followup:req.query.followup,success:true});
+        }
+        req.session.errors = {alert: 'The email address you specified could not be found'};
+        res.redirect(req.url);
+    });
+};
+
+/**
+ * Reset user password.
+ */
+
+exports.resetPasswd = function(req, res) {
+    if (req.body.password === undefined) {
+        return res.send(412);
+    }
+    
+    var errors = {};
+    
+    // Validate new password
+    if (!req.body.password.match(/^[A-Za-z0-9]{6,15}$/)) {
+        errors.password = '6 to 15 alphanumeric characters required';
+    }
+    // Check token availability
+    if (!req.query.token) {
+        errors.alert = 'Missing token.';
+    }
+    
+    if (errors.password || errors.alert) {
+        req.session.errors = errors;
+        return res.redirect(req.url);
+    }
+    
+    var key = 'token:'+req.query.token;
+    db.get(key, function(err, user) {
+        if (user !== null) {
+            // Delete the token
+            db.del(key);
+            // Update password
+            var salt = crypto.randomBytes(6).toString('base64');
+            var password = crypto.createHash('sha256').update(salt+req.body.password).digest('hex');
+            db.hmset(user, 'salt', salt, 'password', password, function(err, data) {
+                res.render('login', {success:'You can now login with your new password.'});
+            });
+            return;
+        }
+        req.session.errors = {alert: 'Invalid or expired token.'};
+        res.redirect(req.url);
+    });
 };
 
 /**
index e761bc487a617d50d5bb19603a76562d1a6de358..17eedd250505a10bfe71048d3b568d80f58b39c5 100644 (file)
@@ -67,6 +67,6 @@ html
                                             input#password(type="password",name="newpassword",
                                                 placeholder="enter your new password...")
                                 button.submit-button.btn.btn-primary(type="submit")
-                                    i.icon-lock.icon-white
+                                    i.icon-edit.icon-white
                                     |  Update
             include footer
index 7461c1c4856a8e2f103ba14afe68128481fe5b8c..51c3a286fcc9066d24f9e35bd2bcecc2c27e72ba 100644 (file)
@@ -77,4 +77,6 @@ html
                                 button.submit-button.btn.btn-primary(type="submit")
                                     i.icon-lock.icon-white
                                     |  Login
+                                a.forgot-passwd(href="/recoverpasswd#{followup}")
+                                    | Forgot your password?
             include footer
diff --git a/views/recoverpasswd.jade b/views/recoverpasswd.jade
new file mode 100644 (file)
index 0000000..a8e8d27
--- /dev/null
@@ -0,0 +1,86 @@
+followup = (typeof(followup) !== "undefined") ? '?followup='+followup : '?followup=/'
+doctype html
+html
+    include header
+        title binb :: Recover password
+        script(src="/static/js/bootstrap.min.js")
+    body
+        include uv.jade
+        .navbar.navbar-fixed-top
+            .navbar-inner
+                .container
+                    a.brand(href="/")
+                        .logo #{motto}
+                    ul.nav.pull-right
+                        li
+                            a(href="/") Home
+                        li
+                            a(href="/signup#{followup}") Sign up
+                        li
+                            a(href="/login#{followup}") Login
+        .container
+            section
+                .row
+                    .span12.offset2
+                        if (typeof(success) !== "undefined")
+                            .alert.alert-block.alert-success
+                                h4.alert-heading Success!
+                                | An email has been sent to you.<br>To start the password reset 
+                                | process, open this email and follow the given instructions.<br>
+                                | If you don't receive it in a reasonable amount of time, please 
+                                | use the support form on the left.
+                        else
+                            if ((typeof(errors) !== "undefined") && (typeof(errors.alert) !== "undefined"))
+                                .alert.alert-error
+                                    a.close(data-dismiss="alert") &times;
+                                    strong Oh snap!
+                                    |  #{errors.alert}
+                            form.form-horizontal.well(method="post",
+                                action="/recoverpasswd#{followup}")
+                                fieldset
+                                    if (typeof(errors) !== "undefined")
+                                        if (typeof(errors.email) !== "undefined")
+                                            .control-group.error
+                                                label.control-label(for="email") Email
+                                                .controls
+                                                    input#oldpassword(type="text",name="email",
+                                                        value="#{oldvalues.email}")
+                                                    span.help-inline #{errors.email}
+                                        else
+                                            .control-group
+                                                label.control-label(for="email") Email
+                                                .controls
+                                                    input#username(type="text",name="email",
+                                                        value="#{oldvalues.email}")
+                                        if (typeof(errors.captcha) !== 'undefined')
+                                            .control-group.error
+                                                label.control-label(for="captcha-input")
+                                                    | Are you human?
+                                                .controls
+                                                    img#captcha(src="#{captchaurl}")
+                                                    input#captcha-input(type="text",name="captcha")
+                                                    span.help-inline #{errors.captcha}
+                                        else
+                                            .control-group
+                                                label.control-label(for="captcha-input")
+                                                    | Are you human?
+                                                .controls
+                                                    img#captcha(src="#{captchaurl}")
+                                                    input#captcha-input(type="text",name="captcha",
+                                                        placeholder="type what you see...")
+                                    else
+                                        .control-group
+                                            label.control-label(for="email") Email
+                                            .controls
+                                                input#email(type="text",name="email",
+                                                    placeholder="type the email you used to sign up...")
+                                        .control-group
+                                            label.control-label(for="captcha-input") Are you human?
+                                            .controls
+                                                img#captcha(src="#{captchaurl}")
+                                                input#captcha-input(type="text",name="captcha",
+                                                    placeholder="type what you see...")
+                                    button.submit-button.btn.btn-primary(type="submit")
+                                        i.icon-envelope.icon-white
+                                        |  Send password reset link
+            include footer
diff --git a/views/resetpasswd.jade b/views/resetpasswd.jade
new file mode 100644 (file)
index 0000000..9a6d916
--- /dev/null
@@ -0,0 +1,47 @@
+token = (typeof(token) !== "undefined") ? '?token='+token : ''
+doctype html
+html
+    include header
+        title binb :: Reset password
+        script(src="/static/js/bootstrap.min.js")
+    body
+        include uv.jade
+        .navbar.navbar-fixed-top
+            .navbar-inner
+                .container
+                    a.brand(href="#")
+                        .logo #{motto}
+        .container
+            section
+                .row
+                    .span12.offset2
+                        if ((typeof(errors) !== "undefined") && (typeof(errors.alert) !== "undefined"))
+                            .alert.alert-error
+                                a.close(data-dismiss="alert") &times;
+                                strong Oh snap!
+                                |  #{errors.alert}
+                        form.form-horizontal.well(method="post",action="/resetpasswd#{token}")
+                            fieldset
+                                if (typeof(errors) !== "undefined")
+                                    if (typeof(errors.password) !== "undefined")
+                                        .control-group.error
+                                            label.control-label(for="password") New password
+                                            .controls
+                                                input#oldpassword(type="password",name="password")
+                                                span.help-inline #{errors.password}
+                                    else
+                                        .control-group
+                                            label.control-label(for="password") New password
+                                            .controls
+                                                input#username(type="password",name="password",
+                                                    placeholder="enter your new password...")
+                                else
+                                    .control-group
+                                        label.control-label(for="oldpassword") New password
+                                        .controls
+                                            input#username(type="password",name="password",
+                                                placeholder="enter your new password...")
+                                button.submit-button.btn.btn-primary(type="submit")
+                                    i.icon-edit.icon-white
+                                    |  Update
+            include footer