diff --git a/README.md b/README.md index 22a2f28259..5d4caf4709 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ ![Alt](https://lh4.googleusercontent.com/-PVw-ZUM9vV8/UuWeH51os0I/AAAAAAAAD6M/0Ikg7viJftQ/w1286-h566-no/hackathon-starter-logo.jpg) -Hackathon Starter [![Dependency Status](https://david-dm.org/sahat/hackathon-starter.svg)](https://david-dm.org/sahat/hackathon-starter) [![Build Status](https://travis-ci.org/sahat/hackathon-starter.svg?branch=master)](https://travis-ci.org/sahat/hackathon-starter) [![Analytics](https://ga-beacon.appspot.com/UA-47447818-2/hackathon-starter?pixel)](https://github.com/igrigorik/ga-beacon) +Hackathon Starter [![Dependency Status](https://david-dm.org/sahat/hackathon-starter/status.svg?style=flat)](https://david-dm.org/sahat/hackathon-starter) [![Build Status](http://img.shields.io/travis/sahat/hackathon-starter.svg?style=flat)](https://travis-ci.org/sahat/hackathon-starter) [![Analytics](https://ga-beacon.appspot.com/UA-47447818-2/hackathon-starter?pixel)](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