From 52ac42212d0160b951394d057ec0b6af0b09871a Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 11:18:08 -0500 Subject: [PATCH 01/18] Add passport-linkedin package --- package.json | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index ab24dc2065..b9ce7b3629 100755 --- a/package.json +++ b/package.json @@ -2,8 +2,8 @@ "name": "hackathon-starter", "version": "0.0.0", "repository": { - "type" : "git", - "url" : "https://github.com/sahat/hackathon-starter.git" + "type": "git", + "url": "https://github.com/sahat/hackathon-starter.git" }, "scripts": { "start": "node app.js", @@ -14,6 +14,8 @@ "bcrypt-nodejs": "~0.0.3", "cheerio": "~0.13.1", "connect-assets": "~3.0.0-beta1", + "connect-mongo": "~0.4.0", + "csso": "~1.3.11", "express": "~3.4.8", "express-flash": "~0.0.2", "express-validator": "~1.0.1", @@ -29,19 +31,18 @@ "passport-facebook": "~1.0.2", "passport-github": "~0.1.5", "passport-google-oauth": "~0.1.5", + "passport-linkedin": "~0.1.3", "passport-local": "~0.1.6", "passport-oauth": "~1.0.0", "passport-twitter": "~1.0.2", + "paypal-rest-sdk": "~0.6.4", "request": "~2.34.0", "tumblr.js": "~0.0.4", + "twilio": "~1.5.0", "twit": "~1.1.12", "underscore": "~1.6.0", - "paypal-rest-sdk": "~0.6.4", - "connect-mongo": "~0.4.0", - "twilio": "~1.5.0", - "validator": "~3.3.0", - "csso": "~1.3.11", - "uglify-js": "~2.4.12" + "uglify-js": "~2.4.12", + "validator": "~3.3.0" }, "devDependencies": { "chai": "~1.9.0", From b09223a0cca733b86beaaaf384dcaefbd0812bd5 Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 11:28:31 -0500 Subject: [PATCH 02/18] Add linkedin /auth and /auth/callback routes --- app.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app.js b/app.js index 4dc6f8716d..a99de89a86 100755 --- a/app.js +++ b/app.js @@ -145,6 +145,8 @@ app.get('/auth/google', passport.authenticate('google', { scope: 'profile email' app.get('/auth/google/callback', passport.authenticate('google', { successRedirect: '/', failureRedirect: '/login' })); app.get('/auth/twitter', passport.authenticate('twitter')); app.get('/auth/twitter/callback', passport.authenticate('twitter', { successRedirect: '/', failureRedirect: '/login' })); +app.get('/auth/linkedin', passport.authenticate('linkedin', { scope: ['r_basicprofile', 'r_emailaddress'] })); +app.get('/auth/linkedin/callback', passport.authenticate('linkedin', { successRedirect: '/', failureRedirect: '/login' })); /** * OAuth routes for API examples that require authorization. From 0ced58c9f557aeb64f8452f39a87b95353a66bdc Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 11:28:48 -0500 Subject: [PATCH 03/18] Add linkedin id field to User model --- models/User.js | 1 + 1 file changed, 1 insertion(+) diff --git a/models/User.js b/models/User.js index ef1a3d8683..4660e7fd98 100644 --- a/models/User.js +++ b/models/User.js @@ -10,6 +10,7 @@ var userSchema = new mongoose.Schema({ twitter: { type: String, unique: true, sparse: true }, google: { type: String, unique: true, sparse: true }, github: { type: String, unique: true, sparse: true }, + linkedin: { type: String, unique: true, sparse: true }, tokens: Array, profile: { From feabdec3aa1b727bed698148fcf401e56b4b7386 Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 11:48:10 -0500 Subject: [PATCH 04/18] Add linkedin button styles --- public/css/styles.less | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/public/css/styles.less b/public/css/styles.less index f5c0b93106..76eac541d3 100644 --- a/public/css/styles.less +++ b/public/css/styles.less @@ -93,6 +93,16 @@ body { } } +.btn-linkedin { + color: #fff; + background: #007bb6;; + border: 1px solid rgba(0, 0, 0, 0.07); + + &:hover { + color: #fff; + } +} + // Extra space between font-awesome icons and text [class^="fa-"], [class*="fa-"] { From 31723cdcc4ce95b8fdc29b29287614626083587c Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 12:33:50 -0500 Subject: [PATCH 05/18] Replaced custom social button styles with bootstrap-social css library --- public/css/lib/bootstrap-social.less | 140 +++++++++++++++++++++++++++ public/css/styles.less | 52 +--------- 2 files changed, 143 insertions(+), 49 deletions(-) create mode 100755 public/css/lib/bootstrap-social.less diff --git a/public/css/lib/bootstrap-social.less b/public/css/lib/bootstrap-social.less new file mode 100755 index 0000000000..c46b1bab28 --- /dev/null +++ b/public/css/lib/bootstrap-social.less @@ -0,0 +1,140 @@ +/* + * Social Buttons for Bootstrap + * + * Copyright 2013-2014 Panayiotis Lipiridis + * Licensed under the MIT License + * + * https://github.com/lipis/bootstrap-social + */ + +.btn-social { + @bs-height-base: (@line-height-computed + @padding-base-vertical * 2); + @bs-height-lg: (floor(@font-size-large * @line-height-base) + @padding-large-vertical * 2); + @bs-height-sm: (floor(@font-size-small * 1.5) + @padding-small-vertical * 2); + @bs-height-xs: (floor(@font-size-small * 1.2) + @padding-small-vertical + 1); + + position: relative; + padding-left: @bs-height-base + @padding-base-horizontal; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + :first-child { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: @bs-height-base; + line-height: (@bs-height-base + 2); + font-size: 1.6em; + text-align: center; + border-right: 1px solid rgba(0, 0, 0, 0.2); + } + &.btn-lg { + padding-left: @bs-height-lg + @padding-large-horizontal; + :first-child { + line-height: @bs-height-lg; + width: @bs-height-lg; + font-size: 1.8em; + } + } + &.btn-sm { + padding-left: @bs-height-sm + @padding-small-horizontal; + :first-child { + line-height: @bs-height-sm; + width: @bs-height-sm; + font-size: 1.4em; + } + } + &.btn-xs { + padding-left: @bs-height-xs + @padding-small-horizontal; + :first-child { + line-height: @bs-height-xs; + width: @bs-height-xs; + font-size: 1.2em; + } + } +} + +.btn-social-icon { + .btn-social; + height: (@bs-height-base + 2); + width: (@bs-height-base + 2); + padding: 0; + :first-child { + border: none; + text-align: center; + width: 100% !important; + } + &.btn-lg { + height: @bs-height-lg; + width: @bs-height-lg; + padding-left: 0; + padding-right: 0; + } + &.btn-sm { + height: (@bs-height-sm + 2); + width: (@bs-height-sm + 2); + padding-left: 0; + padding-right: 0; + } + &.btn-xs { + height: (@bs-height-xs + 2); + width: (@bs-height-xs + 2); + padding-left: 0; + padding-right: 0; + } +} + +.btn-social(@color-bg, @color: white) { + background-color: @color-bg; + .button-variant(@color, @color-bg, rgba(0, 0, 0, 0.2)); +} + +.btn-bitbucket { + .btn-social(#205081); +} + +.btn-dropbox { + .btn-social(#1087dd); +} + +.btn-facebook { + .btn-social(#3b5998); +} + +.btn-flickr { + .btn-social(#ff0084); +} + +.btn-foursquare { + .btn-social(#0072b1); +} + +.btn-github { + .btn-social(#444444); +} + +.btn-google-plus { + .btn-social(#dd4b39); +} + +.btn-instagram { + .btn-social(#3f729b); +} + +.btn-linkedin { + .btn-social(#007bb6); +} + +.btn-tumblr { + .btn-social(#2c4762); +} + +.btn-twitter { + .btn-social(#55acee); +} + +.btn-vk { + .btn-social(#587ea3); +} diff --git a/public/css/styles.less b/public/css/styles.less index 76eac541d3..8762fe87a7 100644 --- a/public/css/styles.less +++ b/public/css/styles.less @@ -1,6 +1,7 @@ @import (less) "lib/animate.css"; @import (less) "lib/font-awesome.min.css"; @import "lib/bootstrap/bootstrap"; +@import "lib/bootstrap-social"; @import "themes/default"; // Scaffolding @@ -52,55 +53,8 @@ body { // Social Buttons // ------------------------- - -.btn-facebook { - color: #fff; - background: #3b5998; - border: 1px solid rgba(0, 0, 0, 0.07); - - &:hover { - color: #fff; - } -} - -.btn-twitter { - color: #fff; - background: #00aced; - border: 1px solid rgba(0, 0, 0, 0.07); - - &:hover { - color: #fff; - } -} - -.btn-google-plus { - color: #fff; - background: #dd4b39; - border: 1px solid rgba(0, 0, 0, 0.07); - - &:hover { - color: #fff; - } -} - -.btn-github { - color: #fff; - background: #333; - border: 1px solid rgba(0, 0, 0, 0.07); - - &:hover { - color: #fff; - } -} - -.btn-linkedin { - color: #fff; - background: #007bb6;; - border: 1px solid rgba(0, 0, 0, 0.07); - - &:hover { - color: #fff; - } +.btn-social { + border-radius: 4px; } // Extra space between font-awesome icons and text From bf02667e9e7a53a4eb348136383490caf2373c5c Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 12:39:51 -0500 Subject: [PATCH 06/18] Redesigned Login page (now includes Sign in with LinkedIn) --- public/css/styles.less | 8 +---- views/account/login.jade | 70 +++++++++++++++++++++------------------- 2 files changed, 38 insertions(+), 40 deletions(-) diff --git a/public/css/styles.less b/public/css/styles.less index 8762fe87a7..e816435690 100644 --- a/public/css/styles.less +++ b/public/css/styles.less @@ -51,12 +51,6 @@ body { height: 45px; } -// Social Buttons -// ------------------------- -.btn-social { - border-radius: 4px; -} - // Extra space between font-awesome icons and text [class^="fa-"], [class*="fa-"] { @@ -83,7 +77,7 @@ body { .facebook-caption { position: absolute; - background-color: rgba(0,0,0,0.5); + background-color: rgba(0, 0, 0, 0.5); font-size: 12px; color: #fff; padding: 3px; diff --git a/views/account/login.jade b/views/account/login.jade index 2eeee1ceb8..068bed1c74 100644 --- a/views/account/login.jade +++ b/views/account/login.jade @@ -1,37 +1,41 @@ extends ../layout block content - .col-sm-8.col-sm-offset-2 - form(method='POST') - legend Sign In - input(type='hidden', name='_csrf', value=token) - .form-group - .btn-group.btn-group-justified - if secrets.facebookAuth - a.btn.btn-facebook(href='/auth/facebook') - i.fa.fa-facebook - | Facebook - if secrets.twitterAuth - a.btn.btn-twitter(href='/auth/twitter') - i.fa.fa-twitter - | Twitter - if secrets.githubAuth - a.btn.btn-github(href='/auth/github') - i.fa.fa-github - | GitHub - if secrets.googleAuth - a.btn.btn-google-plus(href='/auth/google') - i.fa.fa-google-plus - | Google + form(method='POST') + legend Sign In + input(type='hidden', name='_csrf', value=token) + .row + .col-sm-4 + if secrets.facebookAuth + a.btn.btn-block.btn-facebook.btn-social(href='/auth/facebook') + i.fa.fa-facebook + | Sign in with Facebook + if secrets.twitterAuth + a.btn.btn-block.btn-twitter.btn-social(href='/auth/twitter') + i.fa.fa-twitter + | Sign in with Twitter + if secrets.googleAuth + a.btn.btn-block.btn-google-plus.btn-social(href='/auth/google') + i.fa.fa-google-plus + | Sign in with Google + if secrets.githubAuth + a.btn.btn-block.btn-github.btn-social(href='/auth/github') + i.fa.fa-github + | Sign in with GitHub + if secrets.linkedinAuth + a.btn.btn-block.btn-linkedin.btn-social(href='/auth/linkedin') + i.fa.fa-linkedin + | Sign in with LinkedIn if secrets.localAuth - .form-group - label.control-label(for='email') Email - input.form-control(type='text', name='email', id='email', placeholder='Email', autofocus=true) - .form-group - label.control-label(for='password') Password - input.form-control(type='password', name='password', id='password', placeholder='Password') - .form-group - button.btn.btn-primary(type='submit') - i.fa.fa-unlock-alt - | Login - a.btn.btn-link(href='/forgot') Forgot your password? + .col-sm-8 + .form-group + label.control-label(for='email') Email + input.form-control(type='text', name='email', id='email', placeholder='Email', autofocus=true) + .form-group + label.control-label(for='password') Password + input.form-control(type='password', name='password', id='password', placeholder='Password') + .form-group + button.btn.btn-primary(type='submit') + i.fa.fa-unlock-alt + | Login + a.btn.btn-link(href='/forgot') Forgot your password? From 413bd498018984cd0cd770aeb3c6bd2540fd09dc Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 12:44:01 -0500 Subject: [PATCH 07/18] Updated passport-linked library that's compatible with Oauth2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b9ce7b3629..e334518d18 100755 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "passport-facebook": "~1.0.2", "passport-github": "~0.1.5", "passport-google-oauth": "~0.1.5", - "passport-linkedin": "~0.1.3", + "passport-linkedin-oauth2": "~1.1.1", "passport-local": "~0.1.6", "passport-oauth": "~1.0.0", "passport-twitter": "~1.0.2", From c95ee75eeebd98f2950cab001920a6254abe601d Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 13:08:26 -0500 Subject: [PATCH 08/18] Add LinkedIn passport strategy --- config/passport.js | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/config/passport.js b/config/passport.js index a1a3f5c902..3215e4e3bb 100755 --- a/config/passport.js +++ b/config/passport.js @@ -5,6 +5,7 @@ var FacebookStrategy = require('passport-facebook').Strategy; var TwitterStrategy = require('passport-twitter').Strategy; var GitHubStrategy = require('passport-github').Strategy; var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; +var LinkedInStrategy = require('passport-linkedin-oauth2').Strategy; var OAuthStrategy = require('passport-oauth').OAuthStrategy; // Tumblr var OAuth2Strategy = require('passport-oauth').OAuth2Strategy; // Venmo, Foursquare var User = require('../models/User'); @@ -229,6 +230,59 @@ passport.use(new GoogleStrategy(secrets.google, function(req, accessToken, refre } })); +/** + * Sign in with LinkedIn. + */ + +passport.use(new LinkedInStrategy(secrets.linkedin, function(req, accessToken, refreshToken, profile, done) { + if (req.user) { + User.findOne({ $or: [ + { linkedin: profile.id }, + { email: profile._json.emailAddress } + ] }, function(err, existingUser) { + if (existingUser) { + req.flash('errors', { msg: 'There is already a LinkedIn 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.linkedin = profile.id; + user.tokens.push({ kind: 'linkedin', accessToken: accessToken }); + 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.save(function(err) { + req.flash('info', { msg: 'LinkedIn account has been linked.' }); + done(err, user); + }); + }); + } + }); + } else { + 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 Google 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); + }); + } + }); + }); + } +})); + /** * Tumblr API * Uses OAuth 1.0a Strategy. From af4a2a33f66ef7a9ee8055e97ebeb6a7f3d336e5 Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 13:10:50 -0500 Subject: [PATCH 09/18] Add link/unlink LinkedIn to profile template --- views/account/profile.jade | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/views/account/profile.jade b/views/account/profile.jade index e45f8b433a..047d077389 100644 --- a/views/account/profile.jade +++ b/views/account/profile.jade @@ -98,3 +98,9 @@ block content p: a.text-danger(href='/account/unlink/github') Unlink your GitHub account else p: a(href='/auth/github') Link your GitHub account + + if secrets.linkedinAuth + if user.linkedin + p: a.text-danger(href='/account/unlink/linkedin') Unlink your LinkedIn account + else + p: a(href='/auth/linkedin') Link your LinkedIn account From 1bb2fd89a44e845ed3e33f3637739dc886f2167f Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 13:14:09 -0500 Subject: [PATCH 10/18] Updated LinkedIn auth routes to include mandatory "state" object --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index a99de89a86..bff1864789 100755 --- a/app.js +++ b/app.js @@ -145,7 +145,7 @@ app.get('/auth/google', passport.authenticate('google', { scope: 'profile email' app.get('/auth/google/callback', passport.authenticate('google', { successRedirect: '/', failureRedirect: '/login' })); app.get('/auth/twitter', passport.authenticate('twitter')); app.get('/auth/twitter/callback', passport.authenticate('twitter', { successRedirect: '/', failureRedirect: '/login' })); -app.get('/auth/linkedin', passport.authenticate('linkedin', { scope: ['r_basicprofile', 'r_emailaddress'] })); +app.get('/auth/linkedin', passport.authenticate('linkedin', { state: 'SOME STATE' })); app.get('/auth/linkedin/callback', passport.authenticate('linkedin', { successRedirect: '/', failureRedirect: '/login' })); /** From db09dc57bee531575f357fd8167672f70ea4179d Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 13:18:05 -0500 Subject: [PATCH 11/18] Add LinkedIn obtaining api keys instructions --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 6e794ebdc5..b1c1aea3a8 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,23 @@ app.get('/auth/facebook/callback', passport.authenticate('facebook', { successRe
+ +- Sign in at [LinkedIn Developer Network](http://developer.linkedin.com/) +- From the account name dropdown menu select **API Keys** + - It might ask you to sign in once again +- Click on **+ Add New Application** +- Fill in all the required fields +- For **Default Scope** make sure *at least* the following is checked: + - **r_basicprofile** + - **r_emailaddress** +- For **OAuth 1.0 Accept Redirect URL**: http://localhost:3000/ +- Click on **Add Application** button +- Copy and paste *OAuth User Token* and *OAuth User Secret* keys into `config/secrets.js` + - *API Key* is your **clientID** + - *Secret Key* is your **clientSecret** + +
+ - Visit the **Account** section of your Venmo profile after logging in - Click on the **Developers** tab From 1c71789d0704092a3f99d1363d9b036aaa2b5538 Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 13:22:47 -0500 Subject: [PATCH 12/18] Updated LinkedIn instructions --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b1c1aea3a8..5e0e2818b6 100644 --- a/README.md +++ b/README.md @@ -177,18 +177,18 @@ app.get('/auth/facebook/callback', passport.authenticate('facebook', { successRe
- + - Sign in at [LinkedIn Developer Network](http://developer.linkedin.com/) - From the account name dropdown menu select **API Keys** - - It might ask you to sign in once again -- Click on **+ Add New Application** + - *It may ask you to sign in once again* +- Click **+ Add New Application** button - Fill in all the required fields - For **Default Scope** make sure *at least* the following is checked: - - **r_basicprofile** - - **r_emailaddress** + - `r_basicprofile` + - `r_emailaddress` - For **OAuth 1.0 Accept Redirect URL**: http://localhost:3000/ -- Click on **Add Application** button -- Copy and paste *OAuth User Token* and *OAuth User Secret* keys into `config/secrets.js` +- Finish by clicking **Add Application** button +- Copy and paste *API Key* and *Secret Key* keys into `config/secrets.js` - *API Key* is your **clientID** - *Secret Key* is your **clientSecret** From 67b19e3ab2cb0b5a608b58ab9edb17c8c5f56c73 Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 13:24:02 -0500 Subject: [PATCH 13/18] Removed OAuth 1.0 Accept Redirect URL from LinkedIn README --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 5e0e2818b6..65b6119660 100644 --- a/README.md +++ b/README.md @@ -182,11 +182,10 @@ app.get('/auth/facebook/callback', passport.authenticate('facebook', { successRe - From the account name dropdown menu select **API Keys** - *It may ask you to sign in once again* - Click **+ Add New Application** button -- Fill in all the required fields +- Fill out all *required* fields - For **Default Scope** make sure *at least* the following is checked: - `r_basicprofile` - `r_emailaddress` -- For **OAuth 1.0 Accept Redirect URL**: http://localhost:3000/ - Finish by clicking **Add Application** button - Copy and paste *API Key* and *Secret Key* keys into `config/secrets.js` - *API Key* is your **clientID** From f80546bf8452c8c04ef3d6c26d2f7b9b63ce86f5 Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 14:48:09 -0500 Subject: [PATCH 14/18] Add LinkedIn API example route and controller --- app.js | 1 + controllers/api.js | 77 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/app.js b/app.js index bff1864789..3179e4d77e 100755 --- a/app.js +++ b/app.js @@ -132,6 +132,7 @@ app.get('/api/github', passportConf.isAuthenticated, passportConf.isAuthorized, app.get('/api/twitter', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getTwitter); app.get('/api/venmo', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getVenmo); app.post('/api/venmo', passportConf.isAuthenticated, passportConf.isAuthorized, apiController.postVenmo); +app.get('/api/linkedin', apiController.getLinkedin); /** * OAuth routes for sign-in. diff --git a/controllers/api.js b/controllers/api.js index 99a63fdddb..ce2494bd18 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -14,6 +14,7 @@ var Github = require('github-api'); var Twit = require('twit'); var paypal = require('paypal-rest-sdk'); var twilio = require('twilio')(secrets.twilio.sid, secrets.twilio.token); +var Linkedin = require('node-linkedin')(secrets.linkedin.clientID, secrets.linkedin.clientSecret, secrets.linkedin.callbackURL); /** * GET /api @@ -420,26 +421,26 @@ exports.getVenmo = function(req, res, next) { 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) { - done(err, body); - }); - }, - getRecentPayments: function(done) { - request.get({ url: 'https://api.venmo.com/v1/payments?' + query, json: true }, function(err, request, body) { - done(err, body); + getProfile: function(done) { + request.get({ url: 'https://api.venmo.com/v1/me?' + query, json: true }, function(err, request, body) { + done(err, body); + }); + }, + getRecentPayments: function(done) { + request.get({ url: 'https://api.venmo.com/v1/payments?' + query, json: true }, function(err, request, body) { + done(err, body); + }); + } + }, + function(err, results) { + if (err) return next(err); + res.render('api/venmo', { + title: 'Venmo API', + profile: results.getProfile.data, + recentPayments: results.getRecentPayments.data }); - } - }, - function(err, results) { - if (err) return next(err); - res.render('api/venmo', { - title: 'Venmo API', - profile: results.getProfile.data, - recentPayments: results.getRecentPayments.data }); - }); }; exports.postVenmo = function(req, res, next) { @@ -482,3 +483,45 @@ exports.postVenmo = function(req, res, next) { res.redirect('/api/venmo'); }); }; + +exports.getLinkedin = function(req, res, next) { + var token = _.findWhere(req.user.tokens, { kind: 'linkedin' }); + var linkedin = Linkedin.init(token); + + async.parallel({ + profile: function(done) { + linkedin.people.me(function(err, $in) { + console.log($in); + done(err, $in); + }); + }, + profileById: function(doone) { + linkedin.people.url('linkedin_id', function(err, $in) { + console.log($in); + done(err, $in); + }); + }, + connections: function(done) { + linkedin.connections.me(function(err, $in) { + console.log($in); + done(err, $in); + }); + }, + companies: function(done) { + linkedin.companies.me(function(err, $in) { + console.log($in); + done(err, $in); + }); + } + }, + function(err, results) { + if (err) return next(err); + res.render('api/linkedin', { + title: 'LinkedIn API', + profile: results.profile, + profileById: results.profileById, + connections: results.connections, + companies: results.companies + }); + }); +}; From 6f744ccf83713adca400d439ddf545d1711bc525 Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 14:49:27 -0500 Subject: [PATCH 15/18] Add node-linked library for making linkedin api requests --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index e334518d18..2416aa00f6 100755 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "less": "~1.6.3", "mongoose": "~3.8.7", "node-foursquare": "~0.2.0", + "node-linkedin": "~0.1.4", "nodemailer": "~0.6.0", "passport": "~0.2.0", "passport-facebook": "~1.0.2", From c3320f28f297350d4742031e97d83e9e481128f1 Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 16:51:38 -0500 Subject: [PATCH 16/18] Added LinkedIn API example that displays connection and your profile information --- README.md | 3 ++- controllers/api.js | 15 ++++--------- views/api/index.jade | 3 +++ views/api/linkedin.jade | 49 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 12 deletions(-) create mode 100644 views/api/linkedin.jade diff --git a/README.md b/README.md index 65b6119660..7e98d323e7 100644 --- a/README.md +++ b/README.md @@ -184,8 +184,9 @@ app.get('/auth/facebook/callback', passport.authenticate('facebook', { successRe - Click **+ Add New Application** button - Fill out all *required* fields - For **Default Scope** make sure *at least* the following is checked: - - `r_basicprofile` + - `r_fullprofile` - `r_emailaddress` + - `r_network` - Finish by clicking **Add Application** button - Copy and paste *API Key* and *Secret Key* keys into `config/secrets.js` - *API Key* is your **clientID** diff --git a/controllers/api.js b/controllers/api.js index ce2494bd18..c5d1209336 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -486,7 +486,7 @@ exports.postVenmo = function(req, res, next) { exports.getLinkedin = function(req, res, next) { var token = _.findWhere(req.user.tokens, { kind: 'linkedin' }); - var linkedin = Linkedin.init(token); + var linkedin = Linkedin.init(token.accessToken); async.parallel({ profile: function(done) { @@ -495,20 +495,14 @@ exports.getLinkedin = function(req, res, next) { done(err, $in); }); }, - profileById: function(doone) { - linkedin.people.url('linkedin_id', function(err, $in) { - console.log($in); - done(err, $in); - }); - }, connections: function(done) { - linkedin.connections.me(function(err, $in) { - console.log($in); + linkedin.connections.retrieve(function(err, $in) { +// console.log($in); done(err, $in); }); }, companies: function(done) { - linkedin.companies.me(function(err, $in) { + linkedin.companies.company('http://www.linkedin.com/company/continuum-analytics-inc-', function(err, $in) { console.log($in); done(err, $in); }); @@ -519,7 +513,6 @@ exports.getLinkedin = function(req, res, next) { res.render('api/linkedin', { title: 'LinkedIn API', profile: results.profile, - profileById: results.profileById, connections: results.connections, companies: results.companies }); diff --git a/views/api/index.jade b/views/api/index.jade index 9798ac46fb..0ef57f499e 100644 --- a/views/api/index.jade +++ b/views/api/index.jade @@ -17,6 +17,9 @@ block content small ⇢ Login Required li a(href='/api/lastfm') Last.fm + li + a(href='/api/linkedin') LinkedIn + small ⇢ Login Required li a(href='/api/nyt') New York Times li diff --git a/views/api/linkedin.jade b/views/api/linkedin.jade new file mode 100644 index 0000000000..a300c0fa6a --- /dev/null +++ b/views/api/linkedin.jade @@ -0,0 +1,49 @@ +extends ../layout + +block content + .page-header + h2 + i.fa.fa-linkedin-square + | LinkedIn API + + .btn-group.btn-group-justified + a.btn.btn-primary(href='https://github.com/Kuew/node-linkedin', target='_blank') + i.fa.fa-book + | Node LinkedIn Docs + a.btn.btn-primary(href='http://developer.linkedin.com/documents/authentication', target='_blank') + i.fa.fa-check-square-o + | Getting Started + a.btn.btn-primary(href='http://developer.linkedin.com/apis', target='_blank') + i.fa.fa-code-fork + | API Endpoints + + h3.text-primary My Profile + .well.well-sm + .row + .col-xs-2 + img(src='#{profile.pictureUrl}') + .col-xs-10 + h3= profile.formattedName + h4= profile.headline + span.text-muted #{profile.location.name} | #{profile.industry} + .row + hr + dl.dl-horizontal + dt.text-muted Current + dd A description list is perfect for defining terms. + dt.text-muted Education + for education in profile.educations.values + dd= education.schoolName + dt.text-muted Recommendations + dd #{profile.recommendationsReceived} recommendation(s) received + dt.text-muted Connections + dd #{profile.numConnections} connections + + h3 Connections + .row + for connection in connections.values + .col-xs-3.col-md-2 + img(src='#{connection.pictureUrl}') + div.facebook-caption= connection.firstName + + h3 Company Information From 582cdf4e544ee558a3efa3ec578849f32f4fa903 Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 17:29:16 -0500 Subject: [PATCH 17/18] A bunch of improvements to Linkedin API --- controllers/api.js | 35 ++++--------------- views/api/linkedin.jade | 74 ++++++++++++++++++++++++++--------------- 2 files changed, 55 insertions(+), 54 deletions(-) diff --git a/controllers/api.js b/controllers/api.js index c5d1209336..86b9dcb3f0 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -488,33 +488,12 @@ exports.getLinkedin = function(req, res, next) { var token = _.findWhere(req.user.tokens, { kind: 'linkedin' }); var linkedin = Linkedin.init(token.accessToken); - async.parallel({ - profile: function(done) { - linkedin.people.me(function(err, $in) { - console.log($in); - done(err, $in); - }); - }, - connections: function(done) { - linkedin.connections.retrieve(function(err, $in) { -// console.log($in); - done(err, $in); - }); - }, - companies: function(done) { - linkedin.companies.company('http://www.linkedin.com/company/continuum-analytics-inc-', function(err, $in) { - console.log($in); - done(err, $in); - }); - } - }, - function(err, results) { - if (err) return next(err); - res.render('api/linkedin', { - title: 'LinkedIn API', - profile: results.profile, - connections: results.connections, - companies: results.companies - }); + linkedin.people.me(function(err, $in) { + if (err) return next(err); + console.log($in.positions.values); + res.render('api/linkedin', { + title: 'LinkedIn API', + profile: $in }); + }); }; diff --git a/views/api/linkedin.jade b/views/api/linkedin.jade index a300c0fa6a..0620c98d7d 100644 --- a/views/api/linkedin.jade +++ b/views/api/linkedin.jade @@ -17,33 +17,55 @@ block content i.fa.fa-code-fork | API Endpoints - h3.text-primary My Profile + h3.text-primary My LinkedIn Profile .well.well-sm .row - .col-xs-2 - img(src='#{profile.pictureUrl}') - .col-xs-10 - h3= profile.formattedName - h4= profile.headline - span.text-muted #{profile.location.name} | #{profile.industry} + .col-sm-12 + .col-sm-2 + br + img.thumbnail(src='#{profile.pictureUrl}') + .col-sm-10 + h3= profile.formattedName + h4= profile.headline + span.text-muted #{profile.location.name} | #{profile.industry} + br .row - hr - dl.dl-horizontal - dt.text-muted Current - dd A description list is perfect for defining terms. - dt.text-muted Education - for education in profile.educations.values - dd= education.schoolName - dt.text-muted Recommendations - dd #{profile.recommendationsReceived} recommendation(s) received - dt.text-muted Connections - dd #{profile.numConnections} connections + .col-sm-12 + dl.dl-horizontal + dt.text-muted Current + for company in profile.positions.values + if company.isCurrent + dd + strong= company.title + | at + strong #{company.company.name} + dt.text-muted Previous + for company in profile.positions.values + if !company.isCurrent + dd + | #{company.title} + | at + | #{company.company.name} + dt.text-muted Education + for education in profile.educations.values + dd= education.schoolName + dt.text-muted Recommendations + dd + strong #{profile.numRecommenders} + | recommendation(s) received + dt.text-muted Connections + dd + strong #{profile.numConnections} + | connections + .text-center + small.text-muted= profile.publicProfileUrl - h3 Connections - .row - for connection in connections.values - .col-xs-3.col-md-2 - img(src='#{connection.pictureUrl}') - div.facebook-caption= connection.firstName - - h3 Company Information + h3.text-primary LinkedIn Connections + table.table.table-hover.table-striped.table-bordered + tbody + for connection in profile.connections.values + if connection.id != 'private' + tr + td + strong #{connection.firstName} #{connection.lastName} + .text-muted #{connection.headline} From 53f89c355f0f23cb7101dfc41a6d497dad867a96 Mon Sep 17 00:00:00 2001 From: Sahat Yalkabov Date: Thu, 27 Feb 2014 17:39:18 -0500 Subject: [PATCH 18/18] Code formatting update --- controllers/api.js | 270 ++++++++++++++++++++-------------------- views/api/linkedin.jade | 8 +- 2 files changed, 139 insertions(+), 139 deletions(-) diff --git a/controllers/api.js b/controllers/api.js index 86b9dcb3f0..32428df35a 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -35,31 +35,31 @@ exports.getApi = function(req, res) { exports.getFoursquare = function(req, res, next) { var token = _.findWhere(req.user.tokens, { kind: 'foursquare' }); async.parallel({ - trendingVenues: function(callback) { - foursquare.Venues.getTrending('40.7222756', '-74.0022724', { limit: 50 }, token.accessToken, function(err, results) { - callback(err, results); - }); - }, - venueDetail: function(callback) { - foursquare.Venues.getVenue('49da74aef964a5208b5e1fe3', token.accessToken, function(err, results) { - callback(err, results); - }); - }, - userCheckins: function(callback) { - foursquare.Users.getCheckins('self', null, token.accessToken, function(err, results) { - callback(err, results); - }); - } - }, - function(err, results) { - if (err) return next(err); - res.render('api/foursquare', { - title: 'Foursquare API', - trendingVenues: results.trendingVenues, - venueDetail: results.venueDetail, - userCheckins: results.userCheckins + trendingVenues: function(callback) { + foursquare.Venues.getTrending('40.7222756', '-74.0022724', { limit: 50 }, token.accessToken, function(err, results) { + callback(err, results); }); + }, + venueDetail: function(callback) { + foursquare.Venues.getVenue('49da74aef964a5208b5e1fe3', token.accessToken, function(err, results) { + callback(err, results); + }); + }, + userCheckins: function(callback) { + foursquare.Users.getCheckins('self', null, token.accessToken, function(err, results) { + callback(err, results); + }); + } + }, + function(err, results) { + if (err) return next(err); + res.render('api/foursquare', { + title: 'Foursquare API', + trendingVenues: results.trendingVenues, + venueDetail: results.venueDetail, + userCheckins: results.userCheckins }); + }); }; /** @@ -93,25 +93,25 @@ exports.getFacebook = function(req, res, next) { var token = _.findWhere(req.user.tokens, { kind: 'facebook' }); graph.setAccessToken(token.accessToken); async.parallel({ - getMe: function(done) { - graph.get(req.user.facebook, function(err, me) { - done(err, me); - }); - }, - getMyFriends: function(done) { - graph.get(req.user.facebook + '/friends', function(err, friends) { - done(err, friends.data); - }); - } - }, - function(err, results) { - if (err) return next(err); - res.render('api/facebook', { - title: 'Facebook API', - me: results.getMe, - friends: results.getMyFriends + getMe: function(done) { + graph.get(req.user.facebook, function(err, me) { + done(err, me); }); + }, + getMyFriends: function(done) { + graph.get(req.user.facebook + '/friends', function(err, friends) { + done(err, friends.data); + }); + } + }, + function(err, results) { + if (err) return next(err); + res.render('api/facebook', { + title: 'Facebook API', + me: results.getMe, + friends: results.getMyFriends }); + }); }; /** @@ -188,53 +188,53 @@ exports.getNewYorkTimes = function(req, res, next) { exports.getLastfm = function(req, res, next) { var lastfm = new LastFmNode(secrets.lastfm); async.parallel({ - artistInfo: function(done) { - lastfm.request("artist.getInfo", { - artist: 'Epica', - handlers: { - success: function(data) { - done(null, data); - }, - error: function(err) { - done(err); - } + artistInfo: function(done) { + lastfm.request("artist.getInfo", { + artist: 'Epica', + handlers: { + success: function(data) { + done(null, data); + }, + error: function(err) { + done(err); } - }); - }, - artistTopAlbums: function(done) { - lastfm.request("artist.getTopAlbums", { - artist: 'Epica', - handlers: { - success: function(data) { - var albums = []; - _.each(data.topalbums.album, function(album) { - albums.push(album.image.slice(-1)[0]['#text']); - }); - done(null, albums.slice(0, 4)); - }, - error: function(err) { - done(err); - } - } - }); - } - }, - function(err, results) { - if (err) return next(err.message); - var artist = { - name: results.artistInfo.artist.name, - image: results.artistInfo.artist.image.slice(-1)[0]['#text'], - tags: results.artistInfo.artist.tags.tag, - bio: results.artistInfo.artist.bio.summary, - stats: results.artistInfo.artist.stats, - similar: results.artistInfo.artist.similar.artist, - topAlbums: results.artistTopAlbums - }; - res.render('api/lastfm', { - title: 'Last.fm API', - artist: artist + } }); + }, + artistTopAlbums: function(done) { + lastfm.request("artist.getTopAlbums", { + artist: 'Epica', + handlers: { + success: function(data) { + var albums = []; + _.each(data.topalbums.album, function(album) { + albums.push(album.image.slice(-1)[0]['#text']); + }); + done(null, albums.slice(0, 4)); + }, + error: function(err) { + done(err); + } + } + }); + } + }, + function(err, results) { + if (err) return next(err.message); + var artist = { + name: results.artistInfo.artist.name, + image: results.artistInfo.artist.image.slice(-1)[0]['#text'], + tags: results.artistInfo.artist.tags.tag, + bio: results.artistInfo.artist.bio.summary, + stats: results.artistInfo.artist.stats, + similar: results.artistInfo.artist.similar.artist, + topAlbums: results.artistTopAlbums + }; + res.render('api/lastfm', { + title: 'Last.fm API', + artist: artist }); + }); }; /** @@ -348,41 +348,41 @@ exports.getSteam = function(req, res, next) { var query = { l: 'english', steamid: steamId, key: secrets.steam.apiKey }; async.parallel({ - playerAchievements: function(done) { - query.appid = '49520'; - var qs = querystring.stringify(query); - request.get({ url: 'http://api.steampowered.com/ISteamUserStats/GetPlayerAchievements/v0001/?' + qs, json: true }, function(error, request, body) { - if (request.statusCode === 401) return done(new Error('Missing or Invalid Steam API Key')); - done(error, body); - }); - }, - 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) { - if (request.statusCode === 401) return done(new Error('Missing or Invalid Steam API Key')); - done(error, 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) { - if (request.statusCode === 401) return done(new Error('Missing or Invalid Steam API Key')); - done(error, body); - }); - } - }, - function(err, results) { - if (err) return next(err); - res.render('api/steam', { - title: 'Steam Web API', - ownedGames: results.ownedGames.response.games, - playerAchievemments: results.playerAchievements.playerstats, - playerSummary: results.playerSummaries.response.players[0] + playerAchievements: function(done) { + query.appid = '49520'; + var qs = querystring.stringify(query); + request.get({ url: 'http://api.steampowered.com/ISteamUserStats/GetPlayerAchievements/v0001/?' + qs, json: true }, function(error, request, body) { + if (request.statusCode === 401) return done(new Error('Missing or Invalid Steam API Key')); + done(error, body); }); + }, + 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) { + if (request.statusCode === 401) return done(new Error('Missing or Invalid Steam API Key')); + done(error, 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) { + if (request.statusCode === 401) return done(new Error('Missing or Invalid Steam API Key')); + done(error, body); + }); + } + }, + function(err, results) { + if (err) return next(err); + res.render('api/steam', { + title: 'Steam Web API', + ownedGames: results.ownedGames.response.games, + playerAchievemments: results.playerAchievements.playerstats, + playerSummary: results.playerSummaries.response.players[0] }); + }); }; /** @@ -421,26 +421,26 @@ exports.getVenmo = function(req, res, next) { 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) { - done(err, body); - }); - }, - getRecentPayments: function(done) { - request.get({ url: 'https://api.venmo.com/v1/payments?' + query, json: true }, function(err, request, body) { - done(err, body); - - }); - } - }, - function(err, results) { - if (err) return next(err); - res.render('api/venmo', { - title: 'Venmo API', - profile: results.getProfile.data, - recentPayments: results.getRecentPayments.data + getProfile: function(done) { + request.get({ url: 'https://api.venmo.com/v1/me?' + query, json: true }, function(err, request, body) { + done(err, body); }); + }, + getRecentPayments: function(done) { + request.get({ url: 'https://api.venmo.com/v1/payments?' + query, json: true }, function(err, request, body) { + done(err, body); + + }); + } + }, + function(err, results) { + if (err) return next(err); + res.render('api/venmo', { + title: 'Venmo API', + profile: results.getProfile.data, + recentPayments: results.getRecentPayments.data }); + }); }; exports.postVenmo = function(req, res, next) { diff --git a/views/api/linkedin.jade b/views/api/linkedin.jade index 0620c98d7d..f7d79906a4 100644 --- a/views/api/linkedin.jade +++ b/views/api/linkedin.jade @@ -37,14 +37,14 @@ block content if company.isCurrent dd strong= company.title - | at + | at strong #{company.company.name} dt.text-muted Previous for company in profile.positions.values if !company.isCurrent dd | #{company.title} - | at + | at | #{company.company.name} dt.text-muted Education for education in profile.educations.values @@ -52,11 +52,11 @@ block content dt.text-muted Recommendations dd strong #{profile.numRecommenders} - | recommendation(s) received + | recommendation(s) received dt.text-muted Connections dd strong #{profile.numConnections} - | connections + | connections .text-center small.text-muted= profile.publicProfileUrl