# HG changeset patch
# User Quentin Raynaud <quentin@qraynaud.eu>
# Date 1375564789 -7200
#      Sat Aug 03 23:19:49 2013 +0200
# Branch oauth
# Node ID 745e65c3db3cada48f738693f0e37f125cbbf669
# Parent  c23c45bae4cca6653423c6163ca0a1631b739e99
Add OAuth support for connecting to Meetup.com

diff --git a/.hgignore b/.hgignore
--- a/.hgignore
+++ b/.hgignore
@@ -4,3 +4,6 @@
 ^public/
 ^bower_components/
 ^\.ngapp.json$
+
+syntax: glob
+server/config.js
diff --git a/client/html/templates/home.jade b/client/html/templates/home.jade
--- a/client/html/templates/home.jade
+++ b/client/html/templates/home.jade
@@ -54,3 +54,4 @@
     pre {{userform|json}}
     p(x-ng-if="userform.$valid") Form is valid.
     p(x-ng-if="userform.$invalid") Form in NOT valid (yet)!
+a(href="/api/v1/oauth?provider=meetup", target="_self") Connect with Meetup.com
\ No newline at end of file
diff --git a/client/html/templates/oauth.jade b/client/html/templates/oauth.jade
new file mode 100644
--- /dev/null
+++ b/client/html/templates/oauth.jade
@@ -0,0 +1,1 @@
+p.text-success Success!!
diff --git a/client/js/nodejsparis/controllers/oauth.js b/client/js/nodejsparis/controllers/oauth.js
new file mode 100644
--- /dev/null
+++ b/client/js/nodejsparis/controllers/oauth.js
@@ -0,0 +1,4 @@
+angular.module('nodejsparis')
+.controller('njp-oauth', function($scope) {
+  $scope.ctrlName = 'njp-oauth';
+});
diff --git a/client/js/nodejsparis/nodejsparis.js b/client/js/nodejsparis/nodejsparis.js
--- a/client/js/nodejsparis/nodejsparis.js
+++ b/client/js/nodejsparis/nodejsparis.js
@@ -1,10 +1,16 @@
 angular.module('nodejsparis', [])
 .constant('njpVersion', jQuery('html').data('app-version'))
-.config(function($routeProvider, njpVersion) {
+.config(function($routeProvider, $locationProvider, njpVersion) {
   $routeProvider
   .when('/home', {
     templateUrl: '/' + njpVersion + '/templates/home.html',
     controller: 'njp-home'
   })
+  .when('/oauth', {
+    templateUrl: '/' + njpVersion + '/templates/oauth.html',
+    controller: 'njp-oauth'
+  })
   .otherwise({ redirectTo: '/home' });
+
+  $locationProvider.html5Mode(true);
 });
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -15,7 +15,10 @@
   "dependencies": {
     "express": "~3.3.4",
     "deferred": "~0.6.5",
-    "lodash": "~1.3.1"
+    "lodash": "~1.3.1",
+    "request": "~2.25.0",
+    "node.extend": "~1.0.7",
+    "uuid": "~1.4.1"
   },
   "devDependencies": {
     "stylus": "~0.34.1",
diff --git a/server/config.example.js b/server/config.example.js
new file mode 100644
--- /dev/null
+++ b/server/config.example.js
@@ -0,0 +1,19 @@
+module.exports = {
+  oauth: {
+    url: '/api/v1/oauth',
+    redirect_to: '/oauth',
+
+    meetup: {
+      enabled: true,
+      client_id: '<YOURID>',
+      client_secret: '<YOURSECRET>',
+      redirect_uri: '<YOURURI>',
+
+      profile: {
+        params: {
+          group_urlname: 'Nodejs-Paris'
+        }
+      }
+    }
+  }
+}
diff --git a/server/libs/oauth.config.js b/server/libs/oauth.config.js
new file mode 100644
--- /dev/null
+++ b/server/libs/oauth.config.js
@@ -0,0 +1,130 @@
+module.exports = {
+  google: {
+    start: 'redirect',
+
+    redirect: {
+      method: 'REDIRECT',
+      url: 'https://accounts.google.com/o/oauth2/auth',
+      params: {
+        client_id: null,
+        redirect_uri: null,
+        scope: 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email',
+        response_type: 'code',
+        state: null,
+      },
+      get: 'code',
+      next: 'token'
+    },
+    token: {
+      method: 'POST',
+      url: 'https://accounts.google.com/o/oauth2/token',
+      params: {
+        code: null,
+        client_id: null,
+        client_secret: null,
+        redirect_uri: null,
+        grant_type: 'authorization_code'
+      },
+      get: 'access_token',
+      next: 'userinfos'
+    },
+    userinfos: {
+      method: 'GET',
+      url: 'https://www.googleapis.com/oauth2/v1/userinfo',
+      params: {
+        alt: 'json',
+        access_token: null
+      },
+      res: 'userinfos'
+    }
+  },
+
+  facebook: {
+    start: 'redirect',
+
+    redirect: {
+      method: 'REDIRECT',
+      url: 'https://www.facebook.com/dialog/oauth',
+      params: {
+        client_id: null,
+        redirect_uri: null,
+        state: null,
+        scope: 'email,user_birthday'
+      },
+      get: 'code',
+      next: 'token'
+    },
+    token: {
+      method: 'GET',
+      url: 'https://graph.facebook.com/oauth/access_token',
+      params: {
+        client_id: null,
+        client_secret: null,
+        redirect_uri: null,
+        code: null
+      },
+      get: 'access_token',
+      next: 'graph'
+    },
+    graph: {
+      method: 'GET',
+      url: 'https://graph.facebook.com/me',
+      params: {
+        access_token: null
+      },
+      res: 'userinfos',
+      next: 'picture'
+    },
+    picture: {
+      method: 'GET',
+      url: 'https://graph.facebook.com/me/picture',
+      params: {
+        access_token: null,
+        redirect: false,
+        width: 10000
+      },
+      res: 'userinfos',
+      res_add: { picture: 'data.url' }
+    }
+  },
+
+  meetup: {
+    start: 'redirect',
+
+    redirect: {
+      method: 'REDIRECT',
+      url: 'https://secure.meetup.com/oauth2/authorize',
+      params: {
+        client_id: null,
+        scope: 'basic',
+        response_type: 'code',
+        redirect_uri: null,
+        state: null
+      },
+      get: 'code',
+      next: 'token'
+    },
+    token: {
+      method: 'POST',
+      url: 'https://secure.meetup.com/oauth2/access',
+      params: {
+        client_id: null,
+        client_secret: null,
+        grant_type: 'authorization_code',
+        redirect_uri: null,
+        code: null
+      },
+      get: 'access_token',
+      next: 'profile'
+    },
+    profile: {
+      method: 'GET',
+      url: ' https://api.meetup.com/2/profiles',
+      params: {
+        access_token: null,
+        member_id: 'self'
+      },
+      res: 'userinfos'
+    }
+  }
+};
diff --git a/server/libs/oauth.js b/server/libs/oauth.js
new file mode 100644
--- /dev/null
+++ b/server/libs/oauth.js
@@ -0,0 +1,299 @@
+'use strict';
+
+var querystring = require('querystring')
+  , request = require('request')
+  , extend = require('node.extend')
+  , uuid = require('uuid')
+;
+
+// Returns resolved parameters for the given state
+// (resolved parameters = no null values)
+function getConfigParams(session, config, state) {
+  // Get the basic parameters object from the configuration
+  var params = config[state.step].params;
+
+  // Create an empty obj that will get our values
+  var res = {};
+  // For each parameters in the config
+  for (var p in params) {
+    // If the parameter is called 'state', resolve it to the state object & continue
+    if (p == 'state') {
+      res[p] = state.uuid;
+      continue;
+    }
+    // Otherwise, try to use the parameter value from the configuration
+    // If it is null, try to lookup for the same parameter in the configuration's root
+    // If it is null or undefined, try to look for the parameter in the session
+    // If it is still null or undefined, set the value to null
+    res[p] = params[p] || config[p] || session.oauth[p] || null;
+  }
+
+  // Return the so constructed object
+  return res;
+}
+
+// Same as getConfigParams but serialize the object to get into an URL
+function getConfigParamsSrz(session, config, state) {
+  return querystring.stringify(getConfigParams(session, config, state));
+}
+
+// Get an obj attribute using the get/add_res syntax
+function getObjValue(ref, obj) {
+  // Split the ref string using dot '.' and remove empty cells to get attributes' names
+  var attrs = ref.split('.').filter(function(o) { return !!o; });
+
+  // For each attribute in the ref string but the last one
+  for (var i = 0; i < attrs.length - 1; ++i)
+    // Get into the objet's corresponding attribute (if there is none, use an empty object)
+    obj = obj[attrs[i]] || {};
+
+  // Returns the object's attribute (or undefined is it does not exists)
+  return obj[attrs[i]];
+}
+
+// Create an object using the get/add_res syntax
+function createObj(ref, value) {
+  // This will be our result
+  var res = {};
+
+  // Split the ref string using dot '.' and remove empty cells
+  var attrs = ref.split('.').filter(function(o) { return !!o; });
+
+  // tmp will be a "pointer" somewhere in our result object
+  var tmp = res;
+  // For each attributes' names in the ref string but the last one
+  for (var i = 0; i < attrs.length - 1; ++i) {
+    // Create an empty object in the tmp "pointer" mapped to the current attribute's
+    // name and move the pointer to this new empt object
+    tmp = tmp[attrs[i]] = {};
+  }
+  // Add the value in the pointer object mapped with the last attribute's name
+  tmp[attrs[i]] = value;
+
+  // Return the constructed object
+  return res;
+}
+
+// Copy an object from "source" to "dest" using an object containing
+// get/add_res syntax
+function smartCopy(ref, dest, source) {
+  // If the ref is an array, they just copy each attributes
+  // it references from source to dest
+  if (Array.isArray(ref)) {
+    for (var i = 0; i < ref.length; ++i)
+      dest[ref[i]] = source[ref[i]];
+  }
+  // If the ref is an object, is attribute & value can be a get/add_res syntax
+  else if (typeof(ref) == 'object') {
+    // For each attributes
+    for (var a in ref) {
+      // If the value corresponding to the attribute is an object,
+      // then recurse into it
+      if (typeof(ref[a]) == 'object')
+        dest[a] = smartCopy(ref[a], dest[a] || {}, source[a] || {});
+      // Otherwise extend dest with an object creating using a and the value it references
+      else
+        extend(true, dest, createObj(a, getObjValue(ref[a] || a, source)));
+    }
+  }
+  // Otherwise dest is a string, just copy the attribute it references
+  else
+    dest[ref] = source[ref];
+
+  return dest;
+}
+
+module.exports = function(app, config) {
+  // Exend the configuration from our config file with the given configuration
+  config = extend(true, {}, require('./oauth.config.js'), config);
+
+  // This function runs the current step
+  function invokeStep(req, res, state) {
+    // Get the oauth configuration object
+    var oauth = config[state.provider];
+    // Get the current step from it
+    var step = oauth[state.step];
+
+    // Get the url related to the step
+    var url = step.url;
+
+    // If the method is not POST, then add the parameters to the URL
+    if (step.method != 'POST')
+      url += '?' + getConfigParamsSrz(req.session, oauth, state);
+
+    // If the method is redirect, then we can redirect the user to the
+    // computed url
+    if (step.method == 'REDIRECT') {
+      // Save the session before redirecting the user
+      // This is needed because the user could be redirected back
+      // to our oauth service before the session is automatically saved
+      // and if the user is handled by another process of the cluster
+      // it might cause the process to fail badly
+      req.session.save(function() {
+        // Redirect the user
+        res.redirect(url);
+      });
+      return;
+    }
+
+    // Otherwise prepare a request config
+    var req_opts = {
+      url: url,                 // URL to request
+      jar: false,               // Do not store cookies
+      method: step.method       // GET|POST|...
+    };
+
+    // If the method is POST
+    if (step.method == 'POST') {
+      // Add to the request object the correct headers
+      req_opts.headers = {'content-type': 'application/x-www-form-urlencoded'};
+      // And the configuration parameters (to the body)
+      req_opts.body = getConfigParamsSrz(req.session, oauth, state);
+    }
+
+    // Run the request
+    request(req_opts, function(err, req_res, req_body) {
+      // Copy the result to the current request
+      req.body = req_body;
+      // If we have a string, try to resolve it into a JS object
+      if (typeof(req.body) == 'string') {
+        try {
+          // First try to parse it from JSON (standard)
+          req.body = JSON.parse(req.body);
+        } catch (e) {
+          // If it fails, try to parse an urlencoded string
+          // (facebook is non standard and do this)
+          req.body = querystring.parse(req.body);
+        }
+      }
+
+      // If an error occured
+      if (err || !req.body || req.body.error) {
+        // Report it to the client
+        res.writeHead(500);
+        res.end('Error connecting through your provider.');
+        return;
+      }
+
+      // Otherwise, go to the next step
+      invokeNextStep(req, res, state);
+    });
+  }
+
+  // This function changes the state & prep next step, then
+  // calls invokeStep to run it
+  function invokeNextStep(req, res, state) {
+    // Get the current oauth config & step using the provided state
+    var oauth = config[state.provider];
+    var step = oauth[state.step];
+
+    // If we don't have a body to our request, use the GET parameters instead
+    if (!req.body || !Object.keys(req.body).length)
+      req.body = req.query;
+
+    // If we have to get some values from the prevous request before switching state
+    // then do it
+    if (step.get)
+      smartCopy(step.get, req.session.oauth, req.body);
+
+    // If we have to set/modify some res value, do it now
+    if (step.res) {
+      // Modify
+      if (step.res_add)
+        smartCopy(step.res_add, req.session.oauth[step.res], req.body);
+      // Set
+      else
+        req.session.oauth[step.res] = req.body;
+    }
+
+    if (step.next) {
+      state.step = step.next;
+      invokeStep(req, res, state);
+    }
+    else {
+      req.session.oauth.ok = true;
+      res.redirect(config.redirect_to || '/');
+    }
+  }
+
+  // This function init the process enough to invoke the first step
+  function invokeFirstStep(req, res) {
+    // Check that the provider to use is given in the query
+    // that it exists in our conf & is enabled
+    if (!req.query.provider || !(req.query.provider in config) || !config[req.query.provider].enabled) {
+      // Fails if not
+      req.session.oauth = { error: 'no_provider', error_msg: 'The variable provider should be given when using oauth.' };
+      res.redirect(config.redirect_to || '/');
+      return;
+    }
+
+    // Init first state using the configuration
+    var state = {
+      provider: req.query.provider,
+      step: config[req.query.provider].start,
+      uuid: uuid.v4()
+    };
+
+    // Init a basic oauth object to store & report status
+    req.session.oauth = {};
+    req.session.oauth[state.uuid] = state;
+
+    // Run step
+    invokeStep(req, res, state);
+  }
+
+  function getInfos(req, res) {
+    var infos = extend({}, req.session.oauth);
+    delete infos.access_token;
+    delete infos.code;
+
+    res.setHeader('Content-Type', 'application/json');
+    res.end(JSON.stringify(infos));
+  }
+
+  // Entry point for using the oauth module
+  function oauth(req, res) {
+    // Delete oauth informations from session
+    if (req.query.logout === 'true') {
+      delete req.session.oauth;
+      res.setHeader('Content-Type', 'application/json');
+      res.end(JSON.stringify({ ok: true }));
+      return;
+    }
+
+    // Return oauth informations in JSON
+    if (req.query.infos === 'true') {
+      if (!req.session.oauth)
+        req.session.oauth = { error: 'invalid_state', error_msg: 'No oauth object in session.' };
+      getInfos(req, res);
+      return;
+    }
+
+    // If we are provided with a state
+    if (req.query.state) {
+      if (!req.session.oauth)
+        req.session.oauth = { error: 'invalid_state', error_msg: 'No oauth object in session.' };
+
+      // Get it in a JS form
+      var state = req.session.oauth[req.query.state];
+
+      // Check that state looks ok
+      if (!state.provider || !state.step)
+        req.session.oauth = { error: 'invalid_state', error_msg: 'State object is invalid.' };
+
+      // If there is an error, redirect the user where the config says so
+      if (req.session.oauth.error)
+        res.redirect(config.redirect_to || '/');
+
+      // Otherwise invoke next step
+      invokeNextStep(req, res, state);
+      return;
+    }
+
+    // Otherwise invoke first step
+    invokeFirstStep(req, res);
+  }
+
+  // Add the oauth module to the express routing
+  app.get(config.url, oauth);
+};
diff --git a/server/server.js b/server/server.js
--- a/server/server.js
+++ b/server/server.js
@@ -2,12 +2,17 @@
 var fs = require('fs');
 var path = require('path');
 var app = express();
+var config = require('./config');
 var port = 3000;
 
-app.use(express.static('public/'));
+app.use(express.static(path.join(__dirname, '../public')));
+app.use(express.cookieParser());
+app.use(express.session({secret: 'secret'}));
 
-app.get('/', express.static('public/index.html'));
+// Add oauth support
+require('./libs/oauth')(app, config.oauth);
 
+// Serve API mockups
 app.all('/api/*', function (req, res) {
   res.setHeader('Content-type', 'application/json; charset=utf-8');
   fs.readFile(
@@ -18,5 +23,15 @@
   );
 });
 
+// Return index.html
+app.get('/*', function(req, res) {
+  fs.readFile(
+    path.join(__dirname, '../public/index.html'),
+    function (err, data) {
+      res.end(data);
+    }
+  );
+});
+
 app.listen(port);
 console.log('Listening on port ' + port + ' - ' + app.settings.env + ' mode');