Detect route change in an Angular service - javascript

I'm trying to create a service to check if a certain route needs a user to be logged in to access the page. I have a working code but I want to place the $scope.$on('routeChangeStart) function inside the service. I want to place it in a service because I want to use it in multiple controllers. How do I go about this?
Current code:
profileInfoCtrl.js
angular.module('lmsApp', ['ngRoute'])
.controller('profileInfoCtrl', ['$scope', '$location', ' 'pageAuth', function($scope, $location, pageAuth){
//I want to include this in canAccess function
$scope.$on('$routeChangeStart', function(event, next) {
pageAuth.canAccess(event, next);
});
}]);
pageAuth.js
angular.module('lmsApp')
.service('pageAuth', ['$location', function ($location) {
this.canAccess = function(event, next) {
var user = firebase.auth().currentUser;
//requireAuth is a custom route property
if (next.$$route.requireAuth && user == null ) {
event.preventDefault(); //prevents route change
alert("You must be logged in to access page!");
}
else {
console.log("allowed");
}
}
}]);
routes.js
angular.module('lmsApp')
.config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider){
$routeProvider
.when('/admin', {
templateUrl: 'view/admin.html',
css: 'style/admin.css',
controller: 'adminCtrl',
requireAuth: true //custom property to prevent unauthenticated users
})
.otherwise({
redirectTo: '/'
});
}]);

By using $routeChangeStart, you are listening to a broadcast sent by $routeProvider on every change of the route. I don't think you need to call it in multiple places ( controllers ), just to check this.
In your service:
angular.module('lmsApp')
.service('pageAuth', ['$location', function ($location) {
var canAccess = function(event,next,current){
var user = firebase.auth().currentUser;
//requireAuth is a custom route property
if (next.$$route.requireAuth && user == null ) {
event.preventDefault(); //prevents route change
alert("You must be logged in to access page!");
}
else {
console.log("allowed");
}
}
$rootScope.$on('$routeChangeStart',canAccess);
}]);
And then inject your service in the .run() part of your application. This will ensure the check will be done automatically ( by the broadcast as mentioned earlier ).
In you config part :
angular.module('lmsApp')
.run(function runApp(pageAuth){
//rest of your stuff
});

You would add an event handler in the Service to $rootScope instead of $scope.
Also it would be much better if you would add the $routeChangeSuccess in the ".run" section so all the pages can be monitored from one point rather than adding it in every controller

You need to listen $rootScope instead of $scope.
And I think it's better to call that listener on the init of wrapped service, for instance (Services are singletons, so it will be run once you will inject it to any controller).
angular.module('lmsApp')
.service('stateService', ['$rootScope', function ($rootScope) {
$rootScope.$on('$locationChangeStart', (event, next, current) => {
// do your magic
});
}]);

Related

trying to set up several auth permission with angularjs

.state('newProduct', {
url: '/products/new',
templateUrl: 'app/products/templates/product-new.html',
controller: 'ProductNewCtrl',
authenticate: 'cook,admin'
})
I'm trying to add different client routes based on role authentifications but if I try to add another role such as cook for example it won't trigger the page defined by the url for both of them. It will work separately tho if that makes more sense
authenticate: 'cook',
authenticate: 'admin'
is this a syntax error?
In your .state block, 'authenticate' is simply a data holder
You will need to do a manual check on the value of authenticate to handle permissions.
For example:
myApp.config(function($stateProvider, $urlRouterProvider){
$stateProvider
.state("newProduct", {
url: '/products/new',
templateUrl: 'app/products/templates/product-new.html',
controller: 'ProductNewCtrl',
authenticate: true
})
});
Where you require to check permissions, you you will need access the $state u want using the $state object.
for example in your controller inject the $state object and use:
if($state.get('newProduct').authenticate){ //if(true) in this case)
//do what u want
}
If you want to check permissions everytime u change state/screen heres an example for that too:
angular.module("myApp").run(function ($rootScope, $state, AuthService) {
$rootScope.$on("$stateChangeStart", function(event, toState,
toParams,fromState, fromParams){
if (toState.authenticate){
// User is authenticated
// do what u want
}
});
});
found the fix thanks to Alon indexOf it got me thinking :)
.run(function($rootScope, $state, Auth) {
// Redirect to login if route requires auth and the user is not logged in
// also if the user role doesn't match with the one in `next.authenticate`
$rootScope.$on('$stateChangeStart', function(event, next) {
if (next.authenticate) {
var loggedIn = Auth.isLoggedIn(function(role) {
if (role && next.authenticate.indexOf(role[0]) !== -1) {
console.log('works')
return; // logged in and roles matches
}
event.preventDefault();
if(role) {
// logged in but not have the privileges (roles mismatch)
console.log(next.authenticate.indexOf(role[0]));
$state.go('onlyAdmin');
} else {
// not logged in
$state.go('login');
}
});
}
});
});

In Angular ui-router how to emit an event from one view to another view?

The use case is to change login button to text "logged in as xxx" after authentication.
I have devided my page to 3 views: header, content, footer. The login button is in the header view. When I click login, it transits to "app.login" state, and the content view changes to allow user input username and password.
Here's the routing code:
app.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('app', {
url: '/',
views: {
'header': {
templateUrl: 'static/templates/header.html',
controller: 'AppController'
},
'content': {
templateUrl: 'static/templates/home.html',
controller: 'HomeController'
},
'footer': {
templateUrl: 'static/templates/footer.html',
}
}
})
.state('app.login', {
url: 'login',
views: {
'content#': {
templateUrl : 'static/templates/login.html',
controller : 'LoginController'
}
}
})
The html template has code like this:
<li><span ng-if='loggedIn' class="navbar-text">
Signed in as {{currentUser.username}}</span>
</li>
LoginController set a $scope.loggedIn flag to true once authentication succeeded, but how can I populate that flag to the header view?
As I understand it I can't just use $scope.loggedIn in the html template as above because the $scope is different in two controllers. I know if LoginController is a child of AppController, then I can call $scope.$emit in LoginController with an event and call $scope.$on in AppController to capture it. But in this case the two controllers are for different views, how can I make them parent-child?
I know I can use $rootScope but as I'm told polluting $rootScope is the last resort so I'm trying to find a best practise. This must be a very common use cases so I must be missing something obvious.
You can use a factory to handle authentication:
app.factory( 'AuthService', function() {
var currentUser;
return {
login: function() {
// logic
},
logout: function() {
// logic
},
isLoggedIn: function() {
// logic
},
currentUser: function() {
return currentUser;
}
};
});
Than can inject the AuthService in your controllers.
The following code watches for changes in a value from the service (by calling the function specified) and then syncs the changed values:
app.controller( 'AppController', function( $scope, AuthService ) {
$scope.$watch( AuthService.isLoggedIn, function ( isLoggedIn ) {
$scope.isLoggedIn = isLoggedIn;
$scope.currentUser = AuthService.currentUser();
});
});
In such cases I typically opt to use a service to coordinate things. Service's are instantiated using new and then cached, so you effectively get a singleton. You can then put in a simple sub/pub pattern and you're good to go. A basic skeleton is as follows
angular.module('some-module').service('myCoordinationService', function() {
var callbacks = [];
this.register = function(cb) {
callbacks.push(cb);
};
this.send(message) {
callbacks.forEach(function(cb) {
cb(message);
});
};
}).controller('controller1', ['myCoordinationService', function(myCoordinationService) {
myCoordinationService.register(function(message) {
console.log('I was called with ' + message);
});
}).controller('controller2', ['myCoordinationService', function(myCoordinationService) {
myCoordinationService.send(123);
});
Do you use any serivce to keep logged user data? Basically serivces are singletons so they are good for solving that kind of problem without polluting $rootScope.
app.controller('LoginController', ['authService', '$scope', function (authService, $scope) {
$scope.login = function(username, password) {
//Some validation
authService.login(username, password);
}
}]);
app.controller('HeaderController', ['authService', '$scope', function (authService, $scope) {
$scope.authService = authService;
}]);
In your header html file:
<span ng-if="authService.isAuthenticated()">
{{ authService.getCurrentUser().userName }}
</span>

How to get my angular controller to reload data

I have an angular app with three views. When it loads it runs some code to populate the $scope variables. When I change views and then go back to the controller I want the initial code to run again but it doesn't. It seems it is cached and the $scope variables are not updated based on what happened.
How can I force the controller to run the initialisation code every time the view is loaded?
My routes:
app.config(function ($routeProvider) {
$routeProvider
.when('/', {
controller: 'HomeController',
templateUrl: 'home.html'
})
.when('/teach', {
controller: 'TeachController',
templateUrl: 'teach.html'
})
.otherwise({
redirectTo: '/'
});
});
The code I want to run every time the '/' route is clicked:
getSubPools.success(function(data) {
$scope.userPools = data;
});
Controller in full:
app.controller('HomeController', ['$scope', '$filter', 'stream', 'removeDroplet', 'qrecords', 'helps', 'get_user', 'updateRecords', 'getSubPools', function($scope, $filter, stream, removeDroplet, qrecords, helps, get_user, updateRecords, getSubPools) {
get_user.success(function(data) { //get current user
$scope.user = data;
});
getSubPools.success(function(data) {
$scope.userPools = data;
});
stream.success(function(data) {
$scope.stream = data;
if ($scope.stream.length === 0) { //determine if user has stream
$scope.noStream = true;
} else {
$scope.noStream = false;
}
$scope.getNumberReady(); //determine if any droplets are ready
if ($scope.numberReady === 0){
$scope.noneReady = true;
} else {
$scope.noneReady = false;
$scope.stream = $filter('orderBy')($scope.stream, 'next_ready'); //orders droplets by next ready
}
});
$scope.showEditStream = true;
$scope.showStream = false;
$scope.rightAnswer = false;
$scope.wrongAnswer = true;
$scope.noneReady = false;
$scope.subbedDroplets = [];
$scope.focusInput = false;
}]);
You can use the $routeChangeStart and $routeChangeSuccess events to reload the data into the controller:
https://docs.angularjs.org/api/ngRoute/service/$route
Edit:
As mohan said as this will work for every route change, you can make a service to catch these events and for each route broadcast a special event.
And in the relevant controller/service listen to this event and reload data
If you want to force reload, then add an click function like follows,
Note: This will work only if you use $stateProvider
Home
and in controller ,
$scope.goToHome = function(){
$state.transitionTo('home', {}, {reload:true});
}
The issue here was that on clicking the link to '/' not all of the initialisation code was rerunning. Rather than making calls to the database to get fresh data, angular was just returning old data. The way I fixed this was to rewrite my factories. The factories that were failing were written:
app.factory('stream', ['$http', function($http) {
return $http.get('/stream/')
.success(function(data) {
return data;
})
.error(function(err) {
return err;
});
}]);
The factory that worked every time was written:
app.factory('stream', ['$http', function($http) {
return {
fetch: function () {
return $http.get('/stream/');
}
}
}]);
Now it runs every time. I am not sure why though.

Angular issue with $scope and $routeParams

I'm trying to run a function if someone hits the submit button OR there is a value in routeParams (if user hits the page with param already filled out.) I would like a function to run. Im having a brain fart and can't seem to get this to work!
myApp.config([ '$routeProvider', function($routeProvider) {
$routeProvider.when('/:params?', {
templateUrl: 'myTemplate',
controller : 'myController'
}).otherwise({
redirectTo : '/'
});
} ]);
myApp.controller('ipCntrl', function($scope,$log,$http,$q,$routeParams, $location) {
$scope.runReport = function() {
$location.search({'ip': $routeParams['ip']})
}
});
myApp.controller('myController', function($scope,$log,$http,$q,$routeParams, $location) {
if ($routeParams['ip'])
{
$scope.ip = $routeParams['ip'];
runMyFunction();
}
<div ng-controller="ipCntrl">
<form ng-submit="runReport()">
<input class="form-control" ng-model="ip">
</form>
<div ng-view ></div>
</div>
<script type="text/ng-template" id="myTemplate">
HI!
</script>
Since you're trying to call a function on button click or on initialization after checking the $routeParams, just include that code in the angular controller you are using (ipCntrl)
myApp.controller('ipCntrl', function($scope,$log,$http,$q,$routeParams, $location) {
$scope.runReport = function() {
$location.search({'ip': $routeParams['ip']})
}
//Just put the if statement here
//you're already using the ng-submit to call this function from your form
if ($routeParam.ip != null) //other definition check logic
$scope.runReport();
});
You can use...
$rootScope.$on('$stateChangeStart',
function(event, toState, toParams, fromState, fromParams){
console.log('toState ->');
console.log(toState);
console.log('toParams ->');
console.log(toParams);
console.log('fromState ->');
console.log(fromState);
console.log('fromParams ->');
console.log(fromParams);
runMyFunction();
})
...in your controller's function to intercept state transitions and requested URls ($stateParams).
See State Change Events.
You also have View Load events that may be useful.
You can also intercept state changes (and evaluate requested routes) in Angular's .run().
See .run() ui-router's Sample App.

AngularJS: forward to login page for authentication, retain route as GET parameter?

I'm a bit of an Angular newbie. I'm trying to write an Angular service that on any page, will check if a user is logged in, and if not, forward them to a login page, passing their current path as a a GET parameter.
I'm almost there, but it's not quite working. The problem I'm having is as follows: if the user goes to #/articles/my-articles/, they get forwarded to #/login/?next=%2Farticles%2F:module%2F.
In other words, it looks as though Angular is passing the route pattern, not the actual URL.
This is my authentication code:
auth.run(['$rootScope', '$location', '$user', 'TOKEN_AUTH', 'PROJECT_SETTINGS', function ($rootScope, $location, $user, TOKEN_AUTH, PROJECT_SETTINGS) {
var MODULE_SETTINGS = angular.extend({}, TOKEN_AUTH, PROJECT_SETTINGS.TOKEN_AUTH);
$rootScope.$on('$routeChangeStart', function (e, next, current) {
if (next.$$route && !next.$$route.anonymous && !$user.authenticated) {
var nextParam = next.$$route.originalPath;
$location.url(MODULE_SETTINGS.LOGIN + '?next=' + nextParam);
}
});
}]);
I can get the original path in a hacky way using current.params.module - but that doesn't help me, because it seems that routeChangeStart is fired several times and the current object is undefined on all but the last fire.
This is my routes file:
articles.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/articles/:module/', {
templateUrl: 'views/articles/article_list.html',
controller: 'ArticlesListCtrl'
})
.when('/articles/:module/:id/', {
templateUrl: 'views/articles/article_detail.html',
controller: 'ArticlesDetailCtrl'
});
}]);
How can I fix this problem?
auth.run(['$rootScope', '$location', '$user', 'TOKEN_AUTH', 'PROJECT_SETTINGS', function ($rootScope, $location, $user, TOKEN_AUTH, PROJECT_SETTINGS) {
var MODULE_SETTINGS = angular.extend({}, TOKEN_AUTH, PROJECT_SETTINGS.TOKEN_AUTH);
$rootScope.$on('$routeChangeStart', function (e, next, current) {
if (!$user.authenticated) {
$location.url(MODULE_SETTINGS.LOGIN + '?next=' + $location.path());
$location.replace();
}
});
}]);
If logging in is not a AngularJS view, you may have to provide an otherwise route:
(depends on your $locationProvider config)
$routeProvider.otherwise({
template: 'Redirecting…',
controller : 'Redirect'
});
...
articles.controller('Redirect', ['$location', function($location) {
if (someConditionThatChecksIfUrlIsPartOfApp) {
location.href = $location.path();
return;
} else {
// Show 404
}
}]);
Side note: you shouldn't read $$-prefixed properties, they are private AngularJS variables.
Also note: don't use $ prefixes ($user) in your own code, these are public properties, reserved for AngularJS.
My solution works on Angular 1.2.13 :
preventDefault stops angular routing and $window.location sends me out to login page. This is working on a ASP.NET MVC + Angular app.
$rootScope.$on("$locationChangeStart", function (event, next, current) {
event.preventDefault();
$window.location = '/Login';
}
});

Categories

Resources