The Authenticated App With AngularJS + DreamFactory User Management Part Two
by Terence Bennett • July 6, 2018
When we last left off our application had a few routes, corresponding controllers, and templates. All we learned how to do was wire up the DreamFactory User Management module to the app and respond to its events. Not exactly earth shattering stuff. But that's all about to change in this tutorial. We're going to go over some strategies to deal with data, capture and use data from the module, and protect routes in our app.
If you're just joining us I highly recommend reading and participating in Part One of this tutorial. It will help you understand some of the concepts behind the DreamFactory User Management module and the DSP (DreamFactory Services Platform). This tutorial builds upon the project from Part One. If you don't have that project feel free to download the files from here and follow along.
**Note** If you're donwloading the files for this project don't forget to set your 'DSP_URL' constant to your DSP address in app.js.
Let's get started!
Well, not exactly. Before we begin this tutorial I want to take a minute to discuss what role the user has in the application and what techniques we have for dealing with the user. Two things we're going to do require data about the user. We're going to show the correct navigation and protect certain routes based on whether or not the user is logged in. These two things have a global scope for our application. We also may want the current user to be available for other purposes that we can't think of right now.
We have a few options to address this problem.
- Option One: We could ask the DreamFactory User Management module for the user's data in every controller that's loaded. But that'll get messy and we'll be injecting our user everywhere. Not a great option for maintainability.
- Option Two: We could store our user in the $rootScope. But we don't want to pollute our $rootScope with user data and perhaps there are other areas of the application we may need $rootScope (possibly other modules) but don't necessarily want to provide access to the user.
- Option Three: We could create an application scoped controller that we could store the current user data on. Then other controllers could inherit from this application scoped controller as well as modify its data. That sounds like the best choice. Let's do that.
Step One: Create An Application Scoped Controller
Open the /scripts/controllers/main.js file and app the following code:
.controller('TopLevelAppCtrl', ['$scope', function($scope) {
}])
Your main.js should now resemble this:
Then open the index.html file and add this code just under the body tag:
<div data-ng-controller="TopLevelAppCtrl">
Then add the closing div tag just below the angularJS view entry point.
Your index.html should now resemble this:
We now have a controller that our other more specific child application controllers can inherit from. Let's try that out.
Add this code to the body of your TopLevelAppCtrl controller:
$scope.testVar = 'I was inherited';
Open the /views/main.html file and add the following code below the h1 tag:
<p></p>
Due to some Blog formatting issues we can't use double curly brackets in our code. We have to escape the brackets with a slash. Don't put the slashes in your code. Just enter the code as a normal angular expression.
Reload the application and navigate to the main page if you weren't already there. You should see 'I was inherited' printed below the heading. The MainCtrl inherited the $scope.testVar
scope variable from its parent controller (TopLevelAppCtrl). Only vars and functions stored on the '$scope' can be inherited. Defining a variable using var someVar = 'Some string'
won't be passed to the child controller.
Ok. Enough about Angular inheritance. Let's get back to the tutorial. Go ahead and remove the $scope.testVar = 'I was inherited'
line from the TopLevelAppCtrl and remove the <p></p>
from the main.html file.
Step Two: Storing Data On The TopLevelAppCtrl Controller
The TopLevelAppCtrl controller should check for a current user on instantiation. Basically if we reload the app and we're logged in we want the current user var to reflect that. In order to do this we need to first inject our DreamFactory User Management module's 'UserDataService' into the TopLevelAppCtrl. Modify your TopLevelAppCtrl controller to the following:
// inject 'UserDataService'
.controller('TopLevelAppCtrl', ['$scope', 'UserDataService', function ($scope, UserDataService) {// Add $scope variable to store the user
$scope.currentUser = UserDataService.getCurrentUser();
}])
Your TopLevelAppCtrl controller should now resemble:
**Note** The UserDataService provides a few methods for dealing with users. One of those being getCurrentUser(). This method will return false if there is no currentUser data member set in the service. If we have a current user it will return that current user object. For more information on this service please refer to the github readme.
We want to know that we have a logged in user. So let's open the main.html file and add the following code:
<code><p></p><</code>
Due to some Blog formatting issues we can't use double curly brackets in our code. We have to escape the brackets with a slash. Don't put the slashes in your code. Just enter the code as a normal angular expression.
Your main.html file should resemble:
We also need to update the user on login and logout events. Since we know that our $scope variables in our TopLevelAppCtrl controller are available through inheritance we can set the user in our login and logout success event handlers. To set a parent's $scope variable we'll have to access it through the '$parent' property. Let's do that now.
**Note** A child controller can access its parent's entire $scope through the $parent property of its own $scope.
Modify the LoginCtrl controller to:
.controller('LoginCtrl', ['$scope', '$location', 'UserEventsService', function($scope, $location, UserEventsService) {$scope.$on(UserEventsService.login.loginSuccess, function(e, userDataObj) {
$scope.$parent.currentUser = userDataObj;
$location.url('/');
});
}])
The DreamFactory User Management module has passed our currently logged in user object with the event. We use this data to set the $scope.currentUser
variable that was inherited from our parent controller.
We also need to unset the current user when they logout. To do so modify your LogoutCtrl controller to:
.controller('LogoutCtrl', ['$scope', '$location', 'UserEventsService', function($scope, $location, UserEventsService) {$scope.$on(UserEventsService.logout.logoutSuccess, function(e, userDataObj) {
$scope.$parent.currentUser = userDataObj;
$location.url('/')
})
}]);
Your main.js file should resemble:
If we reload the app we should be able to login and upon the redirect to the home page, see the current user's display name. My display name is 'Administrator'. Yours should be the display name of the user you have logged in as. Your app should resemble this when a user is logged in:
Step Three: Using User Data To Conditionally Show Navigation
Let's modify our navigation to only show links that are relevant for the current user status. We could just directly access the user on the TopLevelAppCtrl controller but we want to have good style and clear separation among the parts of our application so it's easy to maintain. That's going to require another controller. Add the following code to your main.js file:
.controller('NavigationCtrl', ['$scope', function($scope) {$scope.hasUser = false;
$scope.$watch('currentUser', function(newValue, oldValue) {
$scope.hasUser = !!newValue;
})
}])
We have set a variable called $scope.hasUser and initialized it to false. We have also set a watcher on the inherited 'currentUser' variable. When the 'currentUser' variable is updated by either our LoginCtrl or LogoutCtrl the NavigationCtrl will see that through the watcher and set its $scope.hasUser var accordingly. You may have noticed the $scope.hasUser = !!newValue
line. If you haven't seen this before; it's really just an 'if' statement. The preceding '!!' coerces the returned value of 'newValue' to be true or false rather than returning the actual value of 'newValue'. So it's the same as writing:
if (newValue != false) {
return true;
}
**Note** Remember that we inherit from the TopLevelAppCtrl controller. When we set $scope.$watch('currentUser', function(newValue, oldValue) {....}) we're really saying to AngularJS 'Hey, keep an eye on my parents $scope.currentUser value. If it changes pass me the newValue it was changed to and the oldValue it was changed from so I can do something.
Your main.js file should resemble:
Now that our controller is in place we need to set up the navigation to use it. Open your index.html file and modify the navigation like so:
<div data-ng-controller="NavigationCtrl">
<ul>
<li data-ng-if="!hasUser"><a href="#/login">Login</a></li>
<li data-ng-if="hasUser"><a href="#/logout">Logout</a></li>
</ul>
</div>
We add the controller on a div that is the parent of our navigation. We can now evaluate expressions in the context to the NavigationCtrl controller! Notice the use of the data-ng-if directives inside the list item tags. I love this directive because it doesn't just hide and show things in the HTML. It actually can add or remove things completely based on the expression. No more 'display:none'! Here we just have the data-ng-if evaluate the expresssion "hasUser" which is just the value of the $scope.hasUser
variable in the NavigationCtrl controller.
Your index.html should now look like:
Reload your app and give it a shot. Your navigation should modify itself based on the application's login status.
Step Four: Protecting A Route
Now we would like to protect a route. The idea is that if a user is not logged in they shouldn't be able to access a particular section of our application. Even if they hack at the url. So, to get started we need a route that we want to protect. Open your app.js file and add the following code:
.when('/user-info', {
templateUrl: 'views/user-info.html',
controller: 'UserInfoCtrl',
resolve: {
}
})
Your app.js file should resemble:
We're using the resolve property to help protect our routes. The resolve property in the router can run functions before a route actually, well, resolves. Think like the constuct program from the movie The Matrix. We can load data and check things before jumping in.
We want to add a function to see if we have a user. If we do we'll preload the user's data. If not we'll reroute to the login page. We can load AngularJS Services like $http or $location as well as our own like the 'UserDataService'. Let's see how that works. Add the following code to the body of the resolve property:
getUserData: ['$location', 'UserDataService', function($location, UserDataService) {if (!UserDataService.getCurrentUser()) {
$location.url('/login')
}else {
return UserDataService.getCurrentUser();
}
}]
Your app.js file's user-info route should resemble:
What we return from a resolve function can be injected into the controller for the route. Let's add the UserInfoCtrl controller and inject the data that we are resolving. Add the following code to your main.js file:
.controller('UserInfoCtrl', ['$scope', 'getUserData', function($scope, getUserData) {}]);
Your main.js file should look like:
We injected the name of the function we defined in the resolve property of our route. The data that was returned from that function is available to us there. Let's use it form something. Add the following code to your UserInfoCtrl controller body:
$scope.userData = getUserData;
Your main.js file should look like:
We need to create a template for '/user-info' route. Create user-info.html to the views folder and open the file. Add the following code:
<h3>User: </h3>
<p>First Name: </p>
<p>Last Name: </p>
<p>Is Admin: </p>
Due to some Blog formatting issues we can't use double curly brackets in our code. We have to escape the brackets with a slash. Don't put the slashes in your code. Just enter the code as a normal angular expression.
And we need a link to the page. So add this to your navigation list in index.html:
<li data-ng-if="hasUser" ><a href="#/user-info">User Info</a></li>
You should be able to reload your application and test at this point. The navigation should conditionally show based on user login status. The main app page should show the logged in user's display name. When you are logged in you should be able to navigate to the user-info page. You should be able to logout and if you try to type in the url to the user-info page while you are logged out it should redirect you to the login page.
Whew! That was a long one. But we learned a strategy for dealing with user data in an AngularJS application and, thanks to our DreamFactory User Management module, we communicated with our DSP without having to write any code. All in all I'd say that's pretty cool.
Be sure to check out Part Three of Authentication With AngularJS + DreamFactory User Management Module. In Part Three we'll explore the registration component of the DreamFactory User Management module and learn a strategy for dealing with errors in AngularJS.
Related reading: Building an AngularJS Application with DreamFactory
Terence Bennett, CEO of DreamFactory, has a wealth of experience in government IT systems and Google Cloud. His impressive background includes being a former U.S. Navy Intelligence Officer and a former member of Google's Red Team. Prior to becoming CEO, he served as COO at DreamFactory Software.