added sample app for client-server authentication

This commit is contained in:
Sahat Yalkabov
2013-11-13 17:16:06 -05:00
parent bf83154066
commit fc5563c70c
29 changed files with 1199 additions and 111 deletions

0
client/css/.gitignore vendored Executable file
View File

51
client/css/app.css Executable file
View File

@@ -0,0 +1,51 @@
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
display: none;
}
body {
font-size: 1em;
}
#cover {
position:absolute;
height:100%;
width:100%;
background:white;
}
#userInfo {
float:right;padding:8px;
}
.icon-twitter {
color:#00A7E7;
}
.icon-facebook-sign {
color:#3662A0;
}
.icon-google-plus-sign {
color: #D74634;
}
.fade-hide-setup, .fade-show-setup {
-webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
-moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
-o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
}
.fade-hide-setup {
opacity:1;
}
.fade-hide-setup.fade-hide-start {
opacity:0;
}
.fade-show-setup {
opacity:0;
}
.fade-show-setup.fade-show-start {
opacity:1;
}

0
client/img/.gitignore vendored Executable file
View File

71
client/js/app.js Executable file
View File

@@ -0,0 +1,71 @@
angular.module('myapp', ['ngCookies', 'ngRoute'])
.config(['$routeProvider', '$locationProvider', '$httpProvider', function ($routeProvider, $locationProvider, $httpProvider) {
var access = routingConfig.accessLevels;
$routeProvider.when('/', {
templateUrl: 'home',
controller: 'HomeCtrl',
access: access.user
});
$routeProvider.when('/login', {
templateUrl: 'login',
controller: 'LoginCtrl',
access: access.anon
});
$routeProvider.when('/register', {
templateUrl: 'register',
controller: 'RegisterCtrl',
access: access.anon
});
$routeProvider.when('/private', {
templateUrl: 'private',
controller: 'PrivateCtrl',
access: access.user
});
$routeProvider.when('/admin', {
templateUrl: 'admin',
controller: 'AdminCtrl',
access: access.admin
});
$routeProvider.when('/404', {
templateUrl: '404',
access: access.public
});
$routeProvider.otherwise({
redirectTo:'/404'
});
$locationProvider.html5Mode(true);
var interceptor = ['$location', '$q', function($location, $q) {
function success(response) {
return response;
}
function error(response) {
if(response.status === 401) {
$location.path('/login');
return $q.reject(response);
}
else {
return $q.reject(response);
}
}
return function(promise) {
return promise.then(success, error);
}
}];
$httpProvider.responseInterceptors.push(interceptor);
}])
.run(['$rootScope', '$location', 'Auth', function ($rootScope, $location, Auth) {
$rootScope.$on("$routeChangeStart", function (event, next, current) {
$rootScope.error = null;
if (!Auth.authorize(next.access)) {
if(Auth.isLoggedIn()) $location.path('/');
else $location.path('/login');
}
});
}]);

92
client/js/controllers.js Executable file
View File

@@ -0,0 +1,92 @@
'use strict';
/* Controllers */
angular.module('angular-client-side-auth')
.controller('NavCtrl', ['$rootScope', '$scope', '$location', 'Auth', function($rootScope, $scope, $location, Auth) {
$scope.user = Auth.user;
$scope.userRoles = Auth.userRoles;
$scope.accessLevels = Auth.accessLevels;
$scope.logout = function() {
Auth.logout(function() {
$location.path('/login');
}, function() {
$rootScope.error = "Failed to logout";
});
};
}]);
angular.module('angular-client-side-auth')
.controller('LoginCtrl',
['$rootScope', '$scope', '$location', '$window', 'Auth', function($rootScope, $scope, $location, $window, Auth) {
$scope.rememberme = true;
$scope.login = function() {
Auth.login({
username: $scope.username,
password: $scope.password,
rememberme: $scope.rememberme
},
function(res) {
$location.path('/');
},
function(err) {
$rootScope.error = "Failed to login";
});
};
$scope.loginOauth = function(provider) {
$window.location.href = '/auth/' + provider;
};
}]);
angular.module('angular-client-side-auth')
.controller('HomeCtrl',
['$rootScope', function($rootScope) {
}]);
angular.module('angular-client-side-auth')
.controller('RegisterCtrl',
['$rootScope', '$scope', '$location', 'Auth', function($rootScope, $scope, $location, Auth) {
$scope.role = Auth.userRoles.user;
$scope.userRoles = Auth.userRoles;
$scope.register = function() {
Auth.register({
username: $scope.username,
password: $scope.password,
role: $scope.role
},
function() {
$location.path('/');
},
function(err) {
$rootScope.error = err;
});
};
}]);
angular.module('angular-client-side-auth')
.controller('PrivateCtrl',
['$rootScope', function($rootScope) {
}]);
angular.module('angular-client-side-auth')
.controller('AdminCtrl',
['$rootScope', '$scope', 'Users', 'Auth', function($rootScope, $scope, Users, Auth) {
$scope.loading = true;
$scope.userRoles = Auth.userRoles;
Users.getAll(function(res) {
$scope.users = res;
$scope.loading = false;
}, function(err) {
$rootScope.error = "Failed to fetch users.";
$scope.loading = false;
});
}]);

55
client/js/directives.js Executable file
View File

@@ -0,0 +1,55 @@
'use strict';
angular.module('angular-client-side-auth')
.directive('accessLevel', ['Auth', function(Auth) {
return {
restrict: 'A',
link: function($scope, element, attrs) {
var prevDisp = element.css('display')
, userRole
, accessLevel;
$scope.user = Auth.user;
$scope.$watch('user', function(user) {
if(user.role)
userRole = user.role;
updateCSS();
}, true);
attrs.$observe('accessLevel', function(al) {
if(al) accessLevel = $scope.$eval(al);
updateCSS();
});
function updateCSS() {
if(userRole && accessLevel) {
if(!Auth.authorize(accessLevel, userRole))
element.css('display', 'none');
else
element.css('display', prevDisp);
}
}
}
};
}]);
angular.module('angular-client-side-auth').directive('activeNav', ['$location', function($location) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var nestedA = element.find('a')[0];
var path = nestedA.href;
scope.location = $location;
scope.$watch('location.absUrl()', function(newPath) {
if (path === newPath) {
element.addClass('active');
} else {
element.removeClass('active');
}
});
}
};
}]);

0
client/js/filters.js Executable file
View File

97
client/js/routingConfig.js Executable file
View File

@@ -0,0 +1,97 @@
(function(exports){
var config = {
/* List all the roles you wish to use in the app
* You have a max of 31 before the bit shift pushes the accompanying integer out of
* the memory footprint for an integer
*/
roles :[
'public',
'user',
'admin'],
/*
Build out all the access levels you want referencing the roles listed above
You can use the "*" symbol to represent access to all roles
*/
accessLevels : {
'public' : "*",
'anon': ['public'],
'user' : ['user', 'admin'],
'admin': ['admin']
}
};
exports.userRoles = buildRoles(config.roles);
exports.accessLevels = buildAccessLevels(config.accessLevels, exports.userRoles);
/*
Method to build a distinct bit mask for each role
It starts off with "1" and shifts the bit to the left for each element in the
roles array parameter
*/
function buildRoles(roles){
var bitMask = "01";
var userRoles = {};
for(var role in roles){
var intCode = parseInt(bitMask, 2);
userRoles[roles[role]] = {
bitMask: intCode,
title: roles[role]
};
bitMask = (intCode << 1 ).toString(2)
}
return userRoles;
}
/*
This method builds access level bit masks based on the accessLevelDeclaration parameter which must
contain an array for each access level containing the allowed user roles.
*/
function buildAccessLevels(accessLevelDeclarations, userRoles){
var accessLevels = {};
for(var level in accessLevelDeclarations){
if(typeof accessLevelDeclarations[level] == 'string'){
if(accessLevelDeclarations[level] == '*'){
var resultBitMask = '';
for( var role in userRoles){
resultBitMask += "1"
}
//accessLevels[level] = parseInt(resultBitMask, 2);
accessLevels[level] = {
bitMask: parseInt(resultBitMask, 2),
title: accessLevelDeclarations[level]
};
}
else console.log("Access Control Error: Could not parse '" + accessLevelDeclarations[level] + "' as access definition for level '" + level + "'")
}
else {
var resultBitMask = 0;
for(var role in accessLevelDeclarations[level]){
if(userRoles.hasOwnProperty(accessLevelDeclarations[level][role]))
resultBitMask = resultBitMask | userRoles[accessLevelDeclarations[level][role]].bitMask
else console.log("Access Control Error: Could not find role '" + accessLevelDeclarations[level][role] + "' in registered roles while building access for '" + level + "'")
}
accessLevels[level] = {
bitMask: resultBitMask,
title: accessLevelDeclarations[level][role]
};
}
}
return accessLevels;
}
})(typeof exports === 'undefined' ? this['routingConfig'] = {} : exports);

62
client/js/services.js Executable file
View File

@@ -0,0 +1,62 @@
'use strict';
angular.module('angular-client-side-auth')
.factory('Auth', function($http, $cookieStore){
var accessLevels = routingConfig.accessLevels
, userRoles = routingConfig.userRoles
, currentUser = $cookieStore.get('user') || { username: '', role: userRoles.public };
$cookieStore.remove('user');
function changeUser(user) {
_.extend(currentUser, user);
};
return {
authorize: function(accessLevel, role) {
if(role === undefined)
role = currentUser.role;
return accessLevel.bitMask & role.bitMask;
},
isLoggedIn: function(user) {
if(user === undefined)
user = currentUser;
return user.role.title == userRoles.user.title || user.role.title == userRoles.admin.title;
},
register: function(user, success, error) {
$http.post('/register', user).success(function(res) {
changeUser(res);
success();
}).error(error);
},
login: function(user, success, error) {
$http.post('/login', user).success(function(user){
changeUser(user);
success(user);
}).error(error);
},
logout: function(success, error) {
$http.post('/logout').success(function(){
changeUser({
username: '',
role: userRoles.public
});
success();
}).error(error);
},
accessLevels: accessLevels,
userRoles: userRoles,
user: currentUser
};
});
angular.module('angular-client-side-auth')
.factory('Users', function($http) {
return {
getAll: function(success, error) {
$http.get('/users').success(success).error(error);
}
};
});

66
client/views/index.jade Executable file
View File

@@ -0,0 +1,66 @@
!!! 5
html(lang='en', data-ng-app='angular-client-side-auth')
head
meta(charset='utf-8')
title Angular Auth Example
link(rel='stylesheet', href='css/app.css')
link(href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.no-icons.min.css", rel="stylesheet")
link(href="//netdna.bootstrapcdn.com/font-awesome/3.1.1/css/font-awesome.min.css", rel="stylesheet")
// This is needed because Facebook login redirects add #_=_ at the end of the URL
script(type="text/javascript").
if (window.location.href.indexOf('#_=_') > 0) {
window.location = window.location.href.replace(/#.*/, '');
}
body(data-ng-cloak)
.navbar(data-ng-controller="NavCtrl")
.navbar-inner
.container-fluid
ul.nav
li(data-access-level='accessLevels.anon', active-nav)
a(href='/login') Log in
li(data-access-level='accessLevels.anon', active-nav)
a(href='/register') Register
li(data-access-level='accessLevels.user', active-nav)
a(href='/') Home
li(data-access-level='accessLevels.user', active-nav)
a(href='/private') Private
li(data-access-level='accessLevels.admin', active-nav)
a(href='/admin') Admin
li(data-access-level='accessLevels.user')
a(href="", data-ng-click="logout()")
| Log out
div#userInfo.pull-right(data-access-level='accessLevels.user')
| Welcome&nbsp;
strong {{ user.username }}&nbsp;
span.label(data-ng-class='{"label-info": user.role.title == userRoles.user.title, "label-success": user.role.title == userRoles.admin.title}') {{ user.role.title }}
.container
div(data-ng-view='ng-view')
.alert.alert-error(data-ng-bind="error", data-ng-show="error")
script(src='http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore-min.js')
script(src='https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0/angular.min.js')
script(src='https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0/angular-cookies.min.js')
script(src='https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0/angular-route.min.js')
script(src='/js/routingConfig.js')
script(src='/js/app.js')
script(src='/js/services.js')
script(src='/js/controllers.js')
script(src='/js/filters.js')
script(src='/js/directives.js')
// Partial views... Load up front to make transitions smoother
script(type="text/ng-template", id="404")
include partials/404
script(type="text/ng-template", id="admin")
include partials/admin
script(type="text/ng-template", id="home")
include partials/home
script(type="text/ng-template", id="login")
include partials/login
script(type="text/ng-template", id="private")
include partials/private
script(type="text/ng-template", id="register")
include partials/register

0
client/views/partials/.gitignore vendored Executable file
View File

2
client/views/partials/404.jade Executable file
View File

@@ -0,0 +1,2 @@
h1 404
p Ain't nothing here

View File

@@ -0,0 +1,20 @@
h1 Admin
p This view is visible to users with the administrator role.
table.table.table-striped(data-ng-hide="loading")
thead
tr
th #
th Username
th Role
tbody
tr(data-ng-repeat="user in users")
td {{ user.id }}
td
i.icon-twitter(data-ng-show="user.provider == 'twitter'")
i.icon-facebook-sign(data-ng-show="user.provider == 'facebook'")
i.icon-google-plus-sign(data-ng-show="user.provider == 'google'")
i.icon-linkedin(data-ng-show="user.provider == 'linkedin'")
| {{ user.username }}
td
span.label(data-ng-class='{"label-info": user.role.title == userRoles.user.title, "label-success": user.role.title == userRoles.admin.title}') {{ user.role.title }}

View File

@@ -0,0 +1,2 @@
h1 Hello
p This view is visible to logged in users.

View File

@@ -0,0 +1,42 @@
.hero-unit
h1 Log in
p This site is an example of how one can implement role based authentication in Angular applications as outlined in
a(href="http://www.frederiknakstad.com/authentication-in-single-page-applications-with-angular-js/") this blogpost
| . All the code can be found in
a(href="https://github.com/fnakstad/angular-client-side-auth") this GitHub repository
| . You can either register a new user, log in with one of the two predefined users...
ul
li admin/123
li user/123
form.form-horizontal(ng-submit="login()", name="loginForm")
.control-group
label.control-label(for="username") Username
.controls
input(type="text", data-ng-model="username", placeholder="Username", name="username", required, autofocus)
.control-group
label.control-label(for="password") Password
.controls
input(type="password", data-ng-model="password", placeholder="Password", name="password", required)
.control-group
.controls
label(for="rememberme").checkbox
input(type="checkbox", data-ng-model="rememberme", name="rememberme")
| Remember me
.control-group
.controls
button.btn(type="submit", data-ng-disabled="loginForm.$invalid") Log in
hr
p ... or use one of them fancy social logins:
.btn-group
a.btn(href="", data-ng-click="loginOauth('facebook')")
i.icon-facebook-sign
| Facebook
a.btn(href="", data-ng-click="loginOauth('twitter')")
i.icon-twitter
| Twitter
a.btn(href="", data-ng-click="loginOauth('google')")
i.icon-google-plus-sign
| Google
a.btn(href="", data-ng-click="loginOauth('linkedin')")
i.icon-linkedin
| LinkedIn

View File

@@ -0,0 +1,2 @@
h1 Private view
p This view is visible to logged in users

View File

@@ -0,0 +1,30 @@
h1 Register
form.form-horizontal(ng-submit="register()", name="registerForm")
.control-group
label.control-label(for="username") Username
.controls
input(type="text", data-ng-model="username", placeholder="Username", name="username", required, data-ng-minlength="1", data-ng-maxlength="20", autofocus)
.control-group
label.control-label(for="password") Password
.controls
input(type="password", data-ng-model="password", placeholder="Password", name="password", required, data-ng-minlength="5", data-ng-maxlength="60")
.control-group
label.radio.inline
input(type="radio", name="role", data-ng-model="role", id="adminRole", data-ng-value="userRoles.admin")
| Administrator
label.radio.inline
input(type="radio", name="role", data-ng-model="role", id="adminRole", data-ng-value="userRoles.user")
| Normal user
.control-group
.controls
button.btn(type="submit", data-ng-disabled="registerForm.$invalid") Submit
.alert.alert-error(ng-show="registerForm.$invalid && registerForm.$dirty")
strong Please correct the following errors:
ul
li(ng-show="registerForm.username.$error.required") Username is required
li(ng-show="registerForm.username.$error.minlength") Username has to be at least 1 character long
li(ng-show="registerForm.username.$error.maxlength") Username has to be at most 20 character long
li(ng-show="registerForm.password.$error.required") Password is required
li(ng-show="registerForm.password.$error.minlength") Password must be at least 5 characters long
li(ng-show="registerForm.password.$error.maxlength") Password must be at most 60 characters long