diff --git a/README.md b/README.md
index 22a2f28259..5d4caf4709 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,16 @@

-Hackathon Starter [](https://david-dm.org/sahat/hackathon-starter) [](https://travis-ci.org/sahat/hackathon-starter) [](https://github.com/igrigorik/ga-beacon)
+Hackathon Starter [](https://david-dm.org/sahat/hackathon-starter) [](https://travis-ci.org/sahat/hackathon-starter) [](https://github.com/igrigorik/ga-beacon)
=======================
:octocat: **Live Demo**: http://hackathonstarter.herokuapp.com
-Jump to [What's new in 2.3.3?](#changelog)
+Jump to [What's new in 2.4.0?](#changelog)
A boilerplate for **Node.js** web applications.
If you have attended any hackathons in the past, then you know how much time it takes to
get a project started: decide on what to build, pick a programming language, pick a web framework,
-pick a CSS framework. A while later, you might have an initial project up on GitHub and only then
+pick a CSS framework. A while later, you might have an initial project xup on GitHub and only then
can other team members start contributing. Or how about doing something as simple as *Sign in with Facebook*
authentication? You can spend hours on it if you are not familiar with how OAuth 2.0 works.
@@ -243,7 +243,7 @@ The same goes for other providers.
-- [Sign up](http://stripe.com) or log into your your [dashboard](https://manage.stripe.com)
+- [Sign up](http://stripe.com) or log into your [dashboard](https://manage.stripe.com)
- Click on your profile and click on Account Settings
- Then click on [API Keys](https://manage.stripe.com/account/apikeys)
- Copy the **Secret Key**. and add this into `config/secrets.js`
@@ -271,7 +271,7 @@ The same goes for other providers.
-
+
- Go to http://www.tumblr.com/oauth/apps
- Once signed in, click **+Register application**
- Fill in all the details
@@ -281,7 +281,7 @@ The same goes for other providers.
-
+
- Go to http://steamcommunity.com/dev/apikey
- Sign in with your existing Steam account
- Enter your *Domain Name*, then and click **Register**
@@ -291,7 +291,7 @@ The same goes for other providers.
- Go to https://sendgrid.com/user/signup
-- Sign up and **confirm** your your account via the *activation email*
+- Sign up and **confirm** your account via the *activation email*
- Then enter your SendGrid *Username* and *Password* into `config/secrets.js`
@@ -1072,10 +1072,27 @@ User.aggregate({ $group: { _id: null, total: { $sum: '$votes' } } }, function(er
Deployment
----------
-Once you are ready to deploy your app, you will need to create an account with a cloud platform to host it. These are not
-the only choices, but they are my top picks. Create an account with **MongoLab** and then pick one of the 4 providers
-below. Once again, there are plenty of other choices and you are not limited to just the ones listed below. From my
-experience, **Heroku** is the easiest to get started with, it will automatically restart your node.js process when it crashes, custom domain support on free accounts and zero-downtime deployments.
+Once you are ready to deploy your app, you will need to create an account with
+a cloud platform to host it. These are not the only choices, but they are my top
+picks. From my experience, **Heroku** is the easiest to get started with, it will
+automatically restart your Node.js process when it crashes, zero-downtime
+deployments and custom domain support on free accounts. Additionally, you can
+create an account with **MongoLab** and then pick one of the *4* providers below.
+Again, there are plenty of other choices and you are not limited to just the ones
+listed below.
+
+### 1-Step Deployment with Heroku
+
+
+- Download and install [Heroku Toolbelt](https://toolbelt.heroku.com/)
+- In terminal, run `heroku login` and enter your Heroku credentials
+- From *your app* directory run `heroku create`
+- Run `heroku addons:add mongolab` to set up Mongo and configure your environment variables
+- Lastly, do `git push heroku master`. Done!
+
+**:exclamation:Note:** To install Heroku add-ons your account must be verified.
+
+---
- Open [mongolab.com](https://mongolab.com) website
@@ -1095,13 +1112,8 @@ experience, **Heroku** is the easiest to get started with, it will automatically
- Finally, in `secrets.js` instead of `db: 'localhost'`, use the following URI with your credentials:
- `db: 'mongodb://USERNAME:PASSWORD@ds027479.mongolab.com:27479/DATABASE_NAME'`
-> **:exclamation:Note:** As an alternative to MongoLab, there is also [MongoHQ](http://www.mongohq.com/home).
+**:exclamation:Note:** As an alternative to MongoLab, there is also [MongoHQ](http://www.mongohq.com/home).
-
-- Download and install [Heroku Toolbelt](https://toolbelt.heroku.com/osx)
-- In terminal, run `heroku login` and enter your Heroku credentials
-- From *your app* directory run `heroku create`, followed by `git push heroku master`
-- Done!
- First, install this Ruby gem: `sudo gem install rhc` :gem:
@@ -1184,6 +1196,20 @@ Also, be sure to check out the [Jump-start your hackathon efforts with DevOps Se
Changelog
---------
+### 2.4.0 (November 8, 2014)
+- Bootstrap 3.3.0.
+- Flatly 3.3.0 theme.
+- User model cleanup.
+- Removed `helperContext` from connect-assets middleware.
+
+### 2.3.4 (October 27, 2014)
+- Font Awesome 4.2.0 [01e7bd5c09926911ca856fe4990e6067d9148694](https://github.com/sahat/hackathon-starter/commit/01e7bd5c09926911ca856fe4990e6067d9148694)
+- Code cleanup in `app.js` and `controllers/api.js`. [8ce48f767c0146062296685cc101acf3d5d224d9](https://github.com/sahat/hackathon-starter/commit/8ce48f767c0146062296685cc101acf3d5d224d9) [cdbb9d1888a96bbba92d4d14deec99a8acba2618](https://github.com/sahat/hackathon-starter/commit/cdbb9d1888a96bbba92d4d14deec99a8acba2618)
+- Updated Stripe API example. [afef373cd57b6a44bf856eb093e8f2801fc2dbe2](https://github.com/sahat/hackathon-starter/commit/afef373cd57b6a44bf856eb093e8f2801fc2dbe2)
+- Added 1-step deployment process with Heroku and MongoLab add-on. [c5def7b7b3b98462e9a2e7896dc11aaec1a48b3f](https://github.com/sahat/hackathon-starter/commit/c5def7b7b3b98462e9a2e7896dc11aaec1a48b3f)
+- Updated Twitter apps dashboard url. [e378fbbc24e269de69494d326bc20fcb641c0697](https://github.com/sahat/hackathon-starter/commit/e378fbbc24e269de69494d326bc20fcb641c0697)
+- Fixed dead links in the README. [78fac5489c596e8bcef0ab11a96e654335573bb4](https://github.com/sahat/hackathon-starter/commit/78fac5489c596e8bcef0ab11a96e654335573bb4)
+
### 2.3.3 (September 1, 2014)
- Use *https* (instead of http) profile image URL with Twitter authentication
diff --git a/app.js b/app.js
index 947edb57cf..5fcd7d7c3c 100644
--- a/app.js
+++ b/app.js
@@ -14,7 +14,7 @@ var methodOverride = require('method-override');
var bodyParser = require('body-parser');
var _ = require('lodash');
-var MongoStore = require('connect-mongo')({ session: session });
+var MongoStore = require('connect-mongo')(session);
var flash = require('express-flash');
var path = require('path');
var mongoose = require('mongoose');
@@ -56,13 +56,9 @@ var app = express();
mongoose.connect(secrets.db);
mongoose.connection.on('error', function() {
- console.error('MongoDB Connection Error. Make sure MongoDB is running.');
+ console.error('MongoDB Connection Error. Please make sure that MongoDB is running.');
});
-var hour = 3600000;
-var day = hour * 24;
-var week = day * 7;
-
/**
* CSRF whitelist.
*/
@@ -122,7 +118,7 @@ app.use(function(req, res, next) {
next();
});
-app.use(express.static(path.join(__dirname, 'public'), { maxAge: week }));
+app.use(express.static(path.join(__dirname, 'public'), { maxAge: 31557600000 }));
/**
* Main routes.
@@ -133,6 +129,8 @@ app.get(
'/resources/interview-questions',
resourcesController.interviewQuestions);
app.get('/learn-to-code', resourcesController.learnToCode);
+app.get('/privacy', resourcesController.privacy);
+app.get('/jquery-exercises', resourcesController.jqueryExercises);
app.get('/about', resourcesController.about);
app.get('/login', userController.getLogin);
app.post('/login', userController.postLogin);
@@ -186,6 +184,7 @@ app.post('/completed_challenge', function(req, res) {
/**
* OAuth sign-in routes.
*/
+
app.get('/auth/twitter', passport.authenticate('twitter'));
app.get(
'/auth/twitter/callback',
@@ -211,6 +210,21 @@ app.get(
res.redirect(req.session.returnTo || '/');
});
+app.get('/auth/facebook', passport.authenticate('facebook', { scope: ['email', 'user_location'] }));
+app.get('/auth/facebook/callback', passport.authenticate('facebook', { successRedirect: '/',failureRedirect: '/login' }), function(req, res) {
+ res.redirect(req.session.returnTo || '/');
+});
+
+app.get('/auth/github', passport.authenticate('github'));
+app.get('/auth/github/callback', passport.authenticate('github', { successRedirect: '/',failureRedirect: '/login' }), function(req, res) {
+ res.redirect(req.session.returnTo || '/');
+});
+
+app.get('/auth/google', passport.authenticate('google', { scope: 'profile email' }));
+app.get('/auth/google/callback', passport.authenticate('google', { successRedirect: '/',failureRedirect: '/login' }), function(req, res) {
+ res.redirect(req.session.returnTo || '/');
+});
+
/**
* 500 Error Handler.
*/
@@ -229,59 +243,26 @@ app.listen(app.get('port'), function() {
module.exports = app;
-
-/* :TODO: Add these.
-app.get('/auth/instagram', passport.authenticate('instagram'));
-app.get('/auth/instagram/callback', passport.authenticate('instagram', { successRedirect: '/',failureRedirect: '/login' }), function(req, res) {
- res.redirect(req.session.returnTo || '/');
-});
-app.get('/auth/facebook', passport.authenticate('facebook', { scope: ['email', 'user_location'] }));
-app.get('/auth/facebook/callback', passport.authenticate('facebook', { successRedirect: '/',failureRedirect: '/login' }), function(req, res) {
- res.redirect(req.session.returnTo || '/');
-});
-app.get('/auth/github', passport.authenticate('github'));
-app.get('/auth/github/callback', passport.authenticate('github', { successRedirect: '/',failureRedirect: '/login' }), function(req, res) {
- res.redirect(req.session.returnTo || '/');
-});
-app.get('/auth/google', passport.authenticate('google', { scope: 'profile email' }));
-app.get('/auth/google/callback', passport.authenticate('google', { successRedirect: '/',failureRedirect: '/login' }), function(req, res) {
- res.redirect(req.session.returnTo || '/');
-});
-
-app.get('/auth/foursquare', passport.authorize('foursquare'));
-app.get('/auth/foursquare/callback', passport.authorize('foursquare', { failureRedirect: '/api' }), function(req, res) {
- res.redirect('/api/foursquare');
-});
-app.get('/auth/tumblr', passport.authorize('tumblr'));
-app.get('/auth/tumblr/callback', passport.authorize('tumblr', { failureRedirect: '/api' }), function(req, res) {
- res.redirect('/api/tumblr');
-});
-app.get('/auth/venmo', passport.authorize('venmo', { scope: 'make_payments access_profile access_balance access_email access_phone' }));
-app.get('/auth/venmo/callback', passport.authorize('venmo', { failureRedirect: '/api' }), function(req, res) {
- res.redirect('/api/venmo');
-});
-
-app.get('/api', apiController.getApi);
-app.get('/api/lastfm', apiController.getLastfm);
-app.get('/api/nyt', apiController.getNewYorkTimes);
-app.get('/api/aviary', apiController.getAviary);
-app.get('/api/steam', apiController.getSteam);
-app.get('/api/stripe', apiController.getStripe);
-app.post('/api/stripe', apiController.postStripe);
-app.get('/api/scraping', apiController.getScraping);
-app.get('/api/twilio', apiController.getTwilio);
-app.post('/api/twilio', apiController.postTwilio);
-app.get('/api/clockwork', apiController.getClockwork);
-app.post('/api/clockwork', apiController.postClockwork);
-app.get('/api/foursquare', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getFoursquare);
-app.get('/api/tumblr', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getTumblr);
-app.get('/api/facebook', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getFacebook);
-app.get('/api/github', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getGithub);
-app.get('/api/twitter', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getTwitter);
-app.post('/api/twitter', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.postTwitter);
-app.get('/api/venmo', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getVenmo);
-app.post('/api/venmo', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.postVenmo);
-app.get('/api/linkedin', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getLinkedin);
-app.get('/api/instagram', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getInstagram);
-app.get('/api/yahoo', apiController.getYahoo);
-*/
+//app.get('/api', apiController.getApi);
+//app.get('/api/lastfm', apiController.getLastfm);
+//app.get('/api/nyt', apiController.getNewYorkTimes);
+//app.get('/api/aviary', apiController.getAviary);
+//app.get('/api/steam', apiController.getSteam);
+//app.get('/api/stripe', apiController.getStripe);
+//app.post('/api/stripe', apiController.postStripe);
+//app.get('/api/scraping', apiController.getScraping);
+//app.get('/api/twilio', apiController.getTwilio);
+//app.post('/api/twilio', apiController.postTwilio);
+//app.get('/api/clockwork', apiController.getClockwork);
+//app.post('/api/clockwork', apiController.postClockwork);
+//app.get('/api/foursquare', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getFoursquare);
+//app.get('/api/tumblr', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getTumblr);
+//app.get('/api/facebook', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getFacebook);
+//app.get('/api/github', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getGithub);
+//app.get('/api/twitter', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getTwitter);
+//app.post('/api/twitter', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.postTwitter);
+//app.get('/api/venmo', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getVenmo);
+//app.post('/api/venmo', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.postVenmo);
+//app.get('/api/linkedin', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getLinkedin);
+//app.get('/api/instagram', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getInstagram);
+//app.get('/api/yahoo', apiController.getYahoo);
diff --git a/config/passport.js b/config/passport.js
index 8ec41ce4e6..517462d41c 100644
--- a/config/passport.js
+++ b/config/passport.js
@@ -62,20 +62,25 @@ passport.use(new TwitterStrategy(secrets.twitter, function(req, accessToken, tok
} else {
User.findOne({ twitter: profile.id }, function(err, existingUser) {
- if (existingUser) return done(null, existingUser);
- var user = new User();
+ //if (existingUser) return done(null, existingUser);
// Twitter will not provide an email address. Period.
// But a person’s twitter username is guaranteed to be unique
// so we can "fake" a twitter email address as follows:
- user.email = profile.username + "@twitter.com";
+ //user.email = profile.username + "@twitter.com";
+ var user = existingUser || new User;
user.twitter = profile.id;
+ user.email = user.email || '';
user.tokens.push({ kind: 'twitter', accessToken: accessToken, tokenSecret: tokenSecret });
- user.profile.name = profile.displayName;
- user.profile.location = profile._json.location;
- user.profile.picture = profile._json.profile_image_url_https;
+ user.profile.name = user.profile.name || profile.displayName;
+ user.profile.location = user.profile.location || profile._json.location;
+ user.profile.picture = user.profile.picture || profile._json.profile_image_url_https;
user.save(function(err) {
done(err, user);
});
+ if (!user.email) {
+ res.redirect('/account');
+ req.flash('errors', { msg: 'OK, you are signed in. Please add your email address to your profile.' });
+ }
});
}
}));
@@ -106,27 +111,164 @@ passport.use(new LinkedInStrategy(secrets.linkedin, function(req, accessToken, r
User.findOne({ linkedin: profile.id }, function(err, existingUser) {
if (existingUser) return done(null, existingUser);
User.findOne({ email: profile._json.emailAddress }, function(err, existingEmailUser) {
- if (existingEmailUser) {
- req.flash('errors', { msg: 'There is already an account using this email address. Sign in to that account and link it with LinkedIn manually from Account Settings.' });
- done(err);
- } else {
- var user = new User();
- user.linkedin = profile.id;
- user.tokens.push({ kind: 'linkedin', accessToken: accessToken });
- user.email = profile._json.emailAddress;
- user.profile.name = profile.displayName;
- user.profile.location = profile._json.location.name;
- user.profile.picture = profile._json.pictureUrl;
- user.profile.website = profile._json.publicProfileUrl;
- user.save(function(err) {
- done(err, user);
- });
- }
+ var user = existingEmailUser || new User;
+ user.linkedin = profile.id;
+ user.tokens.push({ kind: 'linkedin', accessToken: accessToken });
+ user.email = user.email || profile._json.emailAddress;
+ user.profile.name = user.profile.name || profile.displayName;
+ user.profile.location = user.profile.location || profile._json.location.name;
+ user.profile.picture = user.profile.picture || profile._json.pictureUrl;
+ user.profile.website = user.profile.website || profile._json.publicProfileUrl;
+ user.challengesComplete = user.challengesCompleted || [];
+ user.save(function(err) {
+ done(err, user);
+ });
});
});
}
}));
+// Sign in using Email and Password.
+passport.use(new LocalStrategy({ usernameField: 'email' }, function(email, password, done) {
+ User.findOne({ email: email }, function(err, user) {
+ if (!user) return done(null, false, { message: 'Email ' + email + ' not found'});
+ user.comparePassword(password, function(err, isMatch) {
+ if (isMatch) {
+ return done(null, user);
+ } else {
+ return done(null, false, { message: 'Invalid email or password.' });
+ }
+ });
+ });
+}));
+
+
+// Sign in with Facebook.
+
+passport.use(new FacebookStrategy(secrets.facebook, function(req, accessToken, refreshToken, profile, done) {
+ if (req.user) {
+ User.findOne({ facebook: profile.id }, function(err, existingUser) {
+ if (existingUser) {
+ req.flash('errors', { msg: 'There is already a Facebook account that belongs to you. Sign in with that account or delete it, then link it with your current account.' });
+ done(err);
+ } else {
+ User.findById(req.user.id, function(err, user) {
+ user.facebook = profile.id;
+ user.tokens.push({ kind: 'facebook', accessToken: accessToken });
+ user.profile.name = user.profile.name || profile.displayName;
+ user.profile.gender = user.profile.gender || profile._json.gender;
+ user.profile.picture = user.profile.picture || 'https://graph.facebook.com/' + profile.id + '/picture?type=large';
+ user.save(function(err) {
+ req.flash('info', { msg: 'Facebook account has been linked.' });
+ done(err, user);
+ });
+ });
+ }
+ });
+ } else {
+ User.findOne({ facebook: profile.id }, function(err, existingUser) {
+ if (existingUser) return done(null, existingUser);
+ User.findOne({ email: profile._json.email }, function(err, existingEmailUser) {
+ var user = existingEmailUser || new User;
+ user.email = user.email || profile._json.email;
+ user.facebook = profile.id;
+ user.tokens.push({ kind: 'facebook', accessToken: accessToken });
+ user.profile.name = user.profile.name || profile.displayName;
+ user.profile.gender = user.profile.gender || profile._json.gender;
+ user.profile.picture = user.profile.picture || 'https://graph.facebook.com/' + profile.id + '/picture?type=large';
+ user.profile.location = user.profile.location || (profile._json.location) ? profile._json.location.name : '';
+ user.challengesComplete = user.challengesCompleted || [];
+ user.save(function(err) {
+ done(err, user);
+ });
+ });
+ });
+ }
+}));
+
+// Sign in with GitHub.
+
+passport.use(new GitHubStrategy(secrets.github, function(req, accessToken, refreshToken, profile, done) {
+ if (req.user) {
+ User.findOne({ github: profile.id }, function(err, existingUser) {
+ if (existingUser) {
+ req.flash('errors', { msg: 'There is already a GitHub account that belongs to you. Sign in with that account or delete it, then link it with your current account.' });
+ done(err);
+ } else {
+ User.findById(req.user.id, function(err, user) {
+ user.github = profile.id;
+ user.tokens.push({ kind: 'github', accessToken: accessToken });
+ user.profile.name = user.profile.name || profile.displayName;
+ user.profile.picture = user.profile.picture || profile._json.avatar_url;
+ user.profile.location = user.profile.location || profile._json.location;
+ user.profile.website = user.profile.website || profile._json.blog;
+ user.save(function(err) {
+ req.flash('info', { msg: 'GitHub account has been linked.' });
+ done(err, user);
+ });
+ });
+ }
+ });
+ } else {
+ User.findOne({ github: profile.id }, function(err, existingUser) {
+ if (existingUser) return done(null, existingUser);
+ User.findOne({ email: profile._json.email }, function(err, existingEmailUser) {
+ var user = existingEmailUser || new User;
+ user.email = user.email || profile._json.email;
+ user.github = profile.id;
+ user.tokens.push({ kind: 'github', accessToken: accessToken });
+ user.profile.name = user.profile.name || profile.displayName;
+ user.profile.picture = user.profile.picture || profile._json.avatar_url;
+ user.profile.location = user.profile.location || profile._json.location;
+ user.profile.website = user.profile.website || profile._json.blog;
+ user.save(function(err) {
+ done(err, user);
+ });
+ });
+ });
+ }
+}));
+
+// Sign in with Google.
+
+passport.use(new GoogleStrategy(secrets.google, function(req, accessToken, refreshToken, profile, done) {
+ if (req.user) {
+ User.findOne({ google: profile.id }, function(err, existingUser) {
+ if (existingUser) {
+ req.flash('errors', { msg: 'There is already a Google account that belongs to you. Sign in with that account or delete it, then link it with your current account.' });
+ done(err);
+ } else {
+ User.findById(req.user.id, function(err, user) {
+ user.google = profile.id;
+ user.tokens.push({ kind: 'google', accessToken: accessToken });
+ user.profile.name = user.profile.name || profile.displayName;
+ user.profile.gender = user.profile.gender || profile._json.gender;
+ user.profile.picture = user.profile.picture || profile._json.picture;
+ user.save(function(err) {
+ req.flash('info', { msg: 'Google account has been linked.' });
+ done(err, user);
+ });
+ });
+ }
+ });
+ } else {
+ User.findOne({ google: profile.id }, function(err, existingUser) {
+ if (existingUser) return done(null, existingUser);
+ User.findOne({ email: profile._json.email }, function(err, existingEmailUser) {
+ var user = existingEmailUser || new User;
+ user.email = user.email || profile._json.email;
+ user.google = profile.id;
+ user.tokens.push({ kind: 'google', accessToken: accessToken });
+ user.profile.name = user.profile.name || profile.displayName;
+ user.profile.gender = user.profile.gender || profile._json.gender;
+ user.profile.picture = user.profile.picture || profile._json.picture;
+ user.save(function(err) {
+ done(err, user);
+ });
+ });
+ });
+ }
+}));
// Login Required middleware.
@@ -193,163 +335,6 @@ passport.use(new InstagramStrategy(secrets.instagram,function(req, accessToken,
}
}));
-// Sign in using Email and Password.
-passport.use(new LocalStrategy({ usernameField: 'email' }, function(email, password, done) {
- User.findOne({ email: email }, function(err, user) {
- if (!user) return done(null, false, { message: 'Email ' + email + ' not found'});
- user.comparePassword(password, function(err, isMatch) {
- if (isMatch) {
- return done(null, user);
- } else {
- return done(null, false, { message: 'Invalid email or password.' });
- }
- });
- });
-}));
-
-
-// Sign in with Facebook.
-
-passport.use(new FacebookStrategy(secrets.facebook, function(req, accessToken, refreshToken, profile, done) {
- if (req.user) {
- User.findOne({ facebook: profile.id }, function(err, existingUser) {
- if (existingUser) {
- req.flash('errors', { msg: 'There is already a Facebook account that belongs to you. Sign in with that account or delete it, then link it with your current account.' });
- done(err);
- } else {
- User.findById(req.user.id, function(err, user) {
- user.facebook = profile.id;
- user.tokens.push({ kind: 'facebook', accessToken: accessToken });
- user.profile.name = user.profile.name || profile.displayName;
- user.profile.gender = user.profile.gender || profile._json.gender;
- user.profile.picture = user.profile.picture || 'https://graph.facebook.com/' + profile.id + '/picture?type=large';
- user.save(function(err) {
- req.flash('info', { msg: 'Facebook account has been linked.' });
- done(err, user);
- });
- });
- }
- });
- } else {
- User.findOne({ facebook: profile.id }, function(err, existingUser) {
- if (existingUser) return done(null, existingUser);
- User.findOne({ email: profile._json.email }, function(err, existingEmailUser) {
- if (existingEmailUser) {
- req.flash('errors', { msg: 'There is already an account using this email address. Sign in to that account and link it with Facebook manually from Account Settings.' });
- done(err);
- } else {
- var user = new User();
- user.email = profile._json.email;
- user.facebook = profile.id;
- user.tokens.push({ kind: 'facebook', accessToken: accessToken });
- user.profile.name = profile.displayName;
- user.profile.gender = profile._json.gender;
- user.profile.picture = 'https://graph.facebook.com/' + profile.id + '/picture?type=large';
- user.profile.location = (profile._json.location) ? profile._json.location.name : '';
- user.save(function(err) {
- done(err, user);
- });
- }
- });
- });
- }
-}));
-
-// Sign in with GitHub.
-
-passport.use(new GitHubStrategy(secrets.github, function(req, accessToken, refreshToken, profile, done) {
- if (req.user) {
- User.findOne({ github: profile.id }, function(err, existingUser) {
- if (existingUser) {
- req.flash('errors', { msg: 'There is already a GitHub account that belongs to you. Sign in with that account or delete it, then link it with your current account.' });
- done(err);
- } else {
- User.findById(req.user.id, function(err, user) {
- user.github = profile.id;
- user.tokens.push({ kind: 'github', accessToken: accessToken });
- user.profile.name = user.profile.name || profile.displayName;
- user.profile.picture = user.profile.picture || profile._json.avatar_url;
- user.profile.location = user.profile.location || profile._json.location;
- user.profile.website = user.profile.website || profile._json.blog;
- user.save(function(err) {
- req.flash('info', { msg: 'GitHub account has been linked.' });
- done(err, user);
- });
- });
- }
- });
- } else {
- User.findOne({ github: profile.id }, function(err, existingUser) {
- if (existingUser) return done(null, existingUser);
- User.findOne({ email: profile._json.email }, function(err, existingEmailUser) {
- if (existingEmailUser) {
- req.flash('errors', { msg: 'There is already an account using this email address. Sign in to that account and link it with GitHub manually from Account Settings.' });
- done(err);
- } else {
- var user = new User();
- user.email = profile._json.email;
- user.github = profile.id;
- user.tokens.push({ kind: 'github', accessToken: accessToken });
- user.profile.name = profile.displayName;
- user.profile.picture = profile._json.avatar_url;
- user.profile.location = profile._json.location;
- user.profile.website = profile._json.blog;
- user.save(function(err) {
- done(err, user);
- });
- }
- });
- });
- }
-}));
-
-
-// Sign in with Google.
-
-passport.use(new GoogleStrategy(secrets.google, function(req, accessToken, refreshToken, profile, done) {
- if (req.user) {
- User.findOne({ google: profile.id }, function(err, existingUser) {
- if (existingUser) {
- req.flash('errors', { msg: 'There is already a Google account that belongs to you. Sign in with that account or delete it, then link it with your current account.' });
- done(err);
- } else {
- User.findById(req.user.id, function(err, user) {
- user.google = profile.id;
- user.tokens.push({ kind: 'google', accessToken: accessToken });
- user.profile.name = user.profile.name || profile.displayName;
- user.profile.gender = user.profile.gender || profile._json.gender;
- user.profile.picture = user.profile.picture || profile._json.picture;
- user.save(function(err) {
- req.flash('info', { msg: 'Google account has been linked.' });
- done(err, user);
- });
- });
- }
- });
- } else {
- User.findOne({ google: profile.id }, function(err, existingUser) {
- if (existingUser) return done(null, existingUser);
- User.findOne({ email: profile._json.email }, function(err, existingEmailUser) {
- if (existingEmailUser) {
- req.flash('errors', { msg: 'There is already an account using this email address. Sign in to that account and link it with Google manually from Account Settings.' });
- done(err);
- } else {
- var user = new User();
- user.email = profile._json.email;
- user.google = profile.id;
- user.tokens.push({ kind: 'google', accessToken: accessToken });
- user.profile.name = profile.displayName;
- user.profile.gender = profile._json.gender;
- user.profile.picture = profile._json.picture;
- user.save(function(err) {
- done(err, user);
- });
- }
- });
- });
- }
-}));
-
// Tumblr API setup.
diff --git a/controllers/api.js b/controllers/api.js
index 168a4f4d2f..b395a756a6 100644
--- a/controllers/api.js
+++ b/controllers/api.js
@@ -11,7 +11,7 @@ var tumblr = require('tumblr.js');
var foursquare = require('node-foursquare')({ secrets: secrets.foursquare });
var Github = require('github-api');
var Twit = require('twit');
-var stripe = require('stripe')(secrets.stripe.apiKey);
+var stripe = require('stripe')(secrets.stripe.secretKey);
var twilio = require('twilio')(secrets.twilio.sid, secrets.twilio.token);
var Linkedin = require('node-linkedin')(secrets.linkedin.clientID, secrets.linkedin.clientSecret, secrets.linkedin.callbackURL);
var clockwork = require('clockwork')({key: secrets.clockwork.apiKey});
@@ -37,7 +37,6 @@ exports.getApi = function(req, res) {
exports.getFoursquare = function(req, res, next) {
var token = _.find(req.user.tokens, { kind: 'foursquare' });
- console.log(token);
async.parallel({
trendingVenues: function(callback) {
foursquare.Venues.getTrending('40.7222756', '-74.0022724', { limit: 50 }, token.accessToken, function(err, results) {
@@ -71,7 +70,7 @@ exports.getFoursquare = function(req, res, next) {
* Tumblr API example.
*/
-exports.getTumblr = function(req, res) {
+exports.getTumblr = function(req, res, next) {
var token = _.find(req.user.tokens, { kind: 'tumblr' });
var client = tumblr.createClient({
consumer_key: secrets.tumblr.consumerKey,
@@ -80,6 +79,7 @@ exports.getTumblr = function(req, res) {
token_secret: token.tokenSecret
});
client.posts('withinthisnightmare.tumblr.com', { type: 'photo' }, function(err, data) {
+ if (err) return next(err);
res.render('api/tumblr', {
title: 'Tumblr API',
blog: data.blog,
@@ -128,7 +128,7 @@ exports.getScraping = function(req, res, next) {
if (err) return next(err);
var $ = cheerio.load(body);
var links = [];
- $(".title a[href^='http'], a[href^='https']").each(function() {
+ $('.title a[href^="http"], a[href^="https"]').each(function() {
links.push($(this));
});
res.render('api/scraping', {
@@ -142,11 +142,13 @@ exports.getScraping = function(req, res, next) {
* GET /api/github
* GitHub API Example.
*/
-exports.getGithub = function(req, res) {
+
+exports.getGithub = function(req, res, next) {
var token = _.find(req.user.tokens, { kind: 'github' });
var github = new Github({ token: token.accessToken });
var repo = github.getRepo('sahat', 'requirejs-library');
repo.show(function(err, repo) {
+ if (err) return next(err);
res.render('api/github', {
title: 'GitHub API',
repo: repo
@@ -174,7 +176,8 @@ exports.getAviary = function(req, res) {
exports.getNewYorkTimes = function(req, res, next) {
var query = querystring.stringify({ 'api-key': secrets.nyt.key, 'list-name': 'young-adult' });
var url = 'http://api.nytimes.com/svc/books/v2/lists?' + query;
- request.get(url, function(error, request, body) {
+ request.get(url, function(err, request, body) {
+ if (err) return next(err);
if (request.statusCode === 403) return next(Error('Missing or Invalid New York Times API Key'));
var bestsellers = JSON.parse(body);
res.render('api/nyt', {
@@ -194,7 +197,7 @@ exports.getLastfm = function(req, res, next) {
async.parallel({
artistInfo: function(done) {
lastfm.request('artist.getInfo', {
- artist: 'Sirenia',
+ artist: 'The Pierces',
handlers: {
success: function(data) {
done(null, data);
@@ -207,7 +210,7 @@ exports.getLastfm = function(req, res, next) {
},
artistTopTracks: function(done) {
lastfm.request('artist.getTopTracks', {
- artist: 'Sirenia',
+ artist: 'The Pierces',
handlers: {
success: function(data) {
var tracks = [];
@@ -224,7 +227,7 @@ exports.getLastfm = function(req, res, next) {
},
artistTopAlbums: function(done) {
lastfm.request('artist.getTopAlbums', {
- artist: 'Sirenia',
+ artist: 'The Pierces',
handlers: {
success: function(data) {
var albums = [];
@@ -283,19 +286,16 @@ exports.getTwitter = function(req, res, next) {
/**
* POST /api/twitter
- * @param tweet
+ * Post a tweet.
*/
exports.postTwitter = function(req, res, next) {
req.assert('tweet', 'Tweet cannot be empty.').notEmpty();
-
var errors = req.validationErrors();
-
if (errors) {
req.flash('errors', errors);
return res.redirect('/api/twitter');
}
-
var token = _.find(req.user.tokens, { kind: 'twitter' });
var T = new Twit({
consumer_key: secrets.twitter.consumerKey,
@@ -304,6 +304,7 @@ exports.postTwitter = function(req, res, next) {
access_token_secret: token.tokenSecret
});
T.post('statuses/update', { status: req.body.tweet }, function(err, data, response) {
+ if (err) return next(err);
req.flash('success', { msg: 'Tweet has been posted.'});
res.redirect('/api/twitter');
});
@@ -317,7 +318,6 @@ exports.postTwitter = function(req, res, next) {
exports.getSteam = function(req, res, next) {
var steamId = '76561197982488301';
var query = { l: 'english', steamid: steamId, key: secrets.steam.apiKey };
-
async.parallel({
playerAchievements: function(done) {
query.appid = '49520';
@@ -330,18 +330,18 @@ exports.getSteam = function(req, res, next) {
playerSummaries: function(done) {
query.steamids = steamId;
var qs = querystring.stringify(query);
- request.get({ url: 'http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?' + qs, json: true }, function(error, request, body) {
+ request.get({ url: 'http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?' + qs, json: true }, function(err, request, body) {
if (request.statusCode === 401) return done(new Error('Missing or Invalid Steam API Key'));
- done(error, body);
+ done(err, body);
});
},
ownedGames: function(done) {
query.include_appinfo = 1;
query.include_played_free_games = 1;
var qs = querystring.stringify(query);
- request.get({ url: 'http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?' + qs, json: true }, function(error, request, body) {
+ request.get({ url: 'http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?' + qs, json: true }, function(err, request, body) {
if (request.statusCode === 401) return done(new Error('Missing or Invalid Steam API Key'));
- done(error, body);
+ done(err, body);
});
}
},
@@ -363,20 +363,19 @@ exports.getSteam = function(req, res, next) {
exports.getStripe = function(req, res) {
res.render('api/stripe', {
- title: 'Stripe API'
+ title: 'Stripe API',
+ publishableKey: secrets.stripe.publishableKey
});
};
/**
* POST /api/stripe
- * @param stipeToken
- * @param stripeEmail
+ * Make a payment.
*/
exports.postStripe = function(req, res, next) {
var stripeToken = req.body.stripeToken;
var stripeEmail = req.body.stripeEmail;
-
stripe.charges.create({
amount: 395,
currency: 'usd',
@@ -384,10 +383,10 @@ exports.postStripe = function(req, res, next) {
description: stripeEmail
}, function(err, charge) {
if (err && err.type === 'StripeCardError') {
- req.flash('errors', { msg: 'Your card has been declined.'});
+ req.flash('errors', { msg: 'Your card has been declined.' });
res.redirect('/api/stripe');
}
- req.flash('success', { msg: 'Your card has been charged successfully.'});
+ req.flash('success', { msg: 'Your card has been charged successfully.' });
res.redirect('/api/stripe');
});
};
@@ -405,28 +404,22 @@ exports.getTwilio = function(req, res) {
/**
* POST /api/twilio
- * Twilio API example.
- * @param number
- * @param message
+ * Send a text message using Twilio.
*/
exports.postTwilio = function(req, res, next) {
req.assert('number', 'Phone number is required.').notEmpty();
req.assert('message', 'Message cannot be blank.').notEmpty();
-
var errors = req.validationErrors();
-
if (errors) {
req.flash('errors', errors);
return res.redirect('/api/twilio');
}
-
var message = {
to: req.body.number,
from: '+13472235148',
body: req.body.message
};
-
twilio.sendMessage(message, function(err, responseData) {
if (err) return next(err.message);
req.flash('success', { msg: 'Text sent to ' + responseData.to + '.'});
@@ -447,8 +440,7 @@ exports.getClockwork = function(req, res) {
/**
* POST /api/clockwork
- * Clockwork SMS API example.
- * @param telephone
+ * Send a text message using Clockwork SMS
*/
exports.postClockwork = function(req, res, next) {
@@ -472,7 +464,6 @@ exports.postClockwork = function(req, res, next) {
exports.getVenmo = function(req, res, next) {
var token = _.find(req.user.tokens, { kind: 'venmo' });
var query = querystring.stringify({ access_token: token.accessToken });
-
async.parallel({
getProfile: function(done) {
request.get({ url: 'https://api.venmo.com/v1/me?' + query, json: true }, function(err, request, body) {
@@ -482,7 +473,6 @@ exports.getVenmo = function(req, res, next) {
getRecentPayments: function(done) {
request.get({ url: 'https://api.venmo.com/v1/payments?' + query, json: true }, function(err, request, body) {
done(err, body);
-
});
}
},
@@ -498,9 +488,6 @@ exports.getVenmo = function(req, res, next) {
/**
* POST /api/venmo
- * @param user
- * @param note
- * @param amount
* Send money.
*/
@@ -508,22 +495,17 @@ exports.postVenmo = function(req, res, next) {
req.assert('user', 'Phone, Email or Venmo User ID cannot be blank').notEmpty();
req.assert('note', 'Please enter a message to accompany the payment').notEmpty();
req.assert('amount', 'The amount you want to pay cannot be blank').notEmpty();
-
var errors = req.validationErrors();
-
if (errors) {
req.flash('errors', errors);
return res.redirect('/api/venmo');
}
-
var token = _.find(req.user.tokens, { kind: 'venmo' });
-
var formData = {
access_token: token.accessToken,
note: req.body.note,
amount: req.body.amount
};
-
if (validator.isEmail(req.body.user)) {
formData.email = req.body.user;
} else if (validator.isNumeric(req.body.user) &&
@@ -532,7 +514,6 @@ exports.postVenmo = function(req, res, next) {
} else {
formData.user_id = req.body.user;
}
-
request.post('https://api.venmo.com/v1/payments', { form: formData }, function(err, request, body) {
if (err) return next(err);
if (request.statusCode !== 200) {
@@ -552,7 +533,6 @@ exports.postVenmo = function(req, res, next) {
exports.getLinkedin = function(req, res, next) {
var token = _.find(req.user.tokens, { kind: 'linkedin' });
var linkedin = Linkedin.init(token.accessToken);
-
linkedin.people.me(function(err, $in) {
if (err) return next(err);
res.render('api/linkedin', {
@@ -569,10 +549,8 @@ exports.getLinkedin = function(req, res, next) {
exports.getInstagram = function(req, res, next) {
var token = _.find(req.user.tokens, { kind: 'instagram' });
-
ig.use({ client_id: secrets.instagram.clientID, client_secret: secrets.instagram.clientSecret });
ig.use({ access_token: token.accessToken });
-
async.parallel({
searchByUsername: function(done) {
ig.user_search('richellemead', function(err, users, limit) {
@@ -610,6 +588,7 @@ exports.getInstagram = function(req, res, next) {
* GET /api/yahoo
* Yahoo API example.
*/
+
exports.getYahoo = function(req, res) {
Y.YQL('SELECT * FROM weather.forecast WHERE (location = 10007)', function(response) {
var location = response.query.results.channel.location;
diff --git a/controllers/resources.js b/controllers/resources.js
index f1168e2cba..f011bcaedf 100644
--- a/controllers/resources.js
+++ b/controllers/resources.js
@@ -9,6 +9,18 @@ exports.learnToCode = function(req, res) {
});
}
+exports.privacy = function(req, res) {
+ res.render('privacy', {
+ title: 'Privacy'
+ });
+}
+
+exports.jqueryExercises = function(req, res) {
+ res.render('jquery-exercises', {
+ title: 'jQuery Exercises'
+ });
+}
+
exports.about = function(req, res) {
res.render('about', {
title: 'Who We Are'
diff --git a/controllers/user.js b/controllers/user.js
index 79969f5a45..44224a98b5 100644
--- a/controllers/user.js
+++ b/controllers/user.js
@@ -88,7 +88,7 @@ exports.postSignup = function(req, res, next) {
if (errors) {
req.flash('errors', errors);
- return res.redirect('/signup');
+ return res.redirect('/login');
}
var user = new User({
@@ -99,7 +99,7 @@ exports.postSignup = function(req, res, next) {
User.findOne({ email: req.body.email }, function(err, existingUser) {
if (existingUser) {
req.flash('errors', { msg: 'Account with that email address already exists.' });
- return res.redirect('/signup');
+ return res.redirect('/login');
}
user.save(function(err) {
if (err) return next(err);
@@ -158,7 +158,7 @@ exports.postUpdateProfile = function(req, res, next) {
if (err) return next(err);
user.email = req.body.email || '';
user.profile.name = req.body.name || '';
- user.profile.gender = req.body.gender || '';
+ user.profile.username = req.body.username || '';
user.profile.location = req.body.location || '';
user.profile.website = req.body.website || '';
@@ -309,10 +309,10 @@ exports.postReset = function(req, res, next) {
});
var mailOptions = {
to: user.email,
- from: 'hackathon@starter.com',
- subject: 'Your Hackathon Starter password has been changed',
+ from: 'Team@freecodecamp.com',
+ subject: 'Your Free Code Camp password has been changed',
text: 'Hello,\n\n' +
- 'This is a confirmation that the password for your account ' + user.email + ' has just been changed.\n'
+ 'This email is confirming that you requested to reset your password for your Free Code Camp account. This is your email: ' + user.email + '\n'
};
transporter.sendMail(mailOptions, function(err) {
req.flash('success', { msg: 'Success! Your password has been changed.' });
@@ -387,9 +387,9 @@ exports.postForgot = function(req, res, next) {
});
var mailOptions = {
to: user.email,
- from: 'hackathon@starter.com',
- subject: 'Reset your password on Hackathon Starter',
- text: 'You are receiving this email because you (or someone else) have requested the reset of the password for your account.\n\n' +
+ from: 'Team@freecodecamp.com',
+ subject: 'Reset your Free Code Camp password',
+ text: "You are receiving this email because you (or someone else) requested we reset your Free Code Camp account's password.\n\n" +
'Please click on the following link, or paste this into your browser to complete the process:\n\n' +
'http://' + req.headers.host + '/reset/' + token + '\n\n' +
'If you did not request this, please ignore this email and your password will remain unchanged.\n'
diff --git a/models/User.js b/models/User.js
index 5d9254a04e..d9598ffe77 100644
--- a/models/User.js
+++ b/models/User.js
@@ -1,17 +1,18 @@
-var mongoose = require('mongoose');
var bcrypt = require('bcrypt-nodejs');
var crypto = require('crypto');
+var mongoose = require('mongoose');
var userSchema = new mongoose.Schema({
- email: { type: String, unique: true, lowercase: true },
-// password: String,
+ //email: { type: String, unique: true, lowercase: true, match: /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/ },
+ email: String,
+ password: String,
- linkedin: String,
facebook: String,
- github: String,
twitter: String,
google: String,
+ github: String,
instagram: String,
+ linkedin: String,
tokens: Array,
challengesCompleted: { type: Array, default: [] },
challengesHash: {
@@ -221,8 +222,8 @@ var userSchema = new mongoose.Schema({
gender: { type: String, default: '' },
location: { type: String, default: '' },
website: { type: String, default: '' },
- picture: { type: String, default: '' },
- username: { type: String, default: '' }
+ picture: { type: String, default: '' }
+ //username: { type: String, default: '', unique: true, match: /^[a-zA-Z0-9_]+$/ }
},
resetPasswordToken: String,
@@ -230,20 +231,19 @@ var userSchema = new mongoose.Schema({
});
/**
- * Hash the password for security.
- * "Pre" is a Mongoose middleware that executes before each user.save() call.
+ * Password hashing Mongoose middleware.
*/
userSchema.pre('save', function(next) {
var user = this;
- if (!user.isModified('password')) return next();
+ if (!user.isModified('password')) { return next(); }
bcrypt.genSalt(5, function(err, salt) {
- if (err) return next(err);
+ if (err) { return next(err); }
bcrypt.hash(user.password, salt, null, function(err, hash) {
- if (err) return next(err);
+ if (err) { return next(err); }
user.password = hash;
next();
});
@@ -251,24 +251,22 @@ userSchema.pre('save', function(next) {
});
/**
- * Validate user's password.
- * Used by Passport-Local Strategy for password validation.
+ * Helper method for validationg user's password.
*/
userSchema.methods.comparePassword = function(candidatePassword, cb) {
bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
- if (err) return cb(err);
+ if (err) { return cb(err); }
cb(null, isMatch);
});
};
/**
- * Get URL to a user's gravatar.
- * Used in Navbar and Account Management page.
+ * Helper method for getting user's gravatar.
*/
userSchema.methods.gravatar = function(size) {
- if (!size) size = 200;
+ if (!size) { size = 200; }
if (!this.email) {
return 'https://gravatar.com/avatar/?s=' + size + '&d=retro';
@@ -278,4 +276,4 @@ userSchema.methods.gravatar = function(size) {
return 'https://gravatar.com/avatar/' + md5 + '?s=' + size + '&d=retro';
};
-module.exports = mongoose.model('User', userSchema);
\ No newline at end of file
+module.exports = mongoose.model('User', userSchema);
diff --git a/package.json b/package.json
index 2719e7f533..c5375c698a 100644
--- a/package.json
+++ b/package.json
@@ -12,63 +12,62 @@
"dependencies": {
"async": "^0.9.0",
"bcrypt-nodejs": "^0.0.3",
- "body-parser": "^1.9.2",
- "cheerio": "^0.17.0",
+ "body-parser": "^1.9.3",
+ "cheerio": "^0.18.0",
"clockwork": "^0.1.1",
- "compression": "^1.1.0",
+ "compression": "^1.2.1",
"connect-assets": "^4.3.3",
"connect-mongo": "^0.4.1",
"cookie-parser": "^1.3.3",
"csso": "^1.3.11",
"dotenv": "^0.4.0",
- "errorhandler": "^1.2.0",
- "express": "^4.9.5",
+ "errorhandler": "^1.3.0",
+ "express": "^4.10.4",
"express-flash": "^0.0.2",
- "express-session": "^1.8.2",
- "express-validator": "^2.6.0",
- "fbgraph": "^0.2.11",
+ "express-session": "^1.9.2",
+ "express-validator": "^2.7.0",
+ "fbgraph": "^0.3.0",
"github-api": "^0.7.0",
"helmet": "^0.5.2",
"instagram-node": "^0.5.1",
- "jade": "^1.7.0",
+ "jade": "^1.8.0",
"lastfm": "^0.9.2",
"less": "^1.7.5",
"lodash": "^2.4.1",
- "lusca": "^1.0.1",
- "method-override": "^2.2.0",
- "mongoose": "^3.8.16",
- "morgan": "^1.3.1",
"newrelic": "^1.13.3",
+ "lusca": "^1.0.2",
+ "method-override": "^2.3.0",
+ "mongoose": "^3.8.19",
+ "morgan": "^1.5.0",
"node-foursquare": "^0.2.1",
- "node-linkedin": "^0.3.3",
- "nodemailer": "^1.2.2",
+ "node-linkedin": "^0.3.4",
+ "nodemailer": "^1.3.0",
"passport": "^0.2.1",
"passport-facebook": "^1.0.3",
"passport-github": "^0.1.5",
"passport-google-oauth": "^0.1.5",
"passport-instagram": "^0.1.2",
- "passport-linkedin-oauth2": "^1.1.1",
+ "passport-linkedin-oauth2": "^1.2.1",
"passport-local": "^1.0.0",
"passport-oauth": "^1.0.0",
"passport-twitter": "^1.0.2",
- "request": "^2.44.0",
"sitemap": "^0.7.4",
- "socket.io": "^1.1.0",
- "stripe": "^2.8.0",
+ "request": "^2.49.0",
+ "stripe": "^3.0.2",
"tumblr.js": "^0.0.4",
- "twilio": "^1.7.0",
+ "twilio": "^1.9.0",
"twit": "^1.1.18",
"uglify-js": "^2.4.15",
- "validator": "^3.19.0",
- "yui": "^3.17.2"
+ "validator": "^3.22.1",
+ "yui": "^3.18.1"
},
"devDependencies": {
"blessed": "^0.0.37",
- "chai": "^1.9.1",
"gulp": "^3.8.8",
"gulp-inject": "^1.0.2",
- "mocha": "^1.21.4",
- "multiline": "^0.3.4",
- "supertest": "^0.13.0"
+ "chai": "^1.10.0",
+ "mocha": "^2.0.1",
+ "multiline": "^1.0.1",
+ "supertest": "^0.15.0"
}
}
diff --git a/public/css/lib/bootstrap-social.less b/public/css/lib/bootstrap-social/bootstrap-social.less
old mode 100644
new mode 100755
similarity index 88%
rename from public/css/lib/bootstrap-social.less
rename to public/css/lib/bootstrap-social/bootstrap-social.less
index 5b172141ac..82c91f5d2f
--- a/public/css/lib/bootstrap-social.less
+++ b/public/css/lib/bootstrap-social/bootstrap-social.less
@@ -14,12 +14,12 @@
.btn-social {
position: relative;
- padding-left: @bs-height-base + @padding-base-horizontal;
+ padding-left: (@bs-height-base + @padding-base-horizontal);
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- :first-child {
+ > :first-child {
position: absolute;
left: 0;
top: 0;
@@ -31,7 +31,7 @@
border-right: 1px solid rgba(0, 0, 0, 0.2);
}
&.btn-lg {
- padding-left: @bs-height-lg + @padding-large-horizontal;
+ padding-left: (@bs-height-lg + @padding-large-horizontal);
:first-child {
line-height: @bs-height-lg;
width: @bs-height-lg;
@@ -39,7 +39,7 @@
}
}
&.btn-sm {
- padding-left: @bs-height-sm + @padding-small-horizontal;
+ padding-left: (@bs-height-sm + @padding-small-horizontal);
:first-child {
line-height: @bs-height-sm;
width: @bs-height-sm;
@@ -47,7 +47,7 @@
}
}
&.btn-xs {
- padding-left: @bs-height-xs + @padding-small-horizontal;
+ padding-left: (@bs-height-xs + @padding-small-horizontal);
:first-child {
line-height: @bs-height-xs;
width: @bs-height-xs;
@@ -97,13 +97,14 @@
.btn-dropbox { .btn-social(#1087dd); }
.btn-facebook { .btn-social(#3b5998); }
.btn-flickr { .btn-social(#ff0084); }
-.btn-foursquare { .btn-social(#0072b1); }
+.btn-foursquare { .btn-social(#f94877); }
.btn-github { .btn-social(#444444); }
.btn-google-plus { .btn-social(#dd4b39); }
.btn-instagram { .btn-social(#3f729b); }
.btn-linkedin { .btn-social(#007bb6); }
.btn-microsoft { .btn-social(#2672ec); }
.btn-openid { .btn-social(#f7931e); }
+.btn-pinterest { .btn-social(#cb2027); }
.btn-reddit { .btn-social(#eff7ff, #000); }
.btn-soundcloud { .btn-social(#ff5500); }
.btn-tumblr { .btn-social(#2c4762); }
diff --git a/public/css/lib/bootstrap/.csscomb.json b/public/css/lib/bootstrap/.csscomb.json
deleted file mode 100644
index 8456e41df2..0000000000
--- a/public/css/lib/bootstrap/.csscomb.json
+++ /dev/null
@@ -1,297 +0,0 @@
-{
- "always-semicolon": true,
- "block-indent": 2,
- "colon-space": [0, 1],
- "color-case": "lower",
- "color-shorthand": true,
- "combinator-space": true,
- "element-case": "lower",
- "eof-newline": true,
- "leading-zero": false,
- "remove-empty-rulesets": true,
- "rule-indent": 2,
- "stick-brace": " ",
- "strip-spaces": true,
- "unitless-zero": true,
- "vendor-prefix-align": true,
- "sort-order": [
- [
- "position",
- "top",
- "right",
- "bottom",
- "left",
- "z-index",
- "display",
- "float",
- "width",
- "min-width",
- "max-width",
- "height",
- "min-height",
- "max-height",
- "-webkit-box-sizing",
- "-moz-box-sizing",
- "box-sizing",
- "-webkit-appearance",
- "padding",
- "padding-top",
- "padding-right",
- "padding-bottom",
- "padding-left",
- "margin",
- "margin-top",
- "margin-right",
- "margin-bottom",
- "margin-left",
- "overflow",
- "overflow-x",
- "overflow-y",
- "-webkit-overflow-scrolling",
- "-ms-overflow-x",
- "-ms-overflow-y",
- "-ms-overflow-style",
- "clip",
- "clear",
- "font",
- "font-family",
- "font-size",
- "font-style",
- "font-weight",
- "font-variant",
- "font-size-adjust",
- "font-stretch",
- "font-effect",
- "font-emphasize",
- "font-emphasize-position",
- "font-emphasize-style",
- "font-smooth",
- "-webkit-hyphens",
- "-moz-hyphens",
- "hyphens",
- "line-height",
- "color",
- "text-align",
- "-webkit-text-align-last",
- "-moz-text-align-last",
- "-ms-text-align-last",
- "text-align-last",
- "text-emphasis",
- "text-emphasis-color",
- "text-emphasis-style",
- "text-emphasis-position",
- "text-decoration",
- "text-indent",
- "text-justify",
- "text-outline",
- "-ms-text-overflow",
- "text-overflow",
- "text-overflow-ellipsis",
- "text-overflow-mode",
- "text-shadow",
- "text-transform",
- "text-wrap",
- "-webkit-text-size-adjust",
- "-ms-text-size-adjust",
- "letter-spacing",
- "-ms-word-break",
- "word-break",
- "word-spacing",
- "-ms-word-wrap",
- "word-wrap",
- "-moz-tab-size",
- "-o-tab-size",
- "tab-size",
- "white-space",
- "vertical-align",
- "list-style",
- "list-style-position",
- "list-style-type",
- "list-style-image",
- "pointer-events",
- "cursor",
- "visibility",
- "zoom",
- "flex-direction",
- "flex-order",
- "flex-pack",
- "flex-align",
- "table-layout",
- "empty-cells",
- "caption-side",
- "border-spacing",
- "border-collapse",
- "content",
- "quotes",
- "counter-reset",
- "counter-increment",
- "resize",
- "-webkit-user-select",
- "-moz-user-select",
- "-ms-user-select",
- "-o-user-select",
- "user-select",
- "nav-index",
- "nav-up",
- "nav-right",
- "nav-down",
- "nav-left",
- "background",
- "background-color",
- "background-image",
- "-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient",
- "filter:progid:DXImageTransform.Microsoft.gradient",
- "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader",
- "filter",
- "background-repeat",
- "background-attachment",
- "background-position",
- "background-position-x",
- "background-position-y",
- "-webkit-background-clip",
- "-moz-background-clip",
- "background-clip",
- "background-origin",
- "-webkit-background-size",
- "-moz-background-size",
- "-o-background-size",
- "background-size",
- "border",
- "border-color",
- "border-style",
- "border-width",
- "border-top",
- "border-top-color",
- "border-top-style",
- "border-top-width",
- "border-right",
- "border-right-color",
- "border-right-style",
- "border-right-width",
- "border-bottom",
- "border-bottom-color",
- "border-bottom-style",
- "border-bottom-width",
- "border-left",
- "border-left-color",
- "border-left-style",
- "border-left-width",
- "border-radius",
- "border-top-left-radius",
- "border-top-right-radius",
- "border-bottom-right-radius",
- "border-bottom-left-radius",
- "-webkit-border-image",
- "-moz-border-image",
- "-o-border-image",
- "border-image",
- "-webkit-border-image-source",
- "-moz-border-image-source",
- "-o-border-image-source",
- "border-image-source",
- "-webkit-border-image-slice",
- "-moz-border-image-slice",
- "-o-border-image-slice",
- "border-image-slice",
- "-webkit-border-image-width",
- "-moz-border-image-width",
- "-o-border-image-width",
- "border-image-width",
- "-webkit-border-image-outset",
- "-moz-border-image-outset",
- "-o-border-image-outset",
- "border-image-outset",
- "-webkit-border-image-repeat",
- "-moz-border-image-repeat",
- "-o-border-image-repeat",
- "border-image-repeat",
- "outline",
- "outline-width",
- "outline-style",
- "outline-color",
- "outline-offset",
- "-webkit-box-shadow",
- "-moz-box-shadow",
- "box-shadow",
- "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity",
- "-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha",
- "opacity",
- "-ms-interpolation-mode",
- "-webkit-transition",
- "-moz-transition",
- "-ms-transition",
- "-o-transition",
- "transition",
- "-webkit-transition-delay",
- "-moz-transition-delay",
- "-ms-transition-delay",
- "-o-transition-delay",
- "transition-delay",
- "-webkit-transition-timing-function",
- "-moz-transition-timing-function",
- "-ms-transition-timing-function",
- "-o-transition-timing-function",
- "transition-timing-function",
- "-webkit-transition-duration",
- "-moz-transition-duration",
- "-ms-transition-duration",
- "-o-transition-duration",
- "transition-duration",
- "-webkit-transition-property",
- "-moz-transition-property",
- "-ms-transition-property",
- "-o-transition-property",
- "transition-property",
- "-webkit-transform",
- "-moz-transform",
- "-ms-transform",
- "-o-transform",
- "transform",
- "-webkit-transform-origin",
- "-moz-transform-origin",
- "-ms-transform-origin",
- "-o-transform-origin",
- "transform-origin",
- "-webkit-animation",
- "-moz-animation",
- "-ms-animation",
- "-o-animation",
- "animation",
- "-webkit-animation-name",
- "-moz-animation-name",
- "-ms-animation-name",
- "-o-animation-name",
- "animation-name",
- "-webkit-animation-duration",
- "-moz-animation-duration",
- "-ms-animation-duration",
- "-o-animation-duration",
- "animation-duration",
- "-webkit-animation-play-state",
- "-moz-animation-play-state",
- "-ms-animation-play-state",
- "-o-animation-play-state",
- "animation-play-state",
- "-webkit-animation-timing-function",
- "-moz-animation-timing-function",
- "-ms-animation-timing-function",
- "-o-animation-timing-function",
- "animation-timing-function",
- "-webkit-animation-delay",
- "-moz-animation-delay",
- "-ms-animation-delay",
- "-o-animation-delay",
- "animation-delay",
- "-webkit-animation-iteration-count",
- "-moz-animation-iteration-count",
- "-ms-animation-iteration-count",
- "-o-animation-iteration-count",
- "animation-iteration-count",
- "-webkit-animation-direction",
- "-moz-animation-direction",
- "-ms-animation-direction",
- "-o-animation-direction",
- "animation-direction"
- ]
- ]
-}
diff --git a/public/css/lib/bootstrap/.csslintrc b/public/css/lib/bootstrap/.csslintrc
deleted file mode 100644
index 005b86236c..0000000000
--- a/public/css/lib/bootstrap/.csslintrc
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "adjoining-classes": false,
- "box-sizing": false,
- "box-model": false,
- "compatible-vendor-prefixes": false,
- "floats": false,
- "font-sizes": false,
- "gradients": false,
- "important": false,
- "known-properties": false,
- "outline-none": false,
- "qualified-headings": false,
- "regex-selectors": false,
- "shorthand": false,
- "text-indent": false,
- "unique-headings": false,
- "universal-selector": false,
- "unqualified-attributes": false
-}
diff --git a/public/css/lib/bootstrap/alerts.less b/public/css/lib/bootstrap/alerts.less
old mode 100644
new mode 100755
diff --git a/public/css/lib/bootstrap/badges.less b/public/css/lib/bootstrap/badges.less
old mode 100644
new mode 100755
diff --git a/public/css/lib/bootstrap/bootstrap.less b/public/css/lib/bootstrap/bootstrap.less
old mode 100644
new mode 100755
diff --git a/public/css/lib/bootstrap/breadcrumbs.less b/public/css/lib/bootstrap/breadcrumbs.less
old mode 100644
new mode 100755
diff --git a/public/css/lib/bootstrap/button-groups.less b/public/css/lib/bootstrap/button-groups.less
old mode 100644
new mode 100755
index 7021ecd171..fbcdf457b5
--- a/public/css/lib/bootstrap/button-groups.less
+++ b/public/css/lib/bootstrap/button-groups.less
@@ -198,7 +198,6 @@
}
-
// Justified button groups
// ----------------------
@@ -226,15 +225,23 @@
// Checkbox and radio options
//
// In order to support the browser's form validation feedback, powered by the
-// `required` attribute, we have to "hide" the inputs via `opacity`. We cannot
-// use `display: none;` or `visibility: hidden;` as that also hides the popover.
+// `required` attribute, we have to "hide" the inputs via `clip`. We cannot use
+// `display: none;` or `visibility: hidden;` as that also hides the popover.
+// Simply visually hiding the inputs via `opacity` would leave them clickable in
+// certain cases which is prevented by using `clip` and `pointer-events`.
// This way, we ensure a DOM element is visible to position the popover from.
//
-// See https://github.com/twbs/bootstrap/pull/12794 for more.
+// See https://github.com/twbs/bootstrap/pull/12794 and
+// https://github.com/twbs/bootstrap/pull/14559 for more information.
-[data-toggle="buttons"] > .btn > input[type="radio"],
-[data-toggle="buttons"] > .btn > input[type="checkbox"] {
- position: absolute;
- z-index: -1;
- .opacity(0);
+[data-toggle="buttons"] {
+ > .btn,
+ > .btn-group > .btn {
+ input[type="radio"],
+ input[type="checkbox"] {
+ position: absolute;
+ clip: rect(0,0,0,0);
+ pointer-events: none;
+ }
+ }
}
diff --git a/public/css/lib/bootstrap/buttons.less b/public/css/lib/bootstrap/buttons.less
old mode 100644
new mode 100755
index 492bdc65ae..40553c6386
--- a/public/css/lib/bootstrap/buttons.less
+++ b/public/css/lib/bootstrap/buttons.less
@@ -12,6 +12,7 @@
font-weight: @btn-font-weight;
text-align: center;
vertical-align: middle;
+ touch-action: manipulation;
cursor: pointer;
background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214
border: 1px solid transparent;
@@ -22,13 +23,15 @@
&,
&:active,
&.active {
- &:focus {
+ &:focus,
+ &.focus {
.tab-focus();
}
}
&:hover,
- &:focus {
+ &:focus,
+ &.focus {
color: @btn-default-color;
text-decoration: none;
}
@@ -43,7 +46,7 @@
&.disabled,
&[disabled],
fieldset[disabled] & {
- cursor: not-allowed;
+ cursor: @cursor-disabled;
pointer-events: none; // Future-proof disabling of clicks
.opacity(.65);
.box-shadow(none);
@@ -85,11 +88,11 @@
.btn-link {
color: @link-color;
font-weight: normal;
- cursor: pointer;
border-radius: 0;
&,
&:active,
+ &.active,
&[disabled],
fieldset[disabled] & {
background-color: transparent;
diff --git a/public/css/lib/bootstrap/carousel.less b/public/css/lib/bootstrap/carousel.less
old mode 100644
new mode 100755
index 1644ddf7f5..5724d8a56e
--- a/public/css/lib/bootstrap/carousel.less
+++ b/public/css/lib/bootstrap/carousel.less
@@ -24,6 +24,30 @@
&:extend(.img-responsive);
line-height: 1;
}
+
+ // WebKit CSS3 transforms for supported devices
+ @media all and (transform-3d), (-webkit-transform-3d) {
+ transition: transform .6s ease-in-out;
+ backface-visibility: hidden;
+ perspective: 1000;
+
+ &.next,
+ &.active.right {
+ transform: translate3d(100%, 0, 0);
+ left: 0;
+ }
+ &.prev,
+ &.active.left {
+ transform: translate3d(-100%, 0, 0);
+ left: 0;
+ }
+ &.next.left,
+ &.prev.right,
+ &.active {
+ transform: translate3d(0, 0, 0);
+ left: 0;
+ }
+ }
}
> .active,
diff --git a/public/css/lib/bootstrap/close.less b/public/css/lib/bootstrap/close.less
old mode 100644
new mode 100755
diff --git a/public/css/lib/bootstrap/code.less b/public/css/lib/bootstrap/code.less
old mode 100644
new mode 100755
index baa13df613..a08b4d48c4
--- a/public/css/lib/bootstrap/code.less
+++ b/public/css/lib/bootstrap/code.less
@@ -32,6 +32,7 @@ kbd {
kbd {
padding: 0;
font-size: 100%;
+ font-weight: bold;
box-shadow: none;
}
}
diff --git a/public/css/lib/bootstrap/component-animations.less b/public/css/lib/bootstrap/component-animations.less
old mode 100644
new mode 100755
index 9400a0d32f..967715d98b
--- a/public/css/lib/bootstrap/component-animations.less
+++ b/public/css/lib/bootstrap/component-animations.less
@@ -17,8 +17,9 @@
.collapse {
display: none;
+ visibility: hidden;
- &.in { display: block; }
+ &.in { display: block; visibility: visible; }
tr&.in { display: table-row; }
tbody&.in { display: table-row-group; }
}
@@ -27,5 +28,7 @@
position: relative;
height: 0;
overflow: hidden;
- .transition(height .35s ease);
+ .transition-property(~"height, visibility");
+ .transition-duration(.35s);
+ .transition-timing-function(ease);
}
diff --git a/public/css/lib/bootstrap/dropdowns.less b/public/css/lib/bootstrap/dropdowns.less
old mode 100644
new mode 100755
index 3eb7fc05c4..84a48c1413
--- a/public/css/lib/bootstrap/dropdowns.less
+++ b/public/css/lib/bootstrap/dropdowns.less
@@ -103,16 +103,15 @@
&:focus {
color: @dropdown-link-disabled-color;
}
-}
-// Nuke hover/focus effects
-.dropdown-menu > .disabled > a {
+
+ // Nuke hover/focus effects
&:hover,
&:focus {
text-decoration: none;
background-color: transparent;
background-image: none; // Remove CSS gradient
.reset-filter();
- cursor: not-allowed;
+ cursor: @cursor-disabled;
}
}
@@ -212,4 +211,3 @@
}
}
}
-
diff --git a/public/css/lib/bootstrap/forms.less b/public/css/lib/bootstrap/forms.less
old mode 100644
new mode 100755
index 2c5e9bfa93..38e4ce6d64
--- a/public/css/lib/bootstrap/forms.less
+++ b/public/css/lib/bootstrap/forms.less
@@ -141,7 +141,7 @@ output {
&[disabled],
&[readonly],
fieldset[disabled] & {
- cursor: not-allowed;
+ cursor: @cursor-disabled;
background-color: @input-bg-disabled;
opacity: 1; // iOS fix for unreadable disabled content
}
@@ -183,9 +183,26 @@ input[type="month"] {
&.input-sm {
line-height: @input-height-small;
+ line-height: @line-height-small ~"\0";
}
&.input-lg {
line-height: @input-height-large;
+ line-height: @line-height-large ~"\0";
+ }
+}
+
+// IE 11 hack to reverse the iOS temporal input hack.
+_:-ms-fullscreen, :root input[type="date"],
+_:-ms-fullscreen, :root input[type="time"],
+_:-ms-fullscreen, :root input[type="datetime-local"],
+_:-ms-fullscreen, :root input[type="month"] {
+ line-height: @line-height-base;
+
+ &.input-sm {
+ line-height: @line-height-small;
+ }
+ &.input-lg {
+ line-height: @line-height-large;
}
}
@@ -208,11 +225,11 @@ input[type="month"] {
.checkbox {
position: relative;
display: block;
- min-height: @line-height-computed; // clear the floating input if there is no label text
margin-top: 10px;
margin-bottom: 10px;
label {
+ min-height: @line-height-computed; // Ensure the input doesn't jump when there is no text
padding-left: 20px;
margin-bottom: 0;
font-weight: normal;
@@ -258,7 +275,7 @@ input[type="checkbox"] {
&[disabled],
&.disabled,
fieldset[disabled] & {
- cursor: not-allowed;
+ cursor: @cursor-disabled;
}
}
// These classes are used directly on