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.