The problem: how to redirect user to originally requested url after login or sign up

You've set up your single-page app using Angular.js on the front end and have set up authentication for certain routes. If the user clicks a link to a protected route, they will be redirected to a login page. That's all working. Great!

Now, you want to implement some sort of email invitation system (I'm using SendGrid for that) where a user is invited to a protected resource, i.e. one that requires the user to be authenticated, but you want the app to remember that originally requested url after login or sign up. Let's use an example.

I'm developing an application as part of a group project at Hack Reactor that would allow various friends or colleagues to figure out a central meeting point such as a restaurant, cafe, or bar using Google Maps API. Each group meeting is unique and is assigned its own url. Think of it as a room where people can be invited to it and you wouldn't want people to be invited to or access the wrong room.

Using UI-router

UI-router is an angular module that can handle your apps routing. From the UI-router website:

AngularUI Router is a routing framework for AngularJS, which allows you to organize the parts of your interface into a state machine. Unlike the $route service in the Angular ngRoute module, which is organized around URL routes, UI-Router is organized around states, which may optionally have routes, as well as other behavior, attached.

When setting up states with UI-router, in order to set a route as protected (requires authentication), you would do this in the routing.

angular.module('mySweetApp')
  .config(['$stateProvider', function($stateProvider) {
    $stateProvider
      .state('rooms', {
        url: '/rooms',
        templateUrl: 'rooms/rooms.html',
        controller: 'roomsCtrl',
        authenticate: true
      })
     .state('room', {
		url:'/rooms/:Id',
	   templateUrl: 'rooms/room.html',
	 controller: 'roomCtrl',
        authenticate: true
	  })
 }]);

We inject $stateProvider from the UI-router module and set up the /rooms route and set an authenticate property of true for it. We also set up a route for the unique rooms with /rooms/:Id so if anything is after the last /, it will be route to this state. Now, if anyone tries to visit this route without being logged in, they will be sent to our login page (actually, we have not quite implemented the latter part but we are almost there).

Redirect to the login but remember what was the originally requested url

To redirect to the login if not authenticated, we will head over to the main app.js file where we first define the angular module. Note that I will not be going into how to set up authentication as that is better left for another post. For our purposes, we will assume that our app already has authentication set up and is using a Factory or Service called Auth to remember if a user is authenticated. Now, inside our app.js file we will have something like this:

angular.module('mySweetApp', ['ui.router', ... ])
	.config(function ($stateProvider, $urlRouterProvider, $locationProvider, $httpProvider) {
		$urlRouterProvider
        //any url that doesn't exist in routes redirect to '/'
 		 .otherwise('/'); 
         
	  //Do other stuff here
	 })
 .run(function ($rootScope, $location, Auth) {
	// Redirect to login if route requires auth and you're not logged in
	 $rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
  		Auth.isLoggedInAsync(function(loggedIn) {
    		if (toState.authenticate && !loggedIn) {
     			 $rootScope.returnToState = toState.url;
     			 $rootScope.returnToStateParams = toParams.Id;
     			 $location.path('/login');
  		    }
	   });
	 });
  });

We are interested in the .run section. $rootScope.$on('$stateChangeStart'... is a UI-router event that fires when a state changes, in our case when we go from one route to the next. The callback function takes at least five parameters but I'm only looking at the first three here (there is also fromState and fromParams). toState is an object that has a url property on it with the requested url, or the to url. Similarly, from has the from url. In our code, we are using our Auth.isLoggedInAsync from our factory that checks if someone is logged in when they request a resource. It takes a callback with a parameter loggedIn that will tell us if the user is logged in or not. If not logged in !loggedIn and the route the user was trying to get to requires authentication, meaning that toState.authenticate will be true (recall that we set an authenticate property in our state above), we will store the toState.url in a property on the $rootScope, $rootScope.returnToState which will make it available anywhere inside the app. Similarly, we store the toParams.id on the $rootScope because this holds our :Id value from our room state above when we configured $stateProvider.

Lastly, we redirect to /login with $location.path('/login') if a route is requested that requires authentication.

The final step, redirecting to initially requested url after login

You've made it this far, so let's get there fast, eh? The last part requires redirecting to the initially requested url after login. Let's suppose our user is trying to get to a room with a url that looks like this rooms/123456 where 123456 is unique to that room. In our controller for the login page and for the sign up page, we can do something like this:

$scope.login = function(form) {
  $scope.submitted = true;

  if(form.$valid) {
    Auth.login({
      email: $scope.user.email,
      password: $scope.user.password
    })
    .then( function() {
      // Logged in, redirect to correct room
      if( $rootScope.returnToState === "/rooms/:Id" ) {
        $location.path("/rooms/" + $rootScope.returnToStateParams);
      } else {
        //redirect all others after login to /rooms
        $location.path('/rooms');
      }
    })
    .catch( function(err) {
      $scope.errors.other = err.message;
    });
  }
};

Here we are using a function $scope.login in the controller to send the form data to the server to check if email and password are correct. Auth.login checks this using a promise and uses .then to redirect once the user is successfully authenticated. The part we are interested in here is checking if our user originally requested a url for one of our unique rooms. If so, $rootScope.returnToState that we set above will equal rooms/:Id. To change location to the desired room, we simply do $location.path("/rooms/" + $rootScope.returnToStateParams); where $rootScope.returnToStateParams will give us the unique room Id which was 123456 in the example above.

And if you have more unique routes to set up like our rooms example, you can just add another 'if' statement here.

I hope this helped solve a potential issue you faced. Feel free to leave a comment below.