added sample app for client-server authentication
This commit is contained in:
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2013 <Author>
|
Copyright (c) 2013 Sahat Yalkabov
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
0
client/css/.gitignore
vendored
Executable file
0
client/css/.gitignore
vendored
Executable file
51
client/css/app.css
Executable file
51
client/css/app.css
Executable 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
0
client/img/.gitignore
vendored
Executable file
71
client/js/app.js
Executable file
71
client/js/app.js
Executable 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
92
client/js/controllers.js
Executable 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
55
client/js/directives.js
Executable 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
0
client/js/filters.js
Executable file
97
client/js/routingConfig.js
Executable file
97
client/js/routingConfig.js
Executable 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
62
client/js/services.js
Executable 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
66
client/views/index.jade
Executable 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
|
||||||
|
strong {{ user.username }}
|
||||||
|
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
0
client/views/partials/.gitignore
vendored
Executable file
2
client/views/partials/404.jade
Executable file
2
client/views/partials/404.jade
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
h1 404
|
||||||
|
p Ain't nothing here
|
20
client/views/partials/admin.jade
Executable file
20
client/views/partials/admin.jade
Executable 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 }}
|
2
client/views/partials/home.jade
Executable file
2
client/views/partials/home.jade
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
h1 Hello
|
||||||
|
p This view is visible to logged in users.
|
42
client/views/partials/login.jade
Executable file
42
client/views/partials/login.jade
Executable 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
|
2
client/views/partials/private.jade
Executable file
2
client/views/partials/private.jade
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
h1 Private view
|
||||||
|
p This view is visible to logged in users
|
30
client/views/partials/register.jade
Executable file
30
client/views/partials/register.jade
Executable 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
|
@ -1,12 +0,0 @@
|
|||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
*/
|
|
||||||
var mongoose = require('mongoose'),
|
|
||||||
_ = require('underscore');
|
|
||||||
|
|
||||||
|
|
||||||
exports.render = function(req, res) {
|
|
||||||
res.render('index', {
|
|
||||||
user: req.user ? JSON.stringify(req.user) : "null"
|
|
||||||
});
|
|
||||||
};
|
|
87
gruntfile.js
87
gruntfile.js
@ -1,87 +0,0 @@
|
|||||||
exports.exports = function(grunt) {
|
|
||||||
// Project Configuration
|
|
||||||
grunt.initConfig({
|
|
||||||
pkg: grunt.file.readJSON('package.json'),
|
|
||||||
watch: {
|
|
||||||
jade: {
|
|
||||||
files: ['app/views/**'],
|
|
||||||
options: {
|
|
||||||
livereload: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
js: {
|
|
||||||
files: ['public/js/**', 'app/**/*.js'],
|
|
||||||
tasks: ['jshint'],
|
|
||||||
options: {
|
|
||||||
livereload: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
html: {
|
|
||||||
files: ['public/views/**'],
|
|
||||||
options: {
|
|
||||||
livereload: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
files: ['public/css/**'],
|
|
||||||
options: {
|
|
||||||
livereload: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
jshint: {
|
|
||||||
all: ['gruntfile.js', 'public/js/**/*.js', 'test/**/*.js', 'app/**/*.js']
|
|
||||||
},
|
|
||||||
nodemon: {
|
|
||||||
dev: {
|
|
||||||
options: {
|
|
||||||
file: 'server.js',
|
|
||||||
args: [],
|
|
||||||
ignoredFiles: ['README.md', 'node_modules/**', '.DS_Store'],
|
|
||||||
watchedExtensions: ['js'],
|
|
||||||
watchedFolders: ['app', 'config'],
|
|
||||||
debug: true,
|
|
||||||
delayTime: 1,
|
|
||||||
env: {
|
|
||||||
PORT: 3000
|
|
||||||
},
|
|
||||||
cwd: __dirname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
concurrent: {
|
|
||||||
tasks: ['nodemon', 'watch'],
|
|
||||||
options: {
|
|
||||||
logConcurrentOutput: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mochaTest: {
|
|
||||||
options: {
|
|
||||||
reporter: 'spec'
|
|
||||||
},
|
|
||||||
src: ['test/**/*.js']
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
test: {
|
|
||||||
NODE_ENV: 'test'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//Load NPM tasks
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-jshint');
|
|
||||||
grunt.loadNpmTasks('grunt-mocha-test');
|
|
||||||
grunt.loadNpmTasks('grunt-nodemon');
|
|
||||||
grunt.loadNpmTasks('grunt-concurrent');
|
|
||||||
grunt.loadNpmTasks('grunt-env');
|
|
||||||
|
|
||||||
//Making grunt default to force in order not to break the project.
|
|
||||||
grunt.option('force', true);
|
|
||||||
|
|
||||||
//Default task(s).
|
|
||||||
grunt.registerTask('default', ['jshint', 'concurrent']);
|
|
||||||
|
|
||||||
//Test task.
|
|
||||||
grunt.registerTask('test', ['env:test', 'mochaTest']);
|
|
||||||
};
|
|
@ -1,16 +1,9 @@
|
|||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
*/
|
|
||||||
var mongoose = require('mongoose'),
|
var mongoose = require('mongoose'),
|
||||||
Schema = mongoose.Schema,
|
Schema = mongoose.Schema,
|
||||||
crypto = require('crypto'),
|
crypto = require('crypto'),
|
||||||
_ = require('underscore'),
|
_ = require('underscore');
|
||||||
authTypes = ['github', 'twitter', 'facebook', 'google'];
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User Schema
|
|
||||||
*/
|
|
||||||
var UserSchema = new Schema({
|
var UserSchema = new Schema({
|
||||||
name: String,
|
name: String,
|
||||||
email: String,
|
email: String,
|
||||||
@ -23,7 +16,6 @@ var UserSchema = new Schema({
|
|||||||
salt: String,
|
salt: String,
|
||||||
facebook: {},
|
facebook: {},
|
||||||
twitter: {},
|
twitter: {},
|
||||||
github: {},
|
|
||||||
google: {}
|
google: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
14
server.js
14
server.js
@ -21,6 +21,20 @@ app.use(app.router);
|
|||||||
app.use(express.static(config.root + '/public'));
|
app.use(express.static(config.root + '/public'));
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Controllers
|
||||||
|
*/
|
||||||
|
var articles = require('./controllers/articles');
|
||||||
|
var users = require('./controllers/users');
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Models
|
||||||
|
*/
|
||||||
|
var Article = require('./models/article');
|
||||||
|
var User = require('./models/user');
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API Routes
|
* API Routes
|
||||||
*/
|
*/
|
||||||
|
46
server/controllers/auth.js
Executable file
46
server/controllers/auth.js
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
var passport = require('passport')
|
||||||
|
, User = require('../models/User.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
register: function(req, res, next) {
|
||||||
|
try {
|
||||||
|
User.validate(req.body);
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
return res.send(400, err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
User.addUser(req.body.username, req.body.password, req.body.role, function(err, user) {
|
||||||
|
if(err === 'UserAlreadyExists') return res.send(403, "User already exists");
|
||||||
|
else if(err) return res.send(500);
|
||||||
|
|
||||||
|
req.logIn(user, function(err) {
|
||||||
|
if(err) { next(err); }
|
||||||
|
else { res.json(200, { "role": user.role, "username": user.username }); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
login: function(req, res, next) {
|
||||||
|
passport.authenticate('local', function(err, user) {
|
||||||
|
|
||||||
|
if(err) { return next(err); }
|
||||||
|
if(!user) { return res.send(400); }
|
||||||
|
|
||||||
|
|
||||||
|
req.logIn(user, function(err) {
|
||||||
|
if(err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(req.body.rememberme) req.session.cookie.maxAge = 1000 * 60 * 60 * 24 * 7;
|
||||||
|
res.json(200, { "role": user.role, "username": user.username });
|
||||||
|
});
|
||||||
|
})(req, res, next);
|
||||||
|
},
|
||||||
|
|
||||||
|
logout: function(req, res) {
|
||||||
|
req.logout();
|
||||||
|
res.send(200);
|
||||||
|
}
|
||||||
|
};
|
17
server/controllers/user.js
Executable file
17
server/controllers/user.js
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
var _ = require('underscore')
|
||||||
|
, User = require('../models/User.js')
|
||||||
|
, userRoles = require('../../client/js/routingConfig').userRoles;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
index: function(req, res) {
|
||||||
|
var users = User.findAll();
|
||||||
|
_.each(users, function(user) {
|
||||||
|
delete user.password;
|
||||||
|
delete user.twitter;
|
||||||
|
delete user.facebook;
|
||||||
|
delete user.google;
|
||||||
|
delete user.linkedin;
|
||||||
|
});
|
||||||
|
res.json(users);
|
||||||
|
}
|
||||||
|
};
|
175
server/models/User.js
Executable file
175
server/models/User.js
Executable file
@ -0,0 +1,175 @@
|
|||||||
|
var User
|
||||||
|
, _ = require('underscore')
|
||||||
|
, passport = require('passport')
|
||||||
|
, LocalStrategy = require('passport-local').Strategy
|
||||||
|
, TwitterStrategy = require('passport-twitter').Strategy
|
||||||
|
, FacebookStrategy = require('passport-facebook').Strategy
|
||||||
|
, GoogleStrategy = require('passport-google').Strategy
|
||||||
|
, LinkedInStrategy = require('passport-linkedin').Strategy
|
||||||
|
, check = require('validator').check
|
||||||
|
, userRoles = require('../../client/js/routingConfig').userRoles;
|
||||||
|
|
||||||
|
var users = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
username: "user",
|
||||||
|
password: "123",
|
||||||
|
role: userRoles.user
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
username: "admin",
|
||||||
|
password: "123",
|
||||||
|
role: userRoles.admin
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
addUser: function(username, password, role, callback) {
|
||||||
|
if(this.findByUsername(username) !== undefined) return callback("UserAlreadyExists");
|
||||||
|
|
||||||
|
// Clean up when 500 users reached
|
||||||
|
if(users.length > 500) {
|
||||||
|
users = users.slice(0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = {
|
||||||
|
id: _.max(users, function(user) { return user.id; }).id + 1,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
role: role
|
||||||
|
};
|
||||||
|
users.push(user);
|
||||||
|
callback(null, user);
|
||||||
|
},
|
||||||
|
|
||||||
|
findOrCreateOauthUser: function(provider, providerId) {
|
||||||
|
var user = module.exports.findByProviderId(provider, providerId);
|
||||||
|
if(!user) {
|
||||||
|
user = {
|
||||||
|
id: _.max(users, function(user) { return user.id; }).id + 1,
|
||||||
|
username: provider + '_user', // Should keep Oauth users anonymous on demo site
|
||||||
|
role: userRoles.user,
|
||||||
|
provider: provider
|
||||||
|
};
|
||||||
|
user[provider] = providerId;
|
||||||
|
users.push(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
|
||||||
|
findAll: function() {
|
||||||
|
return _.map(users, function(user) { return _.clone(user); });
|
||||||
|
},
|
||||||
|
|
||||||
|
findById: function(id) {
|
||||||
|
return _.clone(_.find(users, function(user) { return user.id === id }));
|
||||||
|
},
|
||||||
|
|
||||||
|
findByUsername: function(username) {
|
||||||
|
return _.clone(_.find(users, function(user) { return user.username === username; }));
|
||||||
|
},
|
||||||
|
|
||||||
|
findByProviderId: function(provider, id) {
|
||||||
|
return _.find(users, function(user) { return user[provider] === id; });
|
||||||
|
},
|
||||||
|
|
||||||
|
validate: function(user) {
|
||||||
|
check(user.username, 'Username must be 1-20 characters long').len(1, 20);
|
||||||
|
check(user.password, 'Password must be 5-60 characters long').len(5, 60);
|
||||||
|
check(user.username, 'Invalid username').not(/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/);
|
||||||
|
|
||||||
|
// TODO: Seems node-validator's isIn function doesn't handle Number arrays very well...
|
||||||
|
// Till this is rectified Number arrays must be converted to string arrays
|
||||||
|
// https://github.com/chriso/node-validator/issues/185
|
||||||
|
var stringArr = _.map(_.values(userRoles), function(val) { return val.toString() });
|
||||||
|
check(user.role, 'Invalid user role given').isIn(stringArr);
|
||||||
|
},
|
||||||
|
|
||||||
|
localStrategy: new LocalStrategy(
|
||||||
|
function(username, password, done) {
|
||||||
|
|
||||||
|
var user = module.exports.findByUsername(username);
|
||||||
|
|
||||||
|
if(!user) {
|
||||||
|
done(null, false, { message: 'Incorrect username.' });
|
||||||
|
}
|
||||||
|
else if(user.password != password) {
|
||||||
|
done(null, false, { message: 'Incorrect username.' });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return done(null, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
twitterStrategy: function() {
|
||||||
|
if(!process.env.TWITTER_CONSUMER_KEY) throw new Error('A Twitter Consumer Key is required if you want to enable login via Twitter.');
|
||||||
|
if(!process.env.TWITTER_CONSUMER_SECRET) throw new Error('A Twitter Consumer Secret is required if you want to enable login via Twitter.');
|
||||||
|
|
||||||
|
return new TwitterStrategy({
|
||||||
|
consumerKey: process.env.TWITTER_CONSUMER_KEY,
|
||||||
|
consumerSecret: process.env.TWITTER_CONSUMER_SECRET,
|
||||||
|
callbackURL: process.env.TWITTER_CALLBACK_URL || 'http://localhost:8000/auth/twitter/callback'
|
||||||
|
},
|
||||||
|
function(token, tokenSecret, profile, done) {
|
||||||
|
var user = module.exports.findOrCreateOauthUser(profile.provider, profile.id);
|
||||||
|
done(null, user);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
facebookStrategy: function() {
|
||||||
|
if(!process.env.FACEBOOK_APP_ID) throw new Error('A Facebook App ID is required if you want to enable login via Facebook.');
|
||||||
|
if(!process.env.FACEBOOK_APP_SECRET) throw new Error('A Facebook App Secret is required if you want to enable login via Facebook.');
|
||||||
|
|
||||||
|
return new FacebookStrategy({
|
||||||
|
clientID: process.env.FACEBOOK_APP_ID,
|
||||||
|
clientSecret: process.env.FACEBOOK_APP_SECRET,
|
||||||
|
callbackURL: process.env.FACEBOOK_CALLBACK_URL || "http://localhost:8000/auth/facebook/callback"
|
||||||
|
},
|
||||||
|
function(accessToken, refreshToken, profile, done) {
|
||||||
|
var user = module.exports.findOrCreateOauthUser(profile.provider, profile.id);
|
||||||
|
done(null, user);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
googleStrategy: function() {
|
||||||
|
|
||||||
|
return new GoogleStrategy({
|
||||||
|
returnURL: process.env.GOOGLE_RETURN_URL || "http://localhost:8000/auth/google/return",
|
||||||
|
realm: process.env.GOOGLE_REALM || "http://localhost:8000/"
|
||||||
|
},
|
||||||
|
function(identifier, profile, done) {
|
||||||
|
var user = module.exports.findOrCreateOauthUser('google', identifier);
|
||||||
|
done(null, user);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
linkedInStrategy: function() {
|
||||||
|
if(!process.env.LINKED_IN_KEY) throw new Error('A LinkedIn App Key is required if you want to enable login via LinkedIn.');
|
||||||
|
if(!process.env.LINKED_IN_SECRET) throw new Error('A LinkedIn App Secret is required if you want to enable login via LinkedIn.');
|
||||||
|
|
||||||
|
return new LinkedInStrategy({
|
||||||
|
consumerKey: process.env.LINKED_IN_KEY,
|
||||||
|
consumerSecret: process.env.LINKED_IN_SECRET,
|
||||||
|
callbackURL: process.env.LINKED_IN_CALLBACK_URL || "http://localhost:8000/auth/linkedin/callback"
|
||||||
|
},
|
||||||
|
function(token, tokenSecret, profile, done) {
|
||||||
|
var user = module.exports.findOrCreateOauthUser('linkedin', profile.id);
|
||||||
|
done(null,user);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
serializeUser: function(user, done) {
|
||||||
|
done(null, user.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
deserializeUser: function(id, done) {
|
||||||
|
var user = module.exports.findById(id);
|
||||||
|
|
||||||
|
if(user) { done(null, user); }
|
||||||
|
else { done(null, false); }
|
||||||
|
}
|
||||||
|
};
|
155
server/routes.js
Executable file
155
server/routes.js
Executable file
@ -0,0 +1,155 @@
|
|||||||
|
var _ = require('underscore')
|
||||||
|
, path = require('path')
|
||||||
|
, passport = require('passport')
|
||||||
|
, AuthCtrl = require('./controllers/auth')
|
||||||
|
, UserCtrl = require('./controllers/user')
|
||||||
|
, User = require('./models/User.js')
|
||||||
|
, userRoles = require('../client/js/routingConfig').userRoles
|
||||||
|
, accessLevels = require('../client/js/routingConfig').accessLevels;
|
||||||
|
|
||||||
|
var routes = [
|
||||||
|
|
||||||
|
// Views
|
||||||
|
{
|
||||||
|
path: '/partials/*',
|
||||||
|
httpMethod: 'GET',
|
||||||
|
middleware: [function (req, res) {
|
||||||
|
var requestedView = path.join('./', req.url);
|
||||||
|
res.render(requestedView);
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
|
||||||
|
// OAUTH
|
||||||
|
{
|
||||||
|
path: '/auth/twitter',
|
||||||
|
httpMethod: 'GET',
|
||||||
|
middleware: [passport.authenticate('twitter')]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/auth/twitter/callback',
|
||||||
|
httpMethod: 'GET',
|
||||||
|
middleware: [passport.authenticate('twitter', {
|
||||||
|
successRedirect: '/',
|
||||||
|
failureRedirect: '/login'
|
||||||
|
})]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/auth/facebook',
|
||||||
|
httpMethod: 'GET',
|
||||||
|
middleware: [passport.authenticate('facebook')]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/auth/facebook/callback',
|
||||||
|
httpMethod: 'GET',
|
||||||
|
middleware: [passport.authenticate('facebook', {
|
||||||
|
successRedirect: '/',
|
||||||
|
failureRedirect: '/login'
|
||||||
|
})]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/auth/google',
|
||||||
|
httpMethod: 'GET',
|
||||||
|
middleware: [passport.authenticate('google')]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/auth/google/return',
|
||||||
|
httpMethod: 'GET',
|
||||||
|
middleware: [passport.authenticate('google', {
|
||||||
|
successRedirect: '/',
|
||||||
|
failureRedirect: '/login'
|
||||||
|
})]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/auth/linkedin',
|
||||||
|
httpMethod: 'GET',
|
||||||
|
middleware: [passport.authenticate('linkedin')]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/auth/linkedin/callback',
|
||||||
|
httpMethod: 'GET',
|
||||||
|
middleware: [passport.authenticate('linkedin', {
|
||||||
|
successRedirect: '/',
|
||||||
|
failureRedirect: '/login'
|
||||||
|
})]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Local Auth
|
||||||
|
{
|
||||||
|
path: '/register',
|
||||||
|
httpMethod: 'POST',
|
||||||
|
middleware: [AuthCtrl.register]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
httpMethod: 'POST',
|
||||||
|
middleware: [AuthCtrl.login]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/logout',
|
||||||
|
httpMethod: 'POST',
|
||||||
|
middleware: [AuthCtrl.logout]
|
||||||
|
},
|
||||||
|
|
||||||
|
// User resource
|
||||||
|
{
|
||||||
|
path: '/users',
|
||||||
|
httpMethod: 'GET',
|
||||||
|
middleware: [UserCtrl.index],
|
||||||
|
accessLevel: accessLevels.admin
|
||||||
|
},
|
||||||
|
|
||||||
|
// All other get requests should be handled by AngularJS's client-side routing system
|
||||||
|
{
|
||||||
|
path: '/*',
|
||||||
|
httpMethod: 'GET',
|
||||||
|
middleware: [function(req, res) {
|
||||||
|
var role = userRoles.public, username = '';
|
||||||
|
if(req.user) {
|
||||||
|
role = req.user.role;
|
||||||
|
username = req.user.username;
|
||||||
|
}
|
||||||
|
res.cookie('user', JSON.stringify({
|
||||||
|
'username': username,
|
||||||
|
'role': role
|
||||||
|
}));
|
||||||
|
res.render('index');
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = function(app) {
|
||||||
|
|
||||||
|
_.each(routes, function(route) {
|
||||||
|
route.middleware.unshift(ensureAuthorized);
|
||||||
|
var args = _.flatten([route.path, route.middleware]);
|
||||||
|
|
||||||
|
switch(route.httpMethod.toUpperCase()) {
|
||||||
|
case 'GET':
|
||||||
|
app.get.apply(app, args);
|
||||||
|
break;
|
||||||
|
case 'POST':
|
||||||
|
app.post.apply(app, args);
|
||||||
|
break;
|
||||||
|
case 'PUT':
|
||||||
|
app.put.apply(app, args);
|
||||||
|
break;
|
||||||
|
case 'DELETE':
|
||||||
|
app.delete.apply(app, args);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid HTTP method specified for route ' + route.path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureAuthorized(req, res, next) {
|
||||||
|
var role;
|
||||||
|
if(!req.user) role = userRoles.public;
|
||||||
|
else role = req.user.role;
|
||||||
|
|
||||||
|
var accessLevel = _.findWhere(routes, { path: req.route.path }).accessLevel || accessLevels.public;
|
||||||
|
|
||||||
|
if(!(accessLevel.bitMask & role.bitMask)) return res.send(403);
|
||||||
|
return next();
|
||||||
|
}
|
57
server/tests/integration/index.spec.js
Executable file
57
server/tests/integration/index.spec.js
Executable file
@ -0,0 +1,57 @@
|
|||||||
|
var app = require('../../../server'),
|
||||||
|
request = require('supertest'),
|
||||||
|
passportStub = require('passport-stub');
|
||||||
|
passportStub.install(app);
|
||||||
|
|
||||||
|
// user account
|
||||||
|
var user = {
|
||||||
|
'username':'newUser',
|
||||||
|
'role':{bitMask: 2,title: "user"},
|
||||||
|
'password':'12345'
|
||||||
|
};
|
||||||
|
|
||||||
|
// user account 2 - no role
|
||||||
|
var user2 = {
|
||||||
|
'username':'newUser',
|
||||||
|
'password':'12345'
|
||||||
|
};
|
||||||
|
|
||||||
|
// admin account
|
||||||
|
var admin = {
|
||||||
|
'username':'admin',
|
||||||
|
'role': { bitMask: 4, title: 'admin' },
|
||||||
|
'id': '2',
|
||||||
|
'password':'123'
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Server Integration Tests - ', function (done) {
|
||||||
|
afterEach(function() {
|
||||||
|
passportStub.logout(); // logout after each test
|
||||||
|
});
|
||||||
|
it('Homepage - Return a 200', function(done) {
|
||||||
|
request(app).get('/').expect(200, done);
|
||||||
|
});
|
||||||
|
it('Logout - Return a 200', function(done) {
|
||||||
|
request(app).post('/logout').expect(200, done);
|
||||||
|
});
|
||||||
|
it('As a Logout user, on /users - Return a 403', function(done) {
|
||||||
|
request(app).get('/users').expect(403, done);
|
||||||
|
});
|
||||||
|
it('Register a new user(no role) - Return a 400', function(done) {
|
||||||
|
request(app).post('/register').send(user2).expect(400, done);
|
||||||
|
});
|
||||||
|
it('Register a new user - Return a 200', function(done) {
|
||||||
|
request(app).post('/register').send(user).expect(200, done);
|
||||||
|
});
|
||||||
|
it('As a normal user, on /users - Return a 403', function(done) {
|
||||||
|
passportStub.login(user); // login as user
|
||||||
|
request(app).get('/users').expect(403, done);
|
||||||
|
});
|
||||||
|
it('Login as Admin - Return a 200', function(done) {
|
||||||
|
request(app).post('/login').send(admin).expect(200, done);
|
||||||
|
});
|
||||||
|
it('As a Admin user, on /users - Return a 200', function(done) {
|
||||||
|
passportStub.login(admin); // login as admin
|
||||||
|
request(app).get('/users').expect(200, done);
|
||||||
|
});
|
||||||
|
});
|
102
server/tests/unit/controllers/auth.spec.js
Executable file
102
server/tests/unit/controllers/auth.spec.js
Executable file
@ -0,0 +1,102 @@
|
|||||||
|
var expect = require('chai').expect
|
||||||
|
, sinon = require('sinon')
|
||||||
|
, AuthCtrl = require('../../../controllers/auth')
|
||||||
|
, User = require('../../../models/User');
|
||||||
|
|
||||||
|
describe('Auth controller Unit Tests - ', function() {
|
||||||
|
|
||||||
|
var req = { }
|
||||||
|
, res = {}
|
||||||
|
, next = {}
|
||||||
|
, sandbox = sinon.sandbox.create();
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('register()', function() {
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
req.body = {
|
||||||
|
username: "user",
|
||||||
|
password: "pass",
|
||||||
|
role: 1
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a 400 when user validation fails', function(done) {
|
||||||
|
|
||||||
|
var userValidateStub = sandbox.stub(User, 'validate').throws();
|
||||||
|
res.send = function(httpStatus) {
|
||||||
|
expect(httpStatus).to.equal(400);
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
|
||||||
|
AuthCtrl.register(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a 403 when UserAlreadyExists error is returned from User.addUser()', function(done) {
|
||||||
|
var userValidateStub = sandbox.stub(User, 'validate').returns();
|
||||||
|
var userAddUserStub = sandbox.stub(User, 'addUser', function(username, password, role, callback) {
|
||||||
|
callback('UserAlreadyExists');
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send = function(httpStatus) {
|
||||||
|
expect(httpStatus).to.equal(403);
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
|
||||||
|
AuthCtrl.register(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a 500 if error other than UserAlreadyExists is returned from User.addUser()', function(done) {
|
||||||
|
var userValidateStub = sandbox.stub(User, 'validate').returns();
|
||||||
|
var userAddUserStub = sandbox.stub(User, 'addUser', function(username, password, role, callback) {
|
||||||
|
callback('SomeError');
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send = function(httpStatus) {
|
||||||
|
expect(httpStatus).to.equal(500);
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
|
||||||
|
AuthCtrl.register(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call next() with an error argument if req.logIn() returns error', function(done) {
|
||||||
|
var userValidateStub = sandbox.stub(User, 'validate').returns();
|
||||||
|
var userAddUserStub = sandbox.stub(User, 'addUser', function(username, password, role, callback) {
|
||||||
|
callback(null, req.body);
|
||||||
|
});
|
||||||
|
req.logIn = function(user, callback) { return callback('SomeError'); };
|
||||||
|
|
||||||
|
next = function(err) {
|
||||||
|
expect(err).to.exist;
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
|
||||||
|
AuthCtrl.register(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a 200 with a username and role in the response body', function(done) {
|
||||||
|
var userValidateStub = sandbox.stub(User, 'validate').returns();
|
||||||
|
var userAddUserStub = sandbox.stub(User, 'addUser', function(username, password, role, callback) {
|
||||||
|
callback(null, req.body);
|
||||||
|
});
|
||||||
|
req.logIn = function(user, callback) { return callback(null); };
|
||||||
|
|
||||||
|
res.json = function(httpStatus, user) {
|
||||||
|
expect(httpStatus).to.equal(200);
|
||||||
|
expect(user.username).to.exist;
|
||||||
|
expect(user.role).to.exist;
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
|
||||||
|
AuthCtrl.register(req, res, next);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
37
server2.js
Executable file
37
server2.js
Executable file
@ -0,0 +1,37 @@
|
|||||||
|
var express = require('express')
|
||||||
|
, http = require('http')
|
||||||
|
, passport = require('passport')
|
||||||
|
, path = require('path')
|
||||||
|
, User = require('./server/models/User.js');
|
||||||
|
|
||||||
|
var app = module.exports = express();
|
||||||
|
|
||||||
|
app.set('views', __dirname + '/client/views');
|
||||||
|
app.set('view engine', 'jade');
|
||||||
|
app.use(express.logger('dev'))
|
||||||
|
app.use(express.cookieParser());
|
||||||
|
app.use(express.bodyParser());
|
||||||
|
app.use(express.methodOverride());
|
||||||
|
app.use(express.static(path.join(__dirname, 'client')));
|
||||||
|
app.use(express.cookieSession(
|
||||||
|
{
|
||||||
|
secret: process.env.COOKIE_SECRET || "Superdupersecret"
|
||||||
|
}));
|
||||||
|
app.use(passport.initialize());
|
||||||
|
app.use(passport.session());
|
||||||
|
|
||||||
|
passport.use(User.localStrategy);
|
||||||
|
passport.use(User.twitterStrategy()); // Comment out this line if you don't want to enable login via Twitter
|
||||||
|
passport.use(User.facebookStrategy()); // Comment out this line if you don't want to enable login via Facebook
|
||||||
|
passport.use(User.googleStrategy()); // Comment out this line if you don't want to enable login via Google
|
||||||
|
passport.use(User.linkedInStrategy()); // Comment out this line if you don't want to enable login via LinkedIn
|
||||||
|
|
||||||
|
passport.serializeUser(User.serializeUser);
|
||||||
|
passport.deserializeUser(User.deserializeUser);
|
||||||
|
|
||||||
|
require('./server/routes.js')(app);
|
||||||
|
|
||||||
|
app.set('port', process.env.PORT || 8000);
|
||||||
|
http.createServer(app).listen(app.get('port'), function(){
|
||||||
|
console.log("Express server listening on port " + app.get('port'));
|
||||||
|
});
|
Reference in New Issue
Block a user