Building a Google Calendar Booking App with MongoDB, ExpressJS, AngularJS, and Node.js - Part 1

MEAN Stack - MongoDB, ExpressJS, AngularJS, Node.js

For a while I've been dabbling with building apps using AngularJS and have quickly become consumed with how amazing it is. Having come from a heavy Adobe Flex background, I felt right at home with the two-way bindings the framework provides. In my quest to learn more about Angular I came across a few articles on the MEAN stack, which is a combination of MongoDB, ExpressJS, AngularJS, and Node.js. Since discovering some of the benefits and quick bootstrapping ability of the stack, I've been meaning to work on a project powered by it.

As fate would have it, a friend of mine needed some help building a booking app linked to his Google Calendar so his clients could book his photography studio from his website. The app needed to display the days already booked on his calendar and allow users to book any of the open days in the future. I took this as the opportunity and motivation I needed to dive into learning these new frameworks.

Before You Begin

I'm going to take you through my code to create a basic calendar booking application. Starting with building the Node.js API in part one and finishing with the Angular interface in part two. Before I start digging into the code I want it to be mentioned that this is by no means meant to be considered a best practices guide or anything of the sort. I've only just started learning Node.js and the following is just the different methodologies I've gathered from multiple resources all over the web. If you have see anything that can be improved upon please don't hesitate to comment below!

Since I've always heard great things about the Heroku platform at Dreamforce every year, I figured that would be a great starting place.

I highly recommend working through the Getting Started with Node.js on Heroku guide. It will walk you through setting up your local environment as well as building a basic app with the Node.js and the Express framework.

The complete source files for part one of this project can be found on my Github account here

Digging In to Node.js

Required Libraries

var express = require("express");
var oauth = require('oauth');
var mongo = require('mongodb');
var gcal = require('google-calendar');
var q = require('q');

Above are the libraries I used within the app. You can install them using the Node Package Manager (npm) which should have come with the Heroku toolset you installed in the getting started guide.

Variables

var oa;
var app = express();

var clientId = 'GOOGLE_CLIENT_ID';
var clientSecret = 'GOOGLE_CLIENT_SECRET';
var scopes = 'https://www.googleapis.com/auth/calendar';
var googleUserId;
var refreshToken;
var baseUrl;

I instantiate the application level variables that will be shared throughout the different functions within the API.

App Setup

app.configure('development',function(){
  console.log('!! DEVELOPMENT MODE !!');

  googleUserId = 'GOOGLE_EMAIL_ADDRESS';
  refreshToken = 'GOOGLE_REFRESH_TOKEN';
  baseUrl = 'DEV_API_URL';
});

app.configure('production', function(){
  console.log('!! PRODUCTION MODE !!');

  googleUserId = 'GOOGLE_EMAIL_ADDRESS';
  refreshToken = 'GOOGLE_REFRESH_TOKEN';
  baseUrl = 'PRODUCTION_API_URL';
});

Express provides you the ability to configure your app for both development and production environments, as well as any custom label you want to give it. (e.g. Staging, Foo, etc.) You can then set the NODE_ENV config setting on the server to your desired configuration. This really made the development to production process a breeze.

I hardcoded the Google refresh token into the app for simplicity, but this could obviously be improved by calling it from a database.

CORS

var allowCrossDomain = function(req, res, next){

  //instantiate allowed domains list
  var allowedDomains = [
    'http://YOUR_DOMAIN.com',
    'https://YOUR_DOMAIN.com'
  ];

  //check if request origin is in allowed domains list
  if(allowedDomains.indexOf(req.headers.origin) != -1)
  {
    res.header('Access-Control-Allow-Origin', req.headers.origin);
    res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
      res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With');
  }

  // intercept OPTIONS method
    if ('OPTIONS' == req.method) {
    res.send(200);
    }
    else {
    next();
    }
};

app.use(allowCrossDomain);
app.use(express.logger());
app.use(express.bodyParser());

Since the API is being hosted on Heroku and my friends site is being hosted elsewhere, I had to setup the API to allow cross-domain calls from the web server using CORS. This is so I could utilize Angular's $http directive without having to worry about XSS errors. (more on this in part two)

Database Setup

var mongoCollectionName = 'MONGO_COLLECTION_NAME';
var mongoUri = process.env.MONGOLAB_URI || process.env.MONGOHQ_URL || 'mongodb://localhost/default';
var database;
function connect(callback)
{
  var deferred = q.defer();

  if(database === undefined)
  {
    mongo.Db.connect(mongoUri, function(err, db){
      if(err) deferred.reject({error: err});

      database = db;
      deferred.resolve();
    });
  }
  else
  {
    deferred.resolve();
  }

  return deferred.promise;
}

Here I create a function to wrap my connection to the mongo database in a Javascript Promise in order to alleviate an issue I had when the API was called while the Heroku server was asleep. Upon awakening, the connection was undefined resulting in an error. I chain call this function before any calls to do the database to ensure the connection is never undefined. 

Google Authorization with OAuth2

function authorize()
{
  var deferred = q.defer();

  oa = new oauth.OAuth2(clientId,
            clientSecret,
            "https://accounts.google.com/o",
            "/oauth2/auth",
            "/oauth2/token");

  if(refreshToken)
  {
    oa.getOAuthAccessToken(refreshToken, {grant_type:'refresh_token', client_id: clientId, client_secret: clientSecret}, function(err, access_token, refresh_token, res){

      //lookup settings from database
      connect().then(function(){
        database.collection(mongoCollectionName).findOne({google_user_id: googleUserId}, function(findError, settings){

          var expiresIn = parseInt(res.expires_in);
          var accessTokenExpiration = new Date().getTime() + (expiresIn * 1000);

          //add refresh token if it is returned
          if(refresh_token != undefined) settings.google_refresh_token = refresh_token;

          //update access token in database
          settings.google_access_token = access_token;
          settings.google_access_token_expiration = accessTokenExpiration;

          database.collection(mongoCollectionName).save(settings);

          deferred.resolve(access_token);
        });
      });

    })
  }
  else
  {
    deferred.reject({error: 'Application needs authorization.'});
  }

  return deferred.promise;
}

With this promise function I use the oauth library to retrieve a new access token from Google's authorization endpoint. After the access token is received I connect to the mongo database and update the access token.

Retrieve Google Access Token

function getAccessToken()
{
  var deferred = q.defer();
  var accessToken;

  connect().then(function(){

    database.collection(mongoCollectionName).findOne({google_user_id: googleUserId}, function(findError, settings){
      //check if access token is still valid
      var today = new Date();
      var currentTime = today.getTime();
      if(currentTime < settings.google_access_token_expiration)
      {
        //use the current access token
        accessToken = settings.google_access_token;
        deferred.resolve(accessToken)
      }
      else
      {
        //refresh the access token
        authorize().then(function(token){

          accessToken = token;
          deferred.resolve(accessToken);

        }, function(error){

          deferred.reject(error);

        });
      }
    });

  }, function(error){
    deferred.reject(error);
  });

  return deferred.promise;
}

This function encapsulates the logic in a promise to check if the access token currently in the database is still valid. If not, it will make a call to the authorize() function and then bubble up the access token that is returned. 

GET Events

app.get('/events', function(request, response){

  var getGoogleEvents = function(accessToken)
  {
    //instantiate google calendar instance
    var google_calendar = new gcal.GoogleCalendar(accessToken);

    google_calendar.events.list(googleUserId, {'timeMin': new Date().toISOString()}, function(err, eventList){
      if(err){
        response.send(500, err);
      }
      else{
        response.writeHead(200, {"Content-Type": "application/json"});
        response.write(JSON.stringify(eventList, null, '\t'));
        response.end();
      }
    });
  };

  //retrieve current access token
  getAccessToken().then(function(accessToken){
    getGoogleEvents(accessToken);
  }, function(error){
    //TODO: handle getAccessToken error
  });

});

Here we begin to see the magic of javascript promises, really allowing me to simplify the logic in the GET events method. 

A call to the getAccessToken() function is made, returning the current access token in a promise. After the access token is successfully returned, I use the google-calendar node library to retrieve the Google event list, format the JSON for readability, and send it back to the browser. 

POST Event

app.post('/event', function(request, response){

  var addEventBody = {
    'status':'confirmed',
    'summary': request.body.contact.firstName + ' ' + request.body.contact.lastName,
    'description': request.body.contact.phone + '\n' + request.body.contact.details,
    'organizer': {
      'email': googleUserId,
      'self': true
    },
    'start': {
      'dateTime': request.body.startdate,
    },
    'end': {
      'dateTime': request.body.enddate
    },
    'attendees': [
        {
          'email': googleUserId,
          'organizer': true,
          'self': true,
          'responseStatus': 'needsAction'
        },
        {
          'email': request.body.contact.email,
        'organizer': false,
        'responseStatus': 'needsAction'
        }
    ]
  };

  var addGoogleEvent = function(accessToken){
    //instantiate google calendar instance
    var google_calendar = new gcal.GoogleCalendar(accessToken);
    google_calendar.events.insert(googleUserId, addEventBody, function(addEventError, addEventResponse){
      console.log('GOOGLE RESPONSE:', addEventError, addEventResponse);

      if(!addEventError)
        response.send(200, addEventResponse);

      response.send(400, addEventError);
    });
  };

  //retrieve current access token
  getAccessToken().then(function(accessToken){
    addGoogleEvent(accessToken);
  }, function(error){
    //TODO: handle error
  });

});

The POST event method allows me to insert a calendar event into Google Calendar from the object data collected from the browser. Upon success, the new event is returned allowing me to update the user interface.

I'm sure this code could be improved a ton, but I'm definitely happy with the result, as is my friend. I plan on following up in the future with improvements I find and I hope this post was helpful to you!!

Continue on to Part 2

Vote on HN


comments powered by Disqus