Making a strider plugin

When I last left off, my codeplex plugin for strider was able to pull in repos from codeplex by passing in a valid codeplex username. While it was a great proof of concept, it isn't the way that strider wants (and in fact needs to) to interract with source control providers. In order to subscribe to code changes, strider needs to authenticate with OAuth. While this turns out to be pretty easy to do, I realized that the longer I tried to write my plugin outside of strider, the more throwaway code I would have to create. As such, I decided to actually make this code a strider plugin now instead of later.

Over at the project page for the strider extensions loader, there is some documentation about creating a plugin. They have some clearly defined roles, one of which is provider. For source control providers. You know, like codeplex. Based on this documentation, I created the following project structure:

codeplex-for-strider
|
+-config
: |
: +-config.html
: +-config.js
|
+-lib
: |
: +-codeplex.js
|
+-webapp.js
+-worker.js
+-package.json
+-strider.json

I just created blank files for all the new items. Following along with the readme, I configured my strider.json thusly:

{
  "id": "codeplex",
  "title": "Codeplex",
  "type": "provider",
  "hosted": true,
  "config": {
    "controller": "CodeplexController"
  },
  "accountConfig": {
    "setupLink": "/ext/codeplex/oauth"
  },
  "webapp": "webapp.js",
  "worker": "worker.js",
  "inline_icon": "windows"
}

I don't know if it is a documentation issue, or if there are two ways of specifying the url to call to start the authorization process. The docs say to export setupLink, but both the strider-github and strider-bitbucket plugins specify it in the strider config (in their package.json). Since I trust code more than documentation, I too put it in the config.

The plugin obviously doesn't yet do anything, but after dropping this folder in strider's node_modules directory, I saw it show up in strider! I didn't even have to put it in the package.json. This makes it incredibly easy to test.

Because I'm a sadist masochist, my first instinct was to click the Setup Codeplex button. I knew it would fail, but I was curious to see how explosively. It (pleasantly) surprisingly didn't crash the node process, but instead I got:

500  
Error: no strategy registered under name: codeplex  

And looking at the stack trace (that is niiiiiiiceeeeee) I can see that the strategy is related to something called passport. I was really hoping that somebody had already done the work of creating a codeplex strategy for me, but alas, no such luck. It turns out you can use the passport-ouath2 strategy directly, but since codeplex recommends passing the api version as a header, I decided to create my own.

Thankfully, this is really easy. I looked at a number of examples, and I came to the conclusion that this is pretty much boilerplate code. As such, I won't include it here. I will likely break it out into its own project at some point in the near future, even if it doesn't have a lot going on.

So, with a codeplex passport strategy working, I was able to add the auth export in webapp.js:

var codeplex = require('./lib/codeplex')  
  , CodeplexStrategy = require('./lib/passport-codeplex');

module.exports = {  
  auth: function (passport, context) {
    var config = this.appConfig
    passport.use(new CodeplexStrategy({
      clientID: this.appConfig.clientId,
      clientSecret: this.appConfig.clientSecret,
      callbackURL: this.appConfig.hostname + '/ext/codeplex/oauth/callback',
      passReqToCallback: true
    }, validateAuth));
  }  
  ...
}

I also had to configure an oauth application at codeplex. After doing so, I had to add the information into the webapp.js:

module.exports = {  
  appConfig: {
    hostname: 'http://localhost:3000',
    clientId: 'feecb876f9044bca8a86ab9089fba8b0',
    clientSecret: 'efc1e8c120c24b59b12ad0f4f7d32d02'
  }
  ...
}

Thoe credentials are for a test application I created that will connect to the default strider DEV settings (localhost:3000). When this plugin gets put into production, I will obviously be using a different set of credentials.

The last step is to create the callback (validateAuth in the auth export) that will register the account in strider. I shamelessly copied most of this from the strider-bitbucket plugin, but modified it slightly to parse the data returned from codeplex:

function validateAuth(req, accessToken, parms, profile, done) {  
  if (!req.user) {
    console.warn('Codeplex OAuth but no logged-in user')
    req.flash('account', "Cannot link a codeplex account if you aren't logged in")
    return done()
  } 
  var account = req.user.account('codeplex', profile.UserName)
  if (account) {
    console.warn("Trying to attach a codeplex account that's already attached...")
    req.flash('account', 'That codeplex account is already linked. <a href="https://codeplex.com/site/signout/" target="_blank">Sign out of codeplex</a> before you click "Add Account".')
    return done(null, req.user)
  }

  req.user.accounts.push(makeAccount(accessToken, profile))
  req.user.save(function (err) {
    done(err, req.user);
  })
}

function makeAccount(accessToken, profile) {  
  var username = profile.UserName;
  return {
    provider: 'codeplex',
    id: username,
    display_url: 'https://www.codeplex.com/site/users/view/' + username,
    title: username,
    config: {
      accessToken: accessToken,
      name: username,
      avatar: profile.Avatar
    }
  }
}

After that, I clicked on the setup codeplex account button again, and I got directed to codeplex! I get really excited for things like this; sad, I know. Upon authorizing the application... node crashed. We need a listRepos export. Well, we have a function that does this... let's call it!

listRepos: function (account, callback) {  
  codeplex.getRepos(account, callback);
}

Oh, wait, that account object isn't just a username anymore. We need to update the getRepos function to use the account class. Also, now that we are authorized, we should pass the token instead of the username. I am a little embarrassed by how long it took me (seriously about 5 hours of googling, and I never really found a helpful answer with my searches...) to figure out how to pass the token, but I have never really done web programming before. So, I guess I should feel better. Maybe.

Anyway, you can pass it as the Authorization header. So, my updated getRepos looks like this:

function getRepos(account, callback) {

    var options = {
      hostname: 'www.codeplex.com',
      path: '/api/user/projects',
      method: 'GET',
      headers: {'x-ms-version': '2012-09-01' }
    };

    if (account && account.accessToken) {
        options.headers.Authorization = 'Bearer ' + account.accessToken;
    }

    var req = https.request(options, function(res) {
        var json = '';
        res.on('data', function(d) {
            json += d;
        });
        res.on('error', function(err) {
            callback(err, null);
        });
        res.on('end', function() {
            var repos = JSON.parse(json);
            callback(null, repos);
        });
    });

    req.on('error', function(err) {
        callback(err, null);
    });
    req.end()
}

One more try clicking that setup codeplex button... success!

I can now go to my codeplex project tab, and I see 2 entries, but they are both blank. Looks like I need to massage the data a little bit. I could do it all in the getRepos method, but eventually I am going to need to make other api calls, and will probably want to use the same helper method for doing so (getRepos is going to become getCodeplex). Keeping this in mind, I created a parseRepo function in webapp.js:

function parseRepo(account, repo) {  
  return {
    name: account.name + '/' + repo.Name,
    display_name: repo.Title,
    display_url: repo.Url,
    group: repo.Role,
    'private': !repo.IsPublished,
    config: {
      sourceType: repo.SourceControl.ServerType,
      sourceUrl: repo.SourceControl.Url
    }
  }
}

According to the docs, strider needs 3 values for the config: name, url, and display_url. Because strider expects the name to be of the form <username>/<reponame>, I had to pass the account in addition to the repo. The other properties are undocumented, but I found them being set in the strider-bitbucket source, so I too decided to set them. Because codeplex doesn't send the actual owner of a project, I am grouping them by the user's role on the project (both strider-github and strider-bitbucket use the repo's owner for the group). The config property is user defined, but the docs said to also export a mongoose template named config:

module.exports = {  
  ...
  config: {
    sourceType: String,
    sourceUrl: String
  },
  ...
}

I then modified the listRepos method to call parseRepo for each item (map):

module.exports = {  
  ...
  listRepos: function (account, callback) {
    codeplex.getRepos(account, function(err, data) {
        if (err) return callback(err)
        callback(null, data.map(function(repo) { return parseRepo(account, repo); }))
    });
}

With all that done, I now see all my codeplex repos in strider! You can grab the latest version over at github.

-AH


View or Post Comments