Creating a Role-Based User Authentication System with Angular, Express and MySQL — Part 3: The Front end

Here we are at the last part in the series. Now that we have a working API, we can finish our application off by building its interface using Angular. Let's get to it!

The UI app should handle three simple tasks:

  • Manage routing the SPA way.
  • Introduce forms and (very) basic form validation.
  • Consume the back end API.

This post is the third and final part of the Creating a Role-Based User Authentication System with Angular, Express and MySQL series.

GitHub Repository

The code from this post is available on the part-3 and master branch in the project's GitHub repository. Make sure you check it out!

View on GitHub

Angular Integration

We already made progress in this direction. In the second part, we created the index.html file and included Angular in our project. We now have to add the remaining missing libraries and kickstart the UI.

# public/app/views/index.html
<!doctype html>  
<html class="no-js" lang="en" ng-app="jamesAuth">  
    <head>
        <meta charset="utf-8">
        <base href="/">
        <meta http-equiv="x-ua-compatible" content="ie=edge">
        <title>JAMES Authentication</title>
        <meta name="description" content="Authentication system using JWTs, Angular, MySQL, Express and the Sequelize ORM.">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    </head>
    <body class="container">

        <div ui-view></div>

        <!-- Libraries -->
        <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.3/angular.min.js"></script>
        <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular-cookies.min.js"></script>
        <script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.4.2/angular-ui-router.min.js"></script>

        <!-- App Scripts -->
        <script src="app/app.js"></script>
    </body>
</html>  

Let's go through what's new in the index file step by step:

  1. We set our application root on the <html> element.

  2. In the document's <head> we included Bootstrap.

  3. We added the ui-view directive as the routing entry point in the document body.

  4. Finally, before the </body> closing tag we included the Angular Cookies and the UI-Router libraries. Right after that the app's entry point.

Great! We can now kickstart the Angular application. Let's create the app.js file and add the following boilerplate code inside:

# public/app/app.js
(function() {
    'use strict';

    var jamesAuth = angular.module('jamesAuth', [
        'ui.router',
        'ngCookies'
    ]);

    // Static data constant.
    var staticData = {};

    var userRoles = staticData.userRoles = {
        guest: 1,
        user: 2,
        admin: 4
    };

    staticData.accessLevels = {
        guest: userRoles.guest | userRoles.user | userRoles.admin,
        user: userRoles.user | userRoles.admin,
        admin: userRoles.admin
    };

    jamesAuth.constant('staticData', staticData);

    // Config block.
    jamesAuth.config([
        '$stateProvider',
        '$urlRouterProvider',
        '$httpProvider',
        '$locationProvider',
        'staticData',
        authConfig
    ]);

    function authConfig(
        $stateProvider,
        $urlRouterProvider,
        $httpProvider,
        $locationProvider,
        staticData ) {

        // TODO: Define routes here.

        $locationProvider.html5Mode(true);
    }
})();

In the code snippet above, we first define the jamesAuth module and set the ui.router and the ngCookies as dependent modules.

Following, we create a staticData constant with the same user roles and access levels we used on the back end.

We then define the config block and inject the $stateProvider, $urlRouterProvider, $httpProvider, $locationProvider and the staticData constant created earlier.

Inside the authConfig function we enable HTML5 mode which will attempt to use the browser history pushState if supported. This will also help us get rid of ugly URLs.

Easy, right? Having this, we can now create our first routes. Let's take care of the homepage first.

Homepage

Inside the authConfig function, we can create the index (or homepage) route using the following code snippet:

# public/app/app.js
// ...

// Index route.
$stateProvider.state('index', {
    url: '/',
    templateUrl: 'app/views/partials/partial-index.html'
});

// ...

We are using the $stateProvider to define a new state called index which will load the index URL.

For this state, we're using a template called partial-index.html which will be stored inside the app/views/partials folder.

We can now create the partial template and add some markup inside.

# app/views/partials/partial-index.html
<div class="jumbotron text-center">  
    <h1>Welcome!</h1>
    <p>This is an authentication system built using JWTs, Angular, MySQL, Express and the Sequelize ORM.</p>
    <a href="#" target="_blank">
        <button class="btn btn-success btn-lg">Learn More</button>
    </a>
</div>  

After we save the file, restart the server and reload the browser, we should see the Bootstrap jumbotron.

Login

Following the same method, we can now create the login route and provide our users a nice login form.

# public/app/app.js
// ...

// Login route.
$stateProvider.state('login', {
    url: '/login',
    templateUrl: 'app/views/partials/partial-login.html',
    controller: 'LoginController as lc'
});

// ...

For this state we'll also use a controller called LoginController which we'll use to store some of the login logic.

# app/controllers/loginController.js
(function() {
    'use strict';

    angular
        .module('jamesAuth')
        .controller('LoginController', [
            '$state',
            'authService',
            loginController
        ]);

    function loginController($state, authService) {
        var vm = this;

        vm.loginError = false
        vm.loginErrorMessage = null;

        vm.login = login;

        function login() {
            vm.loginError = false
            vm.loginErrorMessage = null;

            if(!vm.username || !vm.password) {
                vm.loginError = true;
                vm.loginErrorMessage = 'Username and password required!';
                return;
            }

            authService.login(vm.username, vm.password)
                .then(handleSuccessfulLogin)
                .catch(handleFailedLogin);
        }   

        function handleSuccessfulLogin() {
            $state.go('index');
        }

        function handleFailedLogin(response) {
            if(response && response.data) {
                vm.loginErrorMessage = response.data.message;
                vm.loginError = true;
            }
        }

    }
})();

Using the code above, we create the LoginController needed for our route. In it, we inject the $state and a new service called authService. We haven't created that service yet, but we plan to abstract some of the login logic inside of it.

Next, we initialize the loginError flag and the loginErrorMessage values on the view model. We will use these later to notify the user about failed login errors.

Following, we define a single public method on the view model called login, which does a couple of things:

  1. First, it will reset the error flags — in case there was any error set.

  2. Next, it will check if the view model username or password are both provided. If not, it sets the flags accordingly.

  3. Finally, if everything went well, it will use the authService to pass the username and password and continue the login action.

The authService.login() method is expected to return a promise. If the promise resolves, it will redirect the user to the index route. Otherwise, it will notify the user and set the response message as the error message.

Let's go back to our state and add the missing template markup for our login form.

# app/views/partials/partial-login.html
<form class="form-auth">  
    <h2 class="form-auth-heading">Login</h2>

    <div class="alert alert-danger" ng-if="lc.loginError" role="alert">{{ lc.loginErrorMessage }}</div>

    <label for="inputUsername" class="sr-only">Username</label>
    <input type="text" id="inputUsername" ng-model="lc.username" class="form-control" placeholder="Username" required autofocus>

    <label for="inputPassword" class="sr-only">Password</label>
    <input type="password" id="inputPassword" ng-model="lc.password" class="form-control" placeholder="Password" required>

    <button class="btn btn-lg btn-primary btn-block" type="submit" ng-click="lc.login()">Login</button>
</form>  

The markup above defines a form with two inputs for the username and password and their models.

Below the page heading, we are using the loginError flag to control whether or not to display an alert. Its content will be the loginErrorMessage view model value.

Finally, we specify that the login() method should run when the user clicks the Login button.

Before we can test this in the browser, we need to create the missing authService and include the missing scripts in our document.

Authentication Service

To keep repetition to a minimum and our controllers simple, we can abstract most of the authentication logic inside a service. We'll call it authService and we will place it inside the app/services folder.

# app/services/authService.js
(function() {
    'use strict';

    angular
        .module('jamesAuth')
        .factory('authService', [
            '$http',
            '$cookies',
            '$state',
            authService
        ]);

    function authService($http, $cookies, $state) {

        var authService = {
            login: login,
            logout: logout,
            signup: signup,
            getUserData: getUserData,
            isAuthenticated: isAuthenticated
        };

        return authService;

        function login(username, password) {
            // TODO: Login user.
        }

        function logout() {
            // TODO: Logout user.
        }

        function isAuthenticated() {
            // TODO: Check if the user is authenticated.
        }

        function getUserData() {
            // TODO: Return the user data.
        }

        function signup(username, password) {
            // TODO: Register a new user.
        }
    }
})();

We are using the code above as the base skeleton for the authService.

Inside the service, we inject the $http, $cookies and the $state services for later use.

Following, we expose five public methods: login, logout, signup, getUserData and isAuthenticated.

Let's write some code for our login method:

# app/services/authService.js
// ...

function login(username, password) {
    var reqObj = {
        method: 'POST',
        url: '/api/authenticate',
        data: {
            username: username,
            password: password
        }
    };

    return $http(reqObj).then(function(response) {
        if(response && response.data) {
            response = response.data;

            var expires = new Date(),
                user = {};

            user.username = response.username;
            user.role = response.role;
            user.token = response.token;

            expires.setTime(expires.getTime() + (30 * 60 * 1000));

            $cookies.put(
                'user',
                JSON.stringify(user),
                { expires: expires }
            );
        }
    });
}

//...

The login function handles a few things for us:

  1. First, it builds a request object which specifies that a POST request should be made to the /api/authenticate endpoint. It also sends as data the username and password values.

  2. It then makes the request using the object and the $http service.

  3. If successful, it creates a new user object with the response username, role and token returned from the server. An expires variable is also declared with a date 30 minutes in the future from now.

  4. Finally, using the $cookies service, we set a cookie with its value as the user object, and the expiry date stringified.

Inside the index.html file we can now include the missing scripts and test drive our login method.

# app/views/index.html
// ...

<!-- App Scripts -->  
<script src="app/app.js"></script>  
<script src="app/services/authService.js"></script>  
<script src="app/controllers/loginController.js"></script>

//...

Accessing the http://localhost:8080/login route we can now see the ugliest login form ever.

If I attempt to login with a non-existent user, I receive an error. Good!

Using a real user, I get redirected to the index state. Perfect! I also checked if the cookie is set correctly and wasn't disappointed.

User Area (Profile)

After logging in, it would be great if I could access my profile page that would display some details. More exactly, the greeting we set to be returned on the /api/profile endpoint. We can make a route for this!

# public/app/app.js
// ...

// User area route.
$stateProvider.state('profile', {
    url: '/profile',
    templateUrl: 'app/views/partials/partial-profile.html',
    controller: 'ProfileController as pc',
    data: {
        accessLevel: staticData.accessLevels.user
    }
});

// ...

We're creating this state the same as before. The only difference is the extra accessLevel value added on the state's data object.

By doing so, we can check the current user's role against the state's access level and prevent him/her from accessing the route if not allowed.

Let's finish off the profile template and controller and then we'll get back to the access level check logic.

# app/views/partials/partial-profile.html
<h1>User Profile</h1>  
<p>{{ pc.message }}</p>  

The template for our profile page is plain simple.

We display a state specific title, and we'll use the message model to show the returned data from the server.

Inside the ProfileController we only request the user profile information and assign it to the message view model.

# app/controllers/profileController.js
(function() {
    'use strict';

    angular
        .module('jamesAuth')
        .controller('ProfileController', [
            '$http',
            profileController
        ]);

    function profileController($http) {
        var vm = this;

        vm.message = '';

        $http({ method: 'GET', url: '/api/profile' })
            .then(function(response) {
                if(response && response.data) {
                    vm.message = response.data.message;
                }
            });
    }

})();

Before proceeding, don't forget to include the profileController.js file inside index.html.

It won't work just yet cowboy! If you were eager to jump into testing mode and loaded the /profile page, you've seen that you're not getting anything from the server just yet other than an Unauthorized error in the console.

This happens because, in order to access the /api/profile endpoint, we need to pass the token in the request.

Request Interceptor

Using a request interceptor, we can intercept every request we make and check if there's a logged in user. During this time, we can also attach the token to the Authorization header. Easy enough!

Let's write it right before the staticData constant in the app.js file.

# app/app.js
// ...

// API Request Interceptor
jamesAuth.factory('requestInterceptor', [
    '$cookies',
    function($cookies) {
        return {
            request: function(config) {
                var user = $cookies.get('user'),
                    token = null;

                if(user) {
                    user = JSON.parse(user);
                    token = user.token ? user.token : null;
                }

                if(token) {
                    config.headers = config.headers || {};
                    config.headers.Authorization = token;
                }

                return config; 
            }
        };
    }
]);

// ...

We can now add it to the list of interceptors at the very bottom of our config block using the $httpProvider.

# app/app.js
// ...

$httpProvider.interceptors.push('requestInterceptor');

// ...

Now if we refresh the browser and access the profile page we should receive the message we set in our Express app. We're crazy cool right now!

Access Level Check

Currently, even non-registered users can access the profile page even though the server won't return anything.

Using the access level value we set on our route earlier, we can check if the current user is allowed to access it on every state change.

For this check we will need to create a run block for our Angular app. At the bottom of the app.js file, add the following code snippet:

# app/app.js
// ...

// Run block.
jamesAuth.run([
    '$rootScope',
    '$state',
    'authService',
    authRun
]);

function authRun($rootScope, $state, authService) {
    $rootScope.$on('$stateChangeStart', function(event, toState) {
        if(toState.data && toState.data.accessLevel) {
            var user = authService.getUserData();
            if(!(toState.data.accessLevel & user.role)) {
                event.preventDefault();
                $state.go('index');
                return;
            }
        }
    });
}

// ...

In the run block, we're listening to the $stateChangeStart event and check if the state that needs to load has an accessLevel value defined.

If it does, we're using the same piece of logic we used on the server to check if the current user's role allows it. If not, we redirect that sneaky bastard to the index state.

To retrieve the logged in user's data, we're using a method called getUserData from our authService which is curently missing.

When getting the user's data, we will also need to check if there is actually a user logged in. Since this is a common task, it would be best if we moved the logic into a separate function. Who knows? Maybe we want to use it in the future.

Let's add the necessary logic for our isAuthenticated and getUserData methods.

# app/services/authService.js
 // ...

function isAuthenticated() {
    var user = $cookies.get('user');
    return user && user !== 'undefined';
}

function getUserData() {
    if(isAuthenticated()) {
        return JSON.parse($cookies.get('user'));
    }

    return false;
}

// ...

That's it! Attempting to access the profile page while being a 'guest' should redirect us to the index page.

Admin Area

Rinse and repeat for the admin area! The state configuration inside the config block should look like this:

# app/app.js
// ...

// Admin area route.
$stateProvider.state('admin', {
    url: '/admin',
    templateUrl: 'app/views/partials/partial-admin.html',
    controller: 'AdminController as ac',
    data: {
        accessLevel: staticData.accessLevels.admin
    }
});

// ...

Instead of using a user access level, we're setting an admin access level here.

The AdminController works the same as the ProfileController:

# /app/controllers/adminController.js
(function() {
    'use strict';

    angular
        .module('jamesAuth')
        .controller('AdminController', [
            '$http',
            adminController
        ]);

    function adminController($http) {
        var vm = this;

        vm.message = '';

        $http({ method: 'GET', url: '/api/admin' })
            .then(function(response) {
                if(response && response.data) {
                    vm.message = response.data.message;
                }
            });
    }
})();

The only minor difference except the naming is that we're requesting the /api/admin endpoint.

And finally, the same simplistic template.

# app/views/partials/partial-admin.html
<h1>Admin Area</h1>  
<p>{{ ac.message }}</p>  

Let's give it a try using an admin user now!

Registration

Our app is crazy fun, and everyone wants to create an account. Let's complement our login form with another one for signing up.

# app/app.js
// ...

// Signup route.
$stateProvider.state('signup', {
    url: '/signup',
    templateUrl: 'app/views/partials/partial-signup.html',
    controller: 'SignupController as sc'
});

// ...

We'll also use a similar template.

# app/views/partials/partial-signup.html
<form class="form-auth">  
    <h2 class="form-auth-heading">Signup</h2>

    <div class="alert alert-danger" ng-if="sc.signupError" role="alert">{{ sc.signupErrorMessage }}</div>
    <div class="alert alert-success" ng-if="sc.signupSuccess" role="alert">Account created! You can <a ui-sref="login">login</a> now!</div>

    <label for="inputUsername" class="sr-only">Username</label>
    <input type="text" id="inputUsername" ng-model="sc.username" class="form-control" placeholder="Username" required autofocus>

    <label for="inputPassword" class="sr-only">Password</label>
    <input type="password" id="inputPassword" ng-model="sc.password" class="form-control" placeholder="Password" required>

    <button class="btn btn-lg btn-primary btn-block" type="submit" ng-click="sc.signup()">Signup</button>
</form>  

A notable difference here is a new success alert that we'll show on a successful account creation.

# app/controllers/signupController.js
(function() {
    'use strict';

    angular
        .module('jamesAuth')
        .controller('SignupController', [
            '$scope',
            'authService', 
            signupController
        ]);

    function signupController($scope, authService) {
        var vm = this;

        vm.signupSuccess = false;
        vm.signupError = false
        vm.signupErrorMessage = null;

        vm.signup = signup;

        function signup() {
            vm.signupSuccess = false;
            vm.signupError = false
            vm.signupErrorMessage = null;

            if(!vm.username || !vm.password) {
                vm.signupError = true;
                vm.signupErrorMessage = 'Username and password required!';
                return;
            }

            authService.signup(vm.username, vm.password)
                .then(handleSuccessfulSignup)
                .catch(handleFailedSignup);
        }

        function handleSuccessfulSignup(response) {
            vm.signupSuccess = true;
        }

        function handleFailedSignup(response) {
            vm.signupSuccess = false;

            if(response && response.data) {
                vm.signupErrorMessage = response.data.message;
                vm.signupError = true;
            }
        }
    }

})();

Inside the SignupController we define a signup() method that does the following:

  • Sets the appropriate flags
  • Checks the username and password
  • Uses the authService.signup() method to create a new account.

Note: We're not validating the form the best way here. Instead, a good practice would be to use the Angular way. To learn more, make sure you check out this cool article about AngularJS Form Validation on Scotch.io.

Let's finish off by adding the necessary code inside our authentication service.

# app/services/authService.js
// ... 

function signup(username, password) {
    var reqObj = {
        method: 'POST',
        url: '/api/signup',
        data: {
            username: username,
            password: password
        }
    };

    return $http(reqObj);
}

// ...

Using the new sign up form, I can create a new user account and log in with it.

It's time to make everything stick together and introduce navigation to our users. Inside the index.html file, above the ui-view directive, let's add our navigation bar:

// ...

<nav class="navbar navbar-inverse" role="navigation" ng-controller="NavController as nc">  
    <div class="navbar-header">
        <a class="navbar-brand" ui-sref="index">JAMES Authentication</a>
    </div>
    <ul class="nav navbar-nav pull-right" ng-if="!nc.isAuthenticated()">
        <li><a ui-sref="login">Login</a></li>
        <li><a ui-sref="signup">Signup</a></li>
    </ul>

    <ul class="nav navbar-nav pull-right" ng-if="nc.isAuthenticated()">
        <li><a ui-sref="profile">Profile</a></li>
        <li><a ng-click="nc.logout()">Logout</a></li>
    </ul>
</nav>

// ...

For our navigation we're using a NavController as we'd like to create a method that allows users to log out.

Inside, we create two unordered lists that act as our application's menus. One for guests and one for the logged in users.

We are controlling which menu should be visible using the isAuthenticated method. I knew we had to use it again!!

For each menu item we use the ui-sref directive to transition between states. Smooth!

Let's finish off the nav by adding the missing controller.

# app/controllers/navController.js

(function() {
    'use strict';

    angular
        .module('jamesAuth')
        .controller('NavController', [
            'authService',
            navController
        ]);

    function navController(authService) {
        var vm = this;

        vm.isAuthenticated = authService.isAuthenticated;
        vm.logout = authService.logout;
    }

})();

Inside we're simply referencing methods from the authentication service. The only one missing is the logout.

Going back to the authService, we can now write the necessary logic for logging users out:

# /app/services/authService.js
// ...

function logout() {
    $cookies.remove('user');
    $state.go('index');
}

// ...

The function removes the stored cookie and redirects the user to the index state.

After making sure all missing scripts are included in the index.html file, we can go ahead and try out the new navigation.

Conclusion

Congrats on making it this far! We finished our small project and I would like to thank you for following me this far. During this three-part series we've tested our full stack skills and managed to create a role based user authentication system using a bunch of cool libraries. Using our creativity we can take this further and extend it.

Don't be shy! Share your thoughts about this post or ask for help if you're stuck in the comments below. I'll be glad to assist.

Sharing is Caring

If you 💖 this post, I'd appreciate if you'd let your friends know about it too. You can use the floating buttons to share this post on various social networks. Thanks! :)

Nicolae C. Vasile

Nicolae C. Vasile

I'm Nick, a Software Engineer and Designer.