From 32cb29b45f75e3258e12e55b6815a42924e5e7f6 Mon Sep 17 00:00:00 2001 From: Rainer Eli Date: Tue, 20 Oct 2015 22:48:58 -0600 Subject: [PATCH 1/9] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b9a02261ff..acadee173b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitrpg.svg?branch=dev We need more programmers! Your assistance will be greatly appreciated. -For an introduction to the technologies used and how the software is organised, refer to [Contributing to Habitica](http://habitica.wikia.com/wiki/Contributing_to_Habitica#Coders_.28Web_.26_Mobile.29) - "Coders (Web & Mobile)" section. +For an introduction to the technologies used and how the software is organized, refer to [Contributing to Habitica](http://habitica.wikia.com/wiki/Contributing_to_Habitica#Coders_.28Web_.26_Mobile.29) - "Coders (Web & Mobile)" section. To set up a local install of Habitica for development and testing, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally), which contains instructions for Windows, *nix / Mac OS, and Vagrant. From c7f8426bceea5d0fab8bdecdf69a410270f396c1 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Wed, 21 Oct 2015 11:01:06 +0200 Subject: [PATCH 2/9] preparation for code for lowercase emails and lowercase version of username to check for duplicates --- website/src/controllers/auth.js | 23 ++++++++++++++++------- website/src/models/user.js | 3 ++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/website/src/controllers/auth.js b/website/src/controllers/auth.js index 667bf6454a..043ef200b9 100644 --- a/website/src/controllers/auth.js +++ b/website/src/controllers/auth.js @@ -67,8 +67,12 @@ api.authWithUrl = function(req, res, next) { } api.registerUser = function(req, res, next) { - var regEmail = RegexEscape(req.body.email), - regUname = RegexEscape(req.body.username); + var regEmail = RegexEscape(req.body.email); + var regUname = RegexEscape(req.body.username); + + // Get the lowercase version of username to check that we do not have duplicates + // So we can search for it in the database and then reject the choosen username if 1 or more results are found + var lowerCaseUsername = req.body.username.toLowerCase(); async.auto({ validate: function(cb) { if (!(req.body.username && req.body.password && req.body.email)) @@ -95,7 +99,8 @@ api.registerUser = function(req, res, next) { auth: { local: { username: req.body.username, - email: req.body.email, + lowerCaseUsername: lowerCaseUsername, // Store the lowercase version of the username + email: req.body.email.toLowerCase(), // Store email as lowercase salt: salt, hashed_password: utils.encryptPassword(req.body.password, salt) }, @@ -266,15 +271,19 @@ var invalidPassword = function(user, password){ } api.changeUsername = function(req, res, next) { + var user = res.locals.user; + var username = req.body.username; async.waterfall([ function(cb){ - User.findOne({'auth.local.username': RegexEscape(req.body.username)}, {auth:1}, cb); + User.findOne({'auth.local.username': RegexEscape(username)}, {auth:1}, cb); }, function(found, cb){ if (found) return cb({code:401, err: "Username already taken"}); - if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password)); - res.locals.user.auth.local.username = req.body.username; - res.locals.user.save(cb); + if (invalidPassword(user, req.body.password)) return cb(invalidPassword(user, req.body.password)); + user.auth.local.username = username; + user.auth.local.lowerCaseUsername = username.toLowerCase(); + + user.save(cb); } ], function(err){ if (err) return err.code ? res.json(err.code, err) : next(err); diff --git a/website/src/models/user.js b/website/src/models/user.js index bc416fe3e4..71d7c107e6 100644 --- a/website/src/models/user.js +++ b/website/src/models/user.js @@ -66,7 +66,8 @@ var UserSchema = new Schema({ email: String, hashed_password: String, salt: String, - username: String + username: String, + lowerCaseUsername: String // Store a lowercase version of username to check for duplicates }, timestamps: { created: {type: Date,'default': Date.now}, From fc9d0775ab5ec94d7079ee510c3ca1b3f8a84125 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Wed, 21 Oct 2015 11:19:14 +0200 Subject: [PATCH 3/9] add migration for lowercase version of emails and username --- .../20151021_usernames_emails_lowercase.js | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 migrations/20151021_usernames_emails_lowercase.js diff --git a/migrations/20151021_usernames_emails_lowercase.js b/migrations/20151021_usernames_emails_lowercase.js new file mode 100644 index 0000000000..432d7eb650 --- /dev/null +++ b/migrations/20151021_usernames_emails_lowercase.js @@ -0,0 +1,63 @@ +/* + * Migrate email to lowerCase version and add auth.local.lowerCaseUsername email + */ + +var mongo = require('mongoskin'); +var async = require('async'); + +var dbserver = 'url'; +var dbname = 'dbname'; +var countUsers = 0; + +var db = mongo.db(dbserver + '/' + dbname + '?auto_reconnect'); +var dbUsers = db.collection('users'); + +console.log('Begins work on db'); + +function findUsers(gt){ + var query = {}; + if(gt) query._id = {$gt: gt}; + + console.log(query) + + dbUsers.find(query, { + fields: {_id: 1, auth: 1}, + limit: 10000, + sort: { + _id: 1 + } + }).toArray(function(err, users){ + if(err) throw err; + + var lastUser = null; + if(users.length === 10000){ + lastUser = users[users.length - 1]; + } + + async.eachLimit(users, 20, function(user, cb){ + countUsers++; + console.log('User: ', countUsers, user._id); + + var update = { + $set: {}; + }; + + if(user.auth && user.auth.local) { + if(user.auth.local.username) update['$set']['auth.local.lowerCaseUsername'] = user.auth.local.username.toLowerCase(); + if(user.auth.local.email) update['$set']['auth.local.email'] = user.auth.local.email.toLowerCase(); + } + + dbUsers.update({ + _id: user._id + }, update, cb); + }, function(err){ + if(err) throw err; + + if(lastUser && lastUser._id){ + findUsers(lastUser._id); + } + }); + }); +}; + +findUsers(); \ No newline at end of file From e75d3547c50b4a21b0606ac9be5d5300190c6116 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Wed, 21 Oct 2015 13:14:18 +0200 Subject: [PATCH 4/9] force emails to lowercase and use a lowercase version of username to check for duplicates --- website/src/controllers/auth.js | 58 +++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/website/src/controllers/auth.js b/website/src/controllers/auth.js index 043ef200b9..b9bea19b51 100644 --- a/website/src/controllers/auth.js +++ b/website/src/controllers/auth.js @@ -25,10 +25,6 @@ var accountSuspended = function(uuid){ code: 'ACCOUNT_SUSPENDED' }; } -// Allow case-insensitive regex searching for Mongo queries. See http://stackoverflow.com/a/3561711/362790 -var RegexEscape = function(s){ - return new RegExp('^' + s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$', 'i'); -} api.auth = function(req, res, next) { var uid = req.headers['x-api-user']; @@ -67,40 +63,42 @@ api.authWithUrl = function(req, res, next) { } api.registerUser = function(req, res, next) { - var regEmail = RegexEscape(req.body.email); - var regUname = RegexEscape(req.body.username); - + var email = req.body.email && req.body.email.toLowerCase(); + var username = req.body.username; // Get the lowercase version of username to check that we do not have duplicates // So we can search for it in the database and then reject the choosen username if 1 or more results are found - var lowerCaseUsername = req.body.username.toLowerCase(); + var lowerCaseUsername = username && username.toLowerCase(); + async.auto({ validate: function(cb) { - if (!(req.body.username && req.body.password && req.body.email)) + if (!(username && req.body.password && email)) return cb({code:401, err: ":username, :email, :password, :confirmPassword required"}); if (req.body.password !== req.body.confirmPassword) return cb({code:401, err: ":password and :confirmPassword don't match"}); - if (!validator.isEmail(req.body.email)) + if (!validator.isEmail(email)) return cb({code:401, err: ":email invalid"}); cb(); }, findReg: function(cb) { - User.findOne({$or:[{'auth.local.email': regEmail}, {'auth.local.username': regUname}]}, {'auth.local':1}, cb); + // Search for duplicates using lowercase version of username + User.findOne({$or:[{'auth.local.email': email}, {'auth.local.lowerCaseUsername': lowerCaseUsername}]}, {'auth.local':1}, cb); }, findFacebook: function(cb){ User.findOne({_id: req.headers['x-api-user'], apiToken: req.headers['x-api-key']}, {auth:1}, cb); }, register: ['validate', 'findReg', 'findFacebook', function(cb, data) { if (data.findReg) { - if (regEmail.test(data.findReg.auth.local.email)) return cb({code:401, err:"Email already taken"}); - if (regUname.test(data.findReg.auth.local.username)) return cb({code:401, err:"Username already taken"}); + if (email === data.findReg.auth.local.email) return cb({code:401, err:"Email already taken"}); + // Check that the lowercase username isn't already used + if (lowerCaseUsername === data.findReg.auth.local.lowerCaseUsername) return cb({code:401, err:"Username already taken"}); } var salt = utils.makeSalt(); var newUser = { auth: { local: { - username: req.body.username, + username: username, lowerCaseUsername: lowerCaseUsername, // Store the lowercase version of the username - email: req.body.email.toLowerCase(), // Store email as lowercase + email: email, // Store email as lowercase salt: salt, hashed_password: utils.encryptPassword(req.body.password, salt) }, @@ -126,6 +124,7 @@ api.registerUser = function(req, res, next) { user.save(function(err, savedUser){ // Clean previous email preferences + // TODO when emails added to EmailUnsubcription they should use lowercase version EmailUnsubscription.remove({email: savedUser.auth.local.email}, function(){ utils.txnEmail(savedUser, 'welcome'); }); @@ -146,9 +145,12 @@ api.registerUser = function(req, res, next) { api.loginLocal = function(req, res, next) { var username = req.body.username; - var password = req.body.password; + var password = req.body.password; if (!(username && password)) return res.json(401, {err:'Missing :username or :password in request body, please provide both'}); - var login = validator.isEmail(username) ? {'auth.local.email':username} : {'auth.local.username':username}; + var login = validator.isEmail(username) ? + {'auth.local.email':username.toLowerCase()} : // Emails are all lowercase + {'auth.local.username':username}; // Use the username as the user typed it + User.findOne(login, {auth:1}, function(err, user){ if (err) return next(err); if (!user) return res.json(401, {err:"Uh-oh - your username or password is incorrect.\n- Make sure your username or email is typed correctly.\n- You may have signed up with Facebook, not email. Double-check by trying Facebook login.\n- If you forgot your password, click \"Forgot Password\"."}); @@ -238,12 +240,14 @@ api.deleteSocial = function(req,res,next){ } api.resetPassword = function(req, res, next){ - var email = req.body.email, + var email = req.body.email && req.body.email.toLowerCase(), // Emails are all lowercase salt = utils.makeSalt(), newPassword = utils.makeSalt(), // use a salt as the new password too (they'll change it later) hashed_password = utils.encryptPassword(newPassword, salt); - User.findOne({'auth.local.email': RegexEscape(email)}, function(err, user){ + if(!email) return res.json(400, {err: "Email not provided"}); + + User.findOne({'auth.local.email': email}, function(err, user){ if (err) return next(err); if (!user) return res.send(401, {err:"Sorry, we can't find a user registered with email " + email + "\n- Make sure your email address is typed correctly.\n- You may have signed up with Facebook, not email. Double-check by trying Facebook login."}); user.auth.local.salt = salt; @@ -272,16 +276,19 @@ var invalidPassword = function(user, password){ api.changeUsername = function(req, res, next) { var user = res.locals.user; - var username = req.body.username; + var username = req.body.username; + var lowerCaseUsername = username && username.toLowerCase(); // we search for the lowercased version to intercept duplicates + + if(!username) return res.json(400, {err: "Username not provided"}); async.waterfall([ function(cb){ - User.findOne({'auth.local.username': RegexEscape(username)}, {auth:1}, cb); + User.findOne({'auth.local.lowerCaseUsername': lowerCaseUsername}, {auth:1}, cb); }, function(found, cb){ if (found) return cb({code:401, err: "Username already taken"}); if (invalidPassword(user, req.body.password)) return cb(invalidPassword(user, req.body.password)); user.auth.local.username = username; - user.auth.local.lowerCaseUsername = username.toLowerCase(); + user.auth.local.lowerCaseUsername = lowerCaseUsername; user.save(cb); } @@ -292,14 +299,17 @@ api.changeUsername = function(req, res, next) { } api.changeEmail = function(req, res, next){ + var email = req.body.email && req.body.email.toLowerCase(); // emails are all lowercase + if(!email) return res.json(400, {err: "Email not provided"}); + async.waterfall([ function(cb){ - User.findOne({'auth.local.email': RegexEscape(req.body.email)}, {auth:1}, cb); + User.findOne({'auth.local.email': email}, {auth:1}, cb); }, function(found, cb){ if(found) return cb({code:401, err: "Email already taken"}); if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password)); - res.locals.user.auth.local.email = req.body.email; + res.locals.user.auth.local.email = email; res.locals.user.save(cb); } ], function(err){ From d32f6f23cc3b13df56e9e05e8cd916a97b8edb67 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Wed, 21 Oct 2015 15:59:17 -0400 Subject: [PATCH 5/9] feat(sharing): Improved front page FB share --- website/public/js/controllers/footerCtrl.js | 9 +++++++++ website/views/shared/footer.jade | 3 ++- website/views/static/front.jade | 12 +++++++----- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/website/public/js/controllers/footerCtrl.js b/website/public/js/controllers/footerCtrl.js index 008524db51..18590e4f32 100644 --- a/website/public/js/controllers/footerCtrl.js +++ b/website/public/js/controllers/footerCtrl.js @@ -34,6 +34,15 @@ function($scope, $rootScope, User, $http, Notification, ApiUrl) { // Twitter $.getScript('https://platform.twitter.com/widgets.js'); + // Facebook + (function(d, s, id) { + var js, fjs = d.getElementsByTagName(s)[0]; + if (d.getElementById(id)) return; + js = d.createElement(s); js.id = id; + js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.5"; + fjs.parentNode.insertBefore(js, fjs); + }(document, 'script', 'facebook-jssdk')); + /* Google Content Experiments if (window.env.NODE_ENV === 'production') { $.getScript('//www.google-analytics.com/cx/api.js?experiment=boVO4eEyRfysNE5D53nCMQ', function(){ diff --git a/website/views/shared/footer.jade b/website/views/shared/footer.jade index 55df4cbec5..7f1aacdf79 100644 --- a/website/views/shared/footer.jade +++ b/website/views/shared/footer.jade @@ -1,4 +1,5 @@ footer.footer(ng-controller='FooterCtrl') + div(id='fb-root') .container .row .col-sm-3 @@ -63,7 +64,7 @@ footer.footer(ng-controller='FooterCtrl') table tr td - a.addthis_button_facebook_like(fb:like:layout='button_count') + .fb-like(data-href='https://habitica.com/', data-layout='button', data-action='like', data-show-faces='true', data-share='true') tr td a.twitter-share-button(href='https://twitter.com/intent/tweet?text=Improve+yourself+in+the+land+of+Habitica!&via=habitica&url=https://habitica.com/&count=none')=env.t('tweet') diff --git a/website/views/static/front.jade b/website/views/static/front.jade index a05d16f0e8..3c6aabf7ca 100644 --- a/website/views/static/front.jade +++ b/website/views/static/front.jade @@ -12,11 +12,13 @@ html(ng-app='habitrpg', ng-controller='RootCtrl') meta(name='author', content='') meta(name='geo.placename', content='') meta(name='viewport', content='width=device-width, maximum-scale=1') - meta(property='og:title', content='') - meta(property='og:description', content='') - meta(property='og:url', content='') - meta(property='og:image', content='') - meta(property='og:site_name', content='') + meta(property='og:url', content='https://habitica.com/') + meta(property='og:type', content='website') + meta(property='og:title', content='Habitica: Your Life the Role Playing Game') + meta(property='og:description', content='Habitica is a free habit building and productivity app that treats your real life like a game. With in-game rewards and punishments to motivate you and a strong social network to inspire you, Habitica can help you achieve your goals to become healthy, hard-working, and happy.') + meta(property='og:site_name', content='Habitica') + meta(property='og:image', content='https://s3.amazonaws.com/habitica-assets/assets/gryphon_logo.png') + meta(property='fb:app_id', content='128307497299777') meta(name='twitter:card' content='summary') meta(name='twitter:site' content='@habitica') meta(name='twitter:title' content='Habitica: Your Life the Role Playing Game') From a2c0e022839c8a874261869d64df0886d37c64b6 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Wed, 21 Oct 2015 16:37:33 -0400 Subject: [PATCH 6/9] fix(social): Correct OG tag errors --- website/views/static/front.jade | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/views/static/front.jade b/website/views/static/front.jade index 3c6aabf7ca..c5c54fe1bb 100644 --- a/website/views/static/front.jade +++ b/website/views/static/front.jade @@ -12,12 +12,12 @@ html(ng-app='habitrpg', ng-controller='RootCtrl') meta(name='author', content='') meta(name='geo.placename', content='') meta(name='viewport', content='width=device-width, maximum-scale=1') - meta(property='og:url', content='https://habitica.com/') + meta(property='og:url', content='https://habitica.com/static/front') meta(property='og:type', content='website') meta(property='og:title', content='Habitica: Your Life the Role Playing Game') meta(property='og:description', content='Habitica is a free habit building and productivity app that treats your real life like a game. With in-game rewards and punishments to motivate you and a strong social network to inspire you, Habitica can help you achieve your goals to become healthy, hard-working, and happy.') meta(property='og:site_name', content='Habitica') - meta(property='og:image', content='https://s3.amazonaws.com/habitica-assets/assets/gryphon_logo.png') + meta(property='og:image', content='https://s3.amazonaws.com/habitica-assets/assets/habitica_lockup.png') meta(property='fb:app_id', content='128307497299777') meta(name='twitter:card' content='summary') meta(name='twitter:site' content='@habitica') From 9fb671f0689f9863ab6bf9038fb021c66cd39537 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Wed, 21 Oct 2015 16:25:03 -0500 Subject: [PATCH 7/9] fix(social): Better share image --- website/views/static/front.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/views/static/front.jade b/website/views/static/front.jade index c5c54fe1bb..49ffa53988 100644 --- a/website/views/static/front.jade +++ b/website/views/static/front.jade @@ -17,7 +17,7 @@ html(ng-app='habitrpg', ng-controller='RootCtrl') meta(property='og:title', content='Habitica: Your Life the Role Playing Game') meta(property='og:description', content='Habitica is a free habit building and productivity app that treats your real life like a game. With in-game rewards and punishments to motivate you and a strong social network to inspire you, Habitica can help you achieve your goals to become healthy, hard-working, and happy.') meta(property='og:site_name', content='Habitica') - meta(property='og:image', content='https://s3.amazonaws.com/habitica-assets/assets/habitica_lockup.png') + meta(property='og:image', content='https://s3.amazonaws.com/habitica-assets/assets/gryphon_logo_300x300.png') meta(property='fb:app_id', content='128307497299777') meta(name='twitter:card' content='summary') meta(name='twitter:site' content='@habitica') From d08a3efbcd05d32314c23e2764bf1ad4db4d211d Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Thu, 22 Oct 2015 12:04:43 -0500 Subject: [PATCH 8/9] fix(news): Bogus trailing slash --- website/views/shared/new-stuff.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/views/shared/new-stuff.jade b/website/views/shared/new-stuff.jade index 8e776eef8b..76a113ef8e 100644 --- a/website/views/shared/new-stuff.jade +++ b/website/views/shared/new-stuff.jade @@ -5,7 +5,7 @@ h2 10/21/2015 - FROG PET QUEST, TO-DO SORTING FIX, AND SECOND WORLD BOSS EXHAUST span.Mount_Body_Frog-Base.pull-right span.Mount_Head_Frog-Base.pull-right(style='margin:0') h3 Frog Pet Quest - p Deep in the Swamps of Stagnation, you find your path obstructed by debris... and an angry amphibian. Yuck! If you complete the Clutter Frog quest, you'll be rewarded with some princely Frog pets! + p Deep in the Swamps of Stagnation, you find your path obstructed by debris... and an angry amphibian. Yuck! If you complete the Clutter Frog quest, you'll be rewarded with some princely Frog pets! p.small.muted Art by starsystemic, RosemonkeyCT, Jon Arjinborn, and Breadstrings p.small.muted Writing by Fluitare tr From bcc522f08d50148f2f44b46d922653966c15daeb Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Thu, 22 Oct 2015 13:52:26 -0400 Subject: [PATCH 9/9] feat(sharing): Add Tumblr button --- website/public/js/controllers/footerCtrl.js | 3 +++ website/views/shared/footer.jade | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/website/public/js/controllers/footerCtrl.js b/website/public/js/controllers/footerCtrl.js index 18590e4f32..189a74f43a 100644 --- a/website/public/js/controllers/footerCtrl.js +++ b/website/public/js/controllers/footerCtrl.js @@ -43,6 +43,9 @@ function($scope, $rootScope, User, $http, Notification, ApiUrl) { fjs.parentNode.insertBefore(js, fjs); }(document, 'script', 'facebook-jssdk')); + // Tumblr + $.getScript('https://assets.tumblr.com/share-button.js'); + /* Google Content Experiments if (window.env.NODE_ENV === 'production') { $.getScript('//www.google-analytics.com/cx/api.js?experiment=boVO4eEyRfysNE5D53nCMQ', function(){ diff --git a/website/views/shared/footer.jade b/website/views/shared/footer.jade index 7f1aacdf79..d3a4edb1cc 100644 --- a/website/views/shared/footer.jade +++ b/website/views/shared/footer.jade @@ -64,16 +64,19 @@ footer.footer(ng-controller='FooterCtrl') table tr td - .fb-like(data-href='https://habitica.com/', data-layout='button', data-action='like', data-show-faces='true', data-share='true') + .fb-like(data-href='https://habitica.com/static/front', data-layout='button', data-action='like', data-share='true') tr td a.twitter-share-button(href='https://twitter.com/intent/tweet?text=Improve+yourself+in+the+land+of+Habitica!&via=habitica&url=https://habitica.com/&count=none')=env.t('tweet') tr td - iframe(src='/bower_components/github-buttons/github-btn.html?user=habitrpg&repo=habitrpg&type=watch&count=true', allowtransparency='true', frameborder='0', scrolling='0', width='85px', height='20px') + a.tumblr-share-button(data-href='https://habitica.com/static/front', data-notes='none') tr td a.addthis_button_google_plusone(g:plusone:size='medium') + tr + td + iframe(src='/bower_components/github-buttons/github-btn.html?user=habitrpg&repo=habitrpg&type=watch&count=true', allowtransparency='true', frameborder='0', scrolling='0', width='85px', height='20px') else if (env.NODE_ENV==='development' || env.NODE_ENV==='test') && !env.isStaticPage h4 Debug .btn-group-vertical