Looking to build web app in Node.js with ability for user to log in (authentication), which has 3 non secure pages (/home, /contact, /about) and one secure page (/admin). As an aside, I've been referencing the scotch.io Mean Machine book.
The issue I'm having is that I've build everything out, and the login mechanism works in that when I log in, I get directed to /admin; however, when I go to /admin in the URL without logging in, I can still access the page. I.e. I'm not sure where to put the actual protection.
A bit below on how I've laid out my app. Hoping for as much a conceptual answer to suggest how I should be doing things, rather than necessarily only a code answer.
Services:
auth service posts to server the inputted username/password and returns either false or success (with user info and JWT token)
auth service also injects as AuthInterceptor the token (if there is one) into each HTTP header
Router:
angular.module('routerRoutes', ['ngRoute'])
.config(function($routeProvider, $locationProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/home.html',
controller: 'homeController',
controllerAs: 'home'
})
.when('/about', {
templateUrl: 'views/about.html',
controller: 'aboutController',
controllerAs: 'about'
})
.when('/contact', {
templateUrl: 'views/contact.html',
controller: 'contactController',
controllerAs: 'contact'
})
.when('/login', {
templateUrl: 'views/login.html',
controller: 'adminController',
controllerAs: 'login'
})
.when('/admin', {
templateUrl: 'views/admin/admin.html',
controller: 'adminController',
controllerAs: 'admin'
});
$locationProvider.html5Mode(true);
});
Controllers:
homeController, aboutController, contactController are generally empty for now
adminController:
.controller('adminController', function($rootScope, $location, Auth) {
var vm = this;
vm.loggedIn = Auth.isLoggedIn();
$rootScope.$on('$routeChangeStart', function() {
vm.loggedIn = Auth.isLoggedIn();
window.alert(vm.loggedIn); // this gives correct answer and works
Auth.getUser()
.success(function(data) {
vm.user = data;
});
});
vm.doLogin = function() {
vm.error = '';
Auth.login(vm.loginData.username, vm.loginData.password)
.success(function(data) {
vm.user = data.username;
if (data.success)
$location.path('/admin');
else
vm.error = data.message;
});
};
vm.doLogout = function() {
Auth.logout();
vm.user = {};
$location.path('/login');
};
});
And finally, below is my index.html (just the body):
<body class="container" ng-app="meanApp" ng-controller="adminController as admin">
<i class="fa fa-home">Home </i>
<i class="fa fa-shield">About </i>
<i class="fa fa-comment">Contact</i>
<i class="fa fa-comment">Admin</i>
<ul class="nav navbar-nav navbar-right">
<li ng-if="!admin.loggedIn">Login</li>
<li ng-if="admin.loggedIn" class="navbar-text">Hello {{ admin.user.username }}</li>
<li ng-if="admin.loggedIn">Logout</li>
</ul>
<main>
<div ng-view>
</div>
</main>
</body>
I won't paste the other html pages that get injected into since there isn't anything on them yet (the login.html has just the two input fields and submit button).
So a couple of questions:
In my index.html, when I click on /admin, it takes me to the admin page even if I'm not logged in. Where should I put the protection for that to not happen?
Any general comments on my setup and best practices that I'm not following?
Another nit:
I read that "li ng-if=" wouldn't show up in 'view source' if that branch of the decision tree wasn't hit, but it does. Was I misled or am I doing something wrong?
I took a custom property route to secure the routes in my application. Every state change taking place is listened for and inspected if it has this property. If it has this property set then it checks if user is logged in, if they are not, it routes them to the 'login' state.
I used UI-ROUTER in my current project where I have implemented this. I made a custom parameter called "data" that I used within the route.
Within a .config block to declare my opening routes:
$stateProvider
.state('login', {
url: '/login',
templateUrl: 'login/login.html',
controller: 'LoginController',
controllerAs: 'vm'
})
.state('home', {
url: '',
templateUrl: 'layout/shell.html',
controller: 'ShellController',
controllerAs: 'vm',
data: {
requireLogin: true
}
})
Then I add this to a .run on the application where I'm looking for ui-router's $stateChangeStart event and looking at my custom property ('data') on the state declaration:
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
var requireLogin = toState.hasOwnProperty('data') && toState.data.requireLogin;
if (requireLogin && !authService.isLoggedIn()) {
event.preventDefault();
authService.setDestinationState(toState.name);
$state.go('login');
}
if (toState.name !== 'login') {
authService.setDestinationState(toState.name);
}
});
In case you're wondering what the authService.setDestinationState does... it preserves the URL that the user was attempting to visit... once they successfully login it forwards them to that state automagically (see below):
function login() {
authService.authLogin(vm.credentials)
.then(loginComplete)
.catch(loginFailed);
function loginComplete(data, status, headers, config) {
vm.user = data;
$rootScope.$broadcast('authorized');
$state.go(authService.getDestinationState());
}
function loginFailed(status) {
console.log('XHR Failed for login.');
vm.user = undefined;
vm.error = 'Error: Invalid user or password. ' + status.error;
toastr.error(vm.error, {closeButton: true} );
}
}
When you define your Admin route, you can define a property called resolve. Each property within resolve is a function (it can be an injectable function). This function should return a promise, the promise's result can be injected into the controller.
For more information on resolve, look at http://odetocode.com/blogs/scott/archive/2014/05/20/using-resolve-in-angularjs-routes.aspx.
You can use resolve as follows to do an authentication check.
var authenticateRoute = ['$q', '$http' , function ($q, $http) {
var deferred = $q.defer();
$http.get("http://api.domain.com/whoami")
.then(function(response) {
if (response.data.userId) deferred.resolve(response.data.userId);
else window.location.href = "#/Login"
});
return deferred.promise();
}]
// ...
.when('/admin', {
templateUrl: 'views/admin/admin.html',
controller: 'adminController',
controllerAs: 'admin',
resolve: {
authenticatedAs: authenticateRoute
}
});
With this you could pass the authenticated User Id through - even if null - and let the controller deal with it, if for instance, you want a contextual message.
Else, you could do as above and only do so if there is a user Id from the authentication request, otherwise redirect to your login route.
Hope this helps! /AJ
Related
My problem is quite specific so I haven't been able to find an answer for this particular scenario anywhere, I did manage to get the functionality I require but would like to know if there is a better way of doing it. I will start by explaining the problem.
In index.html I have the following:
<html>
...
<lumx-navbar></lumx-navbar>
<div class="wrap">
<div ui-view></div>
</div>
...
</html>
lumxnavbar is a directive for the navigation bar of the application
nav.js
module.exports = function(appModule) {
appModule.directive('lumxNavbar', function(UserFactory, $window, $rootScope) {
return {
template: require('./nav.html'),
controller: function($scope) {
$scope.nav = require('../../static-data/nav.json');
$scope.user = $rootScope.user;
$scope.logout = function() {
UserFactory.logout();
$scope.user = '';
$window.location.href = '/#/login';
};
}
};
});
};
nav.html
<header>
...
<span ng-show="user" class="main-nav--version">Logged in as: {{user}}</span>
...
</header>
So the application starts with a login page at which point there is no user variable available anywhere. Once the user logs in there will be a user returned from a service.
my routes look like this (I am using angular-ui-router)
$stateProvider
.state('anon', {
abstact: true,
template: '<ui-view/>',
data: {
access: false
}
})
.state('anon.login', {
url: '/login',
template: require('../views/login.html'),
controller: 'LoginCtrl'
});
$stateProvider
.state('user', {
abstract: true,
template: '<ui-view/>',
data: {
access: true
}
})
.state('user.home', {
url: '/home',
template: require('../views/home.html'),
controller: 'HomeCtrl'
})
...
so the idea is that once the user logs in the navbar will change from
to this:
the only way I have found to accomplish this reliably is to do the following
instantiate a variable in $rootScope
appModule.run(function($rootScope, $location, UserFactory, $state) {
$rootScope.user = UserFactory.getUser();
...
then set the same variable from the login controller
...
$scope.login = function (username, password) {
UserFactory.login(username, password).then(function success(response) {
$rootScope.user = response.data.user.username;
$state.go('user.home');
}, handleError);
};
...
so this will update the navbar because of this line in the directive $scope.user = $rootScope.user; but my question is whether there is a better way of doing this without using $rootScope or would this be a suitable use for it?
Any input would be appreciated...:)
I have following routing with athentication, which is done via a PHP-Script and MySQL:
app.config
app.config(['$routeProvider',
function ($routeProvider) {
$routeProvider.
when('/login', {
title: 'Login',
templateUrl: 'partials/login.html',
controller: 'authCtrl'
})
.when('/logout', {
title: 'Logout',
templateUrl: 'partials/login.html',
controller: 'logoutCtrl'
})
.when('/dashboard', {
title: 'Dashboard',
templateUrl: 'partials/dashboard.html',
controller: 'authCtrl'
})
.otherwise({
redirectTo: '/login'
});
}])
.run(function ($rootScope, $location, Data) {
$rootScope.$on("$routeChangeStart", function (event, next, current) {
$rootScope.authenticated = false;
Data.get('session').then(function (results) {
if (results.uid) {
$rootScope.authenticated = true;
$rootScope.uid = results.uid;
$rootScope.name = results.name;
$rootScope.email = results.email;
} else {
var nextUrl = next.$$route.originalPath;
if (nextUrl == '/signup' || nextUrl == '/login') {
} else {
$location.path("/login");
}
}
});
});
});
authCtrl
app.controller('authCtrl', function ($scope, $rootScope, $routeParams, $location, $http, Data) {
$scope.login = {};
$scope.signup = {};
$scope.doLogin = function (customer) {
Data.post('login', {
customer: customer
}).then(function (results) {
Data.toast(results);
if (results.status == "success") {
$location.path('dashboard');
}
});
};
$scope.logout = function () {
Data.get('logout').then(function (results) {
Data.toast(results);
$location.path('login');
});
}
});
Now I want to change the navigation depending on the login-status. If user is logged in there should bei a logOUT-Button and a link to the dashboard. If the user isn't logged in it should look like this
sample for unlogged-in user
<header id="navigation">
<nav id="main">
<ul>
<li id="login"><i class="fa fa-power-off"></i> Login</li>
</ul>
</nav>
</header>
<main ng-view>
Content goes here
</main>
What is the best way for creating the navigation? Should it be a seperate template?
When things get complex, I'd rather use ui-router or maybe angular new router (haven't try this one yet) since they support multiple views in the same page. Thus nav becomes its own view, content its own view, etc. And to communicate between the views I'll use message passing with data in either $rootScope or some kind of shared state service.
So something like this... in the beginning, the login shared state is set to logged out. As the last part of login functionality, I'd set the login shared state and set it to logged in. Like I said, I rather make this a shared state service call since I can have it to also do $rootScope.$broadcast some sort of onLoginStateChange and pass the new value there.
Then I'd set up my nav view to listen to this onLoginStateChange using $scope.$on and set its internal view model state in its controller and bind that to ng-if directive so it will display Login if false, or Logout if true (logged in).
I'm building an app using sails.js backend and angular in frontend. I'm trying to prevent the user from accessing the admin control page if he's not authorized. I've run into couple of answers already, but none of them seem to fully work.
At the moment in my app.js, I have
$stateProvider
.state('home', {
url: "/home",
templateUrl: "home/homeTemplate.html",
controller: 'homeController'
})
.state('adminPage', {
url: "/adminPage",
templateUrl: "adminPage/adminTemplate.html",
controller: 'adminPageController',
resolve: {
validate: function($q, $sails, $location) {
var defer = $q.defer();
$sails.get("/user/getCurrentUser")
.success(function(response) {
if (response.user.accessAdminPage) {
defer.resolve();
}
else {
defer.reject("Access blocked");
$location.path('/');
}
return defer.promise;
})
}
}
})
The current code is partially working; The problem at the moment is, that when the unauthorized user first logs in and lands on the home page, and then accesses localhost:1337/#/adminPage, he actually reaches the page. The url in the address bar changes to localhost:1337/#/home but the user isn't redirected. Now the weird part is, when accessing the home page afterwards through the navbar and trying to access the admin page again, the user IS redirected to the home page as intended (although there's an annoying 'flash' while the page is reloaded).
For other people asking, this kind of handling has worked, and I'm wondering what I may have missed and generally any reasons for why my current solution isn't working.
You are returning promise from success function, this will never work.
You should return defered.promise (promise object) from outside success function.
CODE
$stateProvider
.state('home', {
url: "/home",
templateUrl: "home/homeTemplate.html",
controller: 'homeController'
})
.state('adminPage', {
url: "/adminPage",
templateUrl: "adminPage/adminTemplate.html",
controller: 'adminPageController',
resolve: {
validate: function($q, $sails, $location) {
var defer = $q.defer();
$sails.get("/user/getCurrentUser")
.success(function(response) {
if (response.user.accessAdminPage) {
defer.resolve();
} else {
defer.reject("Access blocked");
$location.path('/');
}
});
return defer.promise;
}
}
});
Hopefully this could help you, Thanks.
With the solution given by pankajparkar, the issue is that you will have to reply the logic in each state declaration. I recommend you to check the user's authorization in the onStateChangeStart event
angular.module('myApp', ['ui.router'])
.run(function($rootScope, AuthService){
$rootScope.$on('$stateChangeStart',
function(event, next, nextParams, prev, prevParams) {
AuthService.isNotAutorized()
.then(function() {
event.preventDefault();
$state.go('defaultState');
});
});
});
Context
Users can register with a unique URL slug that identifies their page, e.g. 'http://example.com/slug'.
Current State
In my Express.js file, I successfully check my database to see if the slug exists on a user, then redirect the user from 'http://example.com/slug' to 'http://example.com/#!/slug' to take advantage of Angular's routing.
With Angular, however, I can't use $http or $location services in my router file (since it's taking place inside module.config...see this Stack Overflow explanation for more details).
Desire
Basically what I want to do is route the user to a 'default' view when a valid slug is found, or home if it's not. Any suggestions would be much appreciated.
For reference, my module.config code can be found here (note that the 'default' state I want to use is 'search'):
core.client.routes.js
'use strict';
// Setting up route
angular.module('core').config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
// Redirect to home when route not found.
$urlRouterProvider.otherwise('/');
// Home state routing
$stateProvider.
state('home', {
url: '/',
templateUrl: 'modules/core/views/home.client.view.html'
}).
state('search', {
url: '/search',
templateUrl: 'modules/core/views/search.client.view.html'
});
}
]);
What I would like to do, is something like this...
'use strict';
// Setting up route
angular.module('core').config(['$stateProvider', '$urlRouterProvider', '$http', '$location',
function($stateProvider, $urlRouterProvider, $http, $location) {
// Get current slug, assign to json.
var slug = $location.path();
var data = {
link: slug
};
// Check db for slug
$http.post('/my/post/route', data).success( function(response) {
// Found slug in db
}).error( function(response) {
// Route to home
$location.path('/');
});
// Home state routing
$stateProvider.
state('home', {
url: '/',
templateUrl: 'modules/core/views/home.client.view.html'
}).
state('search', {
// Set URL to slug
url: '/' + slug,
templateUrl: 'modules/core/views/search.client.view.html'
});
}
]);
To directly answer your question, what you want to do is use the routes "resolve" to check for the dependency and redirect to the appropriate view:
angular.module('app', ['ui.router','ngMockE2E'])
.run(function ($httpBackend) {
$httpBackend.whenGET(/api\/slugs\/.*/).respond(function (method, url) {
return url.match(/good$/) ? [200,{name: 'john doe'}] : [404,''];
});
})
.config(function ($stateProvider) {
$stateProvider
.state(
'search',
{
url: '/search?terms=:slug',
template: '<h1>Search: {{vm.terms}}</h1>',
controllerAs: 'vm',
controller: function ($stateParams) {
this.terms = $stateParams.slug;
}
}
)
.state(
'slug',
{
url: '/:slug',
template: '<h1>Slug: {{vm.user.name}}</h1>',
controllerAs: 'vm',
controller: function (user) {
this.user = user
},
resolve: {
user: function ($q, $http, $stateParams, $state) {
var defer = $q.defer();
$http.get('http://somewhere.com/api/slugs/' + $stateParams.slug)
.success(function (user) {
defer.resolve(user);
})
.error(function () {
defer.reject();
$state.go('search', {slug: $stateParams.slug});
});
return defer.promise;
}
}
}
);
});
<div ng-app="app">
<script data-require="angular.js#*" data-semver="1.3.6" src="https://code.angularjs.org/1.3.6/angular.js"></script>
<script data-require="ui-router#*" data-semver="0.2.13" src="//rawgit.com/angular-ui/ui-router/0.2.13/release/angular-ui-router.js"></script>
<script data-require="angular-mocks#*" data-semver="1.3.5" src="https://code.angularjs.org/1.3.5/angular-mocks.js"></script>
<a ui-sref="slug({slug: 'good'})">Matched Route</a>
<a ui-sref="slug({slug: 'bad'})">Redirect Route</a>
<div ui-view></div>
</div>
But, there are a few things you may want to revisit in your example:
Is there a need to perform this check client side if you are already validating and redirecting server side via express?
You seem to be overloading the / route a bit, if home fails, it redirects to itself
You are grabbing slug from $location on app init, not when the view is routed to which could be post init, you need to grab it when ever you are routing to the view
You may want to consider using a GET request to fetch/read data for this request rather than using a POST which is intended generally for write operations (but thats a different story)
I working on an SPA that works with a REST server on the backend.
My goal is to create an interface, that will be mutual to all of the roles.
for instance:
On a product page, a guest can view the product and the comments, a registered user also has a text box where he can comment.
The administrator can edit both comments and the product it self, and everything is done within the same view at the SPA.
So in fact we have DOM element that should not be 'compiled' for some users, but should be 'compiled' for others.
What I am doing in order to control the access to my application, is resolving a factory that grantees that the use has the sufficient priviledges to access a certain page, this factory also populates the rootScope with his access level.
Then on the compile function of the xLimitAccess directive I check if the access level of the current user is sufficient to view the content within the directive and then remove it.
Problem: there is no way to access the $rootScope from the compile function(because it doesn't exist yet), and if I'll do it in the link function, it is already too late, and the element cannot be removed from the DOM
HTML code example:
<div class="product">...</div>
<div class="manageProduct" x-limit-access x-access-level="admin">...</div>
<div class="commnet" x-limit-access x-access-level="user, admin">...</div>
<div class="commnet" x-limit-access x-access-level="admin">...</div>
Javascript code:
var app = angular.module('home', []);
// var host = 'http://127.0.0.1:8000';
app.config(function($routeProvider){
$routeProvider.
when('/',
{
templateUrl: 'views/home.html',
controller: 'homeCtrl',
resolve: {auth : 'authUser'} //this is a page only for logged in users
}).
when('/login',
{
templateUrl: 'views/login.html',
controller: 'loginCtrl',
resolve: {}
}).
when('/logout',
{
templateUrl: 'views/logout.html',
controller: 'logoutCtrl',
resolve: {auth : 'authUser'} //this is a page only for logged in users
}).
when('/register',
{
templateUrl: 'views/register.html',
controller: 'registerController',
resolve: {}
}).
when('/admin',
{
templateUrl: 'views/admin/home.html',
controller: 'registerController',
resolve: {auth: 'authAdmin'}
}).
otherwise({ redirectTo: '/'});
// $locationProvider.html5Mode(true);
}).
run(function($rootScope, $location, $http, authUser){
$rootScope.$on("$routeChangeError", function(event, current, previous, rejection){
if(rejection.status == 401)
$location.path('/login');
})
$http.get('/users/me', {withCredentials: true}).
success(function(data, status, headers, config){
$rootScope.roles = data.roles;
}).
error(function(data, status, headers, config){
});
});
app.factory('authUser', function($http){
return $http.head('/users/me', {withCredentials: true});
});
app.directive('xLimitAccess', function(){
return{
restrict: 'A',
prioriry: 100000,
scope: {
xAccessLevel: '='
}
compile: function(element,$rootScope){//this is essentially the problem
if(scope.xAccessLevel != $rootScope.roles)
element.children().remove();
elemnet.remove();
}
}
})
Only looking at the specific problem, of not having $rootScope in your directive's compile function: you can inject it into your directive instead of into your compile function as follows: app.directive('xLimitAccess', function ($rootScope) { }. The compile function does not support injection—it gets passed a set of values directly.