Votr Part 4: AngularJS and Authentication with CouchDB

August 27, 2013
Written by

carter-rabasa-seattle-shirt-lo-res (1)

This is the fourth part in a series of blog posts about building a real-time SMS and voice voting application using Node.js. In part one, we created the Node.js application and captured incoming votes over SMS and stored them in a CouchDB. In part two, we created a real-time visualization of the voting using Socket.io and Highcharts. In part three, we tweaked our app to scale to thousands of votes per second and millions of total votes.

Through the first 3 parts of this series we now have a scalable voting application that can process votes for events via SMS or voice and display the real-time progress of voting. However, there is currently no web interface for an administrator. Creating events or modifying them require making changes directly to the documents in CouchDB. In this blog post, I will walk through the process of creating a simple web interface for administrators using AngularJS.

This tutorial uses the Twilio Node.JS Module and the Twilio Rest Message API

Why Angular JS?

There are quite a few client-side MVC frameworks out there that a developer can use to build web apps. This is both a blessing and a curse. On the one hand, competition between all of these frameworks for your attention ensures that they will get better and better. On the other hand, it can be bewildering trying to figure out which one is the best fit for you and your project.

That being said, I needed to choose one. I played around with Backbone and Ember but ultimately settled on Angular for the for the following reasons:

AngularJS + Node.js + CouchDB

In this blog post, we’re going to cover:

  1. Creating the beginning of a simple AngularJS app
  2. Routing web requests to it through Node.js
  3. Authentication using CouchDB credentials

This is less of an exhaustive AngularJS 101 tutorial and more of a pragmatic walk-through on how to get AngularJS running, connect it to RESTful services being powered by Node.js and handle authentication using CouchDB. If that sounds good to you, let’s go!

Route Admin Requests to Angular JS

The first thing we need to do is take browser requests for /admin/ and route them to our AngularJS application. We’ll need to add a new Express route in app.js to handle this. In addition, we’re going to do a little bit of extra work to make sure that all requests are sent over HTTPS since we will be dealing with sensitive log-in credentials:

app.get('/admin/', function(req, res) {
  if(process.env.NODE_ENV === 'production' && req.headers['x-forwarded-proto'] !== 'https') {
    return res.redirect('https://' + req.get('Host') + req.url);
  }
  else {
    routes.admin(req, res);
  }
});

Now let’s add an admin method to our routes module to render a template called admin:

admin = exports.admin = function(req, res) {
  res.render('admin');
}

Create A View For The App

All requests for /admin/ now cause a template called admin to get rendered. Let’s create a file called admin.hjs and place it in our views directory. Think of this file as a template that contains the frame for the web page (header, footer, etc) and a portion inside of the frame that AngularJS will populate with dynamic content.

As I mentioned, AngularJS builds on the declarative nature of HTML and CSS. There are several custom attributes that we will use to declare our application’s intentions. Let’s walk through admin.hjs and discuss what some of these declarations do:

<html ng-app='votr'>

The ng-app declaration tells AngularJS that this this element and everything inside of it are part of the AngularJS application called votr.

<script src="/javascripts/angular.min.js"></script>
<script src="/javascripts/angular-resource.min.js"></script>
<script src="/javascripts/votr.js"></script>

Here we include the core AngularJS library, the ngResource library (more on that later) and our own votr.js file which defines our application.

<div class="content" ng-view></div>

Lastly, we use the ng-view declaration to specify that this is the <div> that we will populate with the content of our application.

Create The AngularJS App

The file votr.js will house the AngularJS application, so let’s look at some of the first few lines:

var app = angular.module('votr', ['ngResource']);

This declares our application, names it votr and specifies that it will rely on the ngResource module. Next, let’s define the routes that make up our admin application. In our application we’re only going to define three: logging-in, listing events and logging-out:

app.config(function($routeProvider) {
  $routeProvider
    .when('/', {templateUrl: 'event-list.html', controller: 'EventListCtrl'})
    .when('/login', {templateUrl: 'login.html', controller: 'LoginCtrl'})
    .when('/logout', {templateUrl: 'login.html', controller: 'LogoutCtrl'})
    .otherwise({redirectTo: '/'});
});

Remember, these are just the client-side routes for AngularJS. The server-side route for our Node.js application is /admin/. So the fully qualified URIs will look like this:

  • Login = /admin/#/login
  • Event List = /admin/#/
  • Logout = /admin/#/logout

Create A Login Controller and View

Now that most of the bootstrapping is behind us, we can start to focus on the HTML and JS that will power our application. Let’s start by going back and reviewing our route declarations and look for the one corresponding to the path /login:

.when('/login', {templateUrl: 'login.html', controller: 'LoginCtrl'})

So, the path /login is associated with a template called login.html and a controller called LoginCtrl. Let’s check out our login template:

<h3>Please Login</h3>
<div>
  <div class="alert alert-error" ng-show="loginError">Username and/or Password Incorrect</div>
  <form ng-submit="login()">
    <div><input type='text' ng-model='user.username' placeholder='username'/></div>
    <div><input type='password' ng-model='user.password' placeholder='password'/></div>
    <div><input class="btn" type="submit" value="Login"></div>
  </form>
</div>

This is a pretty simple form. The input tags include ng-model directives for the username and password. This directive is used to bind the value of what you type into those fields with an object called user that has username and password attributes.

Attached to the form tag is an AngularJS directive called ng-submit that points to a function called login. Since our /login path is associated with the LoginCtrl, you should expect a login function to be defined to handle the form submission, which it is:

app.controller('LoginCtrl', function($scope, $rootScope, $location, SessionService) {
  $scope.user = {username: '', password: ''};
  
  $scope.login = function() {
    $scope.user = SessionService.save($scope.user, function(success) {
      $rootScope.loggedIn = true;
      $location.path('/');
    }, function(error) {
      $scope.loginError = true;
    });
  };
});

Let’s Pause: $scope and Dependency Injection

Let’s not gloss over the parameters being passed-in to the LoginCtrl function. AngularJS makes extensive use of dependency injection. You’ll start to notice that none of the functions in an AngularJS application reference variables that aren’t passed-in to or declared within the function. No global variables are used.

There are numerous benefits to this approach that I don’t have time to cover here. From the perspective of a developer building an AngularJS application, please be aware that you’ll need to pass in any objects that your functions need to work properly. Some objects (such as $scope and $rootScope) don’t require any set-up on your part, they are pre-defined by the framework.

Other dependencies, such as the SessionService you see in LoginCtrl, will need to be defined and registered with AngularJS:

app.factory('SessionService', function($resource) {
  return $resource('/api/sessions');
});

Finally, let’s cover $scope and $rootScope. The $scope object is a container for all functions and objects that are needed for a single controller. In the case of LoginCtrl, $scope contains the object user (which hold username and password data) and the login function (which logs a user in to the web app). The $rootScope object, as the name implies, is a container for values and functionality needed across all controllers. In our app we use $rootScope to store the logged-in state of the user.

Authenticate Using CouchDB and Cookies

Now, before we can show the user a list of events we need to ask them to log-in. Rather than create an additional username/password system, we’ll piggyback on CouchDB’s authentication scheme. After all, if you can log-in to CouchDB directly and edit the data, you’re probably allowed the log-in to the web application to do the same.

When a user submits their username and password on the login form, these credentials are passed all the way down and used to log-in to CouchDB. CouchDB returns a cookie to use for subsequent authenticated requests to the database. When the cookie expires (or hasn’t been issued yet) CouchDB will return an HTTP 401 error.

The AngularJS application interacts with the server (i.e. the fetch a list of events) via AJAX calls. Errors are passed back, so we can keep an eye out for 401 errors to let us know that the user needs to log-in. Below is how you configure AngularJS to intercept and inspect HTTP requests:

app.config(function($httpProvider) {
  $httpProvider.interceptors.push(function($rootScope, $location, $q) {
    return {
      'request': function(request) {
        // if we're not logged-in to the AngularJS app, redirect to login page
        $rootScope.loggedIn = $rootScope.loggedIn || $rootScope.username;
        if (!$rootScope.loggedIn && $location.path() != '/login') {
          $location.path('/login');       
        }
        return request;
      },
      'responseError': function(rejection) {
        // if we're not logged-in to the web service, redirect to login page
        if (rejection.status === 401 && $location.path() != '/login') {
          $rootScope.loggedIn = false;
          $location.path('/login');
        }
        return $q.reject(rejection);         
      }
    };
  });
});

It’s worth going over that code again. The first block (request) covers all HTTP requests. The AngularJS application has a global variable ( $rootScope.loggedIn) that we use to store the login state. If this variable is undefined or false, the user is redirected to the login view.

The second block (responseError) covers the event of an HTTP error happening during an AJAX call. If the error is 401 (not logged in) the user is redirected to the login page.

Build A RESTful Login API

Getting back to the login function, you’ll notice a call to SessionService.save(). SessionService is a service class. In AngularJS you specify the unique id of the service and the code for how it should be instantiated by the service factory.

In our case SessionService is a thin wrapper around a $resource object. A $resource object provides a programmatic interface between your app and a RESTful web service. When you instantiate a $resource object, you specify the root endpoint for that RESTful service and AngularJS will convert your calls of get(), save(), etc into the equivalent RESTful HTTP requests to that endpoint.

In our case, when a user attempts to login, we execute save on the SessionService. This initiates a POST to the API endpoint that we defined. Let’s walk all the way through this form submission to the log-in to CouchDB itself starting with app.js:

app.post  ('/api/sessions',   routes.login);

In the routes module we have a handle to the sessions module. This is the module that we use to house all functionality related to logging-in. We call the login method, passing in the username and password:

login = exports.login = function(req, res) {
    sessions.login(req.body.username, req.body.password, function(err, cookie) {
      if (err) {
        res.send(401, JSON.stringify({error: true}));
      }
      else {
        //var authSession = cookie.split(';')[0].split('=')[1];
        //sessions.addLoggedInUser(req.body.username, cookie);
        res.cookie(cookie);
        //req.body['authSession'] = authSession;
        res.send(req.body);
      }
    });
  }
  }

Inside of the sessions module, we have a handle to our CouchDB client and we attempt to authenticate:

login = exports.login = function(username, password, callback) {
      db.auth(username, password, function (err, body, headers) {
        if (err) {
          return callback(err);
        }
        var cookie = headers['set-cookie'][0];
        var authSession = cookie.split(';')[0].split('=')[1];
        addLoggedInUser(authSession, username);
        callback(null, cookie);
      });
    };

If the login fails, we return the error. This errors bubbles its way back up and and HTTP error is returned to the client. The error handler of the call to SessionService.save is triggered and we set $scope.loginError to true.

In the markup for login.html you’ll notice a <div> tag that contains the error message. That <div> also includes the ng-show directive. This directive selective shows or hides an element based on the value specified. In our case, setting $scope.loginError to true caused the div to display. If it was set to false or undefined, it would be hidden.

Now, if authentication succeeds, we are given a cookie that we can use to make subsequent requests. We store this cookie in memory on the server and associate it with the username of the person who logged-in. This enables us to delete the cookie from memory when the user logs out. The success of the log-in bubbles up through our Node.js application is returned to the client as an HTTP 200. We then forward the user to the Event List view.

Seeing This In Action

Fire up the node process and point your browser at http://localhost:3000/admin/. Initially, the root of our AngularJS application will load ( /admin/#/) but we will soon get re-directed to /admin/#/login:

Votr1

At this point, go ahead and enter some bogus credentials. These will get passed all the way through to the CouchDB instance where authentication will fail and our server will return a 401 error. This will set loginError to true which will trigger the error message:

Votr2

Finally, go ahead and authenticate using your valid CouchDB credentials. This should succeed and the LoginCtrl will then redirect to the root path ( /admin/#/) which will load the…

VotrSuccess

To Be Continued…

And that’s where we’re going to stop! We’ve covered a ton of ground in this blog post, let’s quickly recapped the things we’ve learned:

  1. Setting up Node.js and Express to route requests to the AngularJS application
  2. Creating the HTML page to house the AngularJS app
  3. Defining the routes
  4. Building the controller and associated view the login experience
  5. Authentication using CouchDB and Cookies
  6. Handling login with a RESTful API and the $http interceptor

All of the code we’ve gone over here is posted on Github. In the next blog post, we will finish building this AngularJS application and add all of the functionality necessary to load a list of events, edit an event, create new events and delete events.

Join the conversation on Reddit!