Adding a codeplex repo to strider

After my last post, I can now get an authenticated user's repositores. However, I am experiencing a couple of issues.

First, the codeplex oauth tokens expire after an hour. Because of this, I am seeing a number of authentication errors. In order to solve this, I need to get the refresh token from codeplex. Thankfully, it seems this is a feature built into passport. All I had to do was update the validation methods to take one more parameter:

function validateAuth(req, accessToken, refreshToken, 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, refreshToken, profile))
  req.user.save(function (err) {
    done(err, req.user);
  })
}

function makeAccount(accessToken, refreshToken, 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,
      refreshToken: refreshToken,
      name: username,
      avatar: profile.Avatar
    }
  }
}

With the refresh token on hand, I was able to use the refresh-token package to get a valid oauth token. The way I see it, there are 3 options for handling token refreshes:

  • Always get a fresh token before making an API call
  • Store the expiration of the token, and only refresh when needed
  • Assume a valid token; on error, refresh and retry

I am unsure if there is a best practice for this, but I decided on option 3. The first option seemed like too much overhead for my taste. The second option is appealing to me, but I was too lazy to go look up how to translate the token's expiration timeout into an absolute expiration date. Horrible, I know. But, there you have it. I went with #3 because lazy.

Now that I might have to refresh the token, my API method will need the clientId and clientSecret. I modified getCodeplex and getRepos to take the appConfig, as both are properties there. The most important bit, getCodeplex:

function getCodeplex(account, appConfig, resourcePath, callback) {

    var options = {
      hostname: 'www.codeplex.com',
      path: resourcePath,
      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 fin = JSON.parse(json);

            // this is an error message
            if (fin.Message) {
                if (appConfig.clientId && appConfig.clientSecret && fin.Message === 'Authentication required.') {
                    var tokenProvider = new TokenProvider('https://www.codeplex.com/oauth/token', {
                    refresh_token: account.refreshToken, 
                    client_id:     appConfig.clientId, 
                    client_secret: appConfig.clientSecret
                    });

                    tokenProvider.getToken(function (err, token) {
                        if (err) { callback(err, null); }
                        else {
                            account.accessToken = token;
                            getCodeplex(account, resourcePath, callback);
                        }
                    });
                }
                else {
                    callback(fin.Message, null); 
                }
            }
            else { callback(null, fin); }
        });
    });

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

The second bug: when I try to add a repo to strider, I get an error message in the UI:

Error creating project for repo aheidebrecht/codeonlystoredprocedures:  
{
  "results": [], 
  "status": "error", 
  "errors": [ { 
    "code": 400, 
    "reason": "provider.repo_id is required" 
  } ]
}

It seems that I left out a required id property in the parseRepo method:

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

After fixing these issues, I was finally able to start the process of setting up a repo. I decided to hold off on implementing the webhook part mostly because, as of this blog post, there is no generic webhook support (it supports these and these commercial websites... oh Microsoft).

Instead, I just let strider think I have done something special. Since I don't have to setup or teardown a webhook, those two required api methods are quite simple:

setupRepo: function (account, config, project, done) {  
  if (!account.accessToken) return done(new Error('Codeplex account not configured'));
  done(null, config);
},

teardownRepo: function (account, config, project, done) {  
  if (!account.accessToken) return done(new Error('Codeplex account not configured'));
  done(null, config);
}

With those methods in place, we can add and remove a repo to strider! Woot!

You can grab this version at github.

-AH


View or Post Comments