diff --git a/app/generator.js b/app/generator.js index 907df7234..00fad1ad5 100644 --- a/app/generator.js +++ b/app/generator.js @@ -380,7 +380,10 @@ export default class Generator extends Base { if(this.filters.socketio) angModules.push("'btford.socket-io'"); if(this.filters.uirouter) angModules.push("'ui.router'"); if(this.filters.uibootstrap) angModules.push("'ui.bootstrap'"); - if(this.filters.auth) angModules.push("'validation.match'"); + if(this.filters.auth) { + angModules.unshift(`'${this.scriptAppName}.auth'`); + angModules.push("'validation.match'"); + } this.angularModules = '\n ' + angModules.join(',\n ') +'\n'; } diff --git a/app/templates/Gruntfile.js b/app/templates/Gruntfile.js index 1a8bd012f..cc3d414e0 100644 --- a/app/templates/Gruntfile.js +++ b/app/templates/Gruntfile.js @@ -658,6 +658,13 @@ module.exports = function (grunt) { filePath = filePath.replace('/.tmp/', ''); return ''; }, + sort: function(a, b) { + var module = /\.module\.js$/; + var aMod = module.test(a); + var bMod = module.test(b); + // inject *.module.js first + return (aMod === bMod) ? 0 : (aMod ? -1 : 1); + }, starttag: '', endtag: '' }, diff --git a/app/templates/client/app/admin(auth)/admin(js).js b/app/templates/client/app/admin(auth)/admin(js).js index f37ba9fcc..510363868 100644 --- a/app/templates/client/app/admin(auth)/admin(js).js +++ b/app/templates/client/app/admin(auth)/admin(js).js @@ -5,13 +5,15 @@ angular.module('<%= scriptAppName %>') $routeProvider .when('/admin', { templateUrl: 'app/admin/admin.html', - controller: 'AdminCtrl' + controller: 'AdminCtrl', + authenticate: 'admin' }); });<% } %><% if (filters.uirouter) { %>.config(function($stateProvider) { $stateProvider .state('admin', { url: '/admin', templateUrl: 'app/admin/admin.html', - controller: 'AdminCtrl' + controller: 'AdminCtrl', + authenticate: 'admin' }); });<% } %> diff --git a/app/templates/client/app/app(js).js b/app/templates/client/app/app(js).js index 0c8ec39e8..a9d9f2b09 100644 --- a/app/templates/client/app/app(js).js +++ b/app/templates/client/app/app(js).js @@ -1,59 +1,13 @@ 'use strict'; angular.module('<%= scriptAppName %>', [<%- angularModules %>]) - <% if (filters.ngroute) { %>.config(function($routeProvider, $locationProvider<% if (filters.auth) { %>, $httpProvider<% } %>) { + .config(function(<% if (filters.ngroute) { %>$routeProvider<% } if (filters.uirouter) { %>$urlRouterProvider<% } %>, $locationProvider) {<% if (filters.ngroute) { %> $routeProvider .otherwise({ redirectTo: '/' - }); - - $locationProvider.html5Mode(true);<% if (filters.auth) { %> - $httpProvider.interceptors.push('authInterceptor');<% } %> - })<% } if (filters.uirouter) { %>.config(function($stateProvider, $urlRouterProvider, $locationProvider<% if (filters.auth) { %>, $httpProvider<% } %>) { + });<% } if (filters.uirouter) { %> $urlRouterProvider - .otherwise('/'); - - $locationProvider.html5Mode(true);<% if (filters.auth) { %> - $httpProvider.interceptors.push('authInterceptor');<% } %> - })<% } if (filters.auth) { %> - - .factory('authInterceptor', function($rootScope, $q, $cookies<% if (filters.ngroute) { %>, $location<% } if (filters.uirouter) { %>, $injector<% } %>) { - <% if (filters.uirouter) { %>var state; - <% } %>return { - // Add authorization token to headers - request: function(config) { - config.headers = config.headers || {}; - if ($cookies.get('token')) { - config.headers.Authorization = 'Bearer ' + $cookies.get('token'); - } - return config; - }, - - // Intercept 401s and redirect you to login - responseError: function(response) { - if (response.status === 401) { - <% if (filters.ngroute) { %>$location.path('/login');<% } if (filters.uirouter) { %>(state || (state = $injector.get('$state'))).go('login');<% } %> - // remove any stale tokens - $cookies.remove('token'); - return $q.reject(response); - } - else { - return $q.reject(response); - } - } - }; - }) + .otherwise('/');<% } %> - .run(function($rootScope<% if (filters.ngroute) { %>, $location<% } if (filters.uirouter) { %>, $state<% } %>, Auth) { - // Redirect to login if route requires auth and the user is not logged in - $rootScope.$on(<% if (filters.ngroute) { %>'$routeChangeStart'<% } %><% if (filters.uirouter) { %>'$stateChangeStart'<% } %>, function(event, next) { - if (next.authenticate) { - Auth.isLoggedIn(function(loggedIn) { - if (!loggedIn) { - event.preventDefault(); - <% if (filters.ngroute) { %>$location.path('/login');<% } if (filters.uirouter) { %>$state.go('login');<% } %> - } - }); - } - }); - })<% } %>; + $locationProvider.html5Mode(true); + }); diff --git a/app/templates/client/app/main/main.controller(js).js b/app/templates/client/app/main/main.controller(js).js index 35fd95951..db9bbd5c3 100644 --- a/app/templates/client/app/main/main.controller(js).js +++ b/app/templates/client/app/main/main.controller(js).js @@ -1,4 +1,5 @@ 'use strict'; + (function() { function MainController($scope, $http<% if (filters.socketio) { %>, socket<% } %>) { @@ -8,8 +9,8 @@ function MainController($scope, $http<% if (filters.socketio) { %>, socket<% } % $http.get('/api/things').then(function(response) { self.awesomeThings = response.data;<% if (filters.socketio) { %> socket.syncUpdates('thing', self.awesomeThings);<% } %> - }); -<% if (filters.models) { %> + });<% if (filters.models) { %> + this.addThing = function() { if (self.newThing === '') { return; @@ -20,7 +21,7 @@ function MainController($scope, $http<% if (filters.socketio) { %>, socket<% } % this.deleteThing = function(thing) { $http.delete('/api/things/' + thing._id); - };<% } %><% if (filters.socketio) { %> + };<% } if (filters.socketio) { %> $scope.$on('$destroy', function() { socket.unsyncUpdates('thing'); diff --git a/app/templates/client/components/auth(auth)/auth.module(js).js b/app/templates/client/components/auth(auth)/auth.module(js).js new file mode 100644 index 000000000..60ce6e321 --- /dev/null +++ b/app/templates/client/components/auth(auth)/auth.module(js).js @@ -0,0 +1,12 @@ +'use strict'; + +angular.module('<%= scriptAppName %>.auth', [ + '<%= scriptAppName %>.constants', + '<%= scriptAppName %>.util', + 'ngCookies'<% if (filters.ngroute) { %>, + 'ngRoute'<% } if (filters.uirouter) { %>, + 'ui.router'<% } %> +]) + .config(function($httpProvider) { + $httpProvider.interceptors.push('authInterceptor'); + }); diff --git a/app/templates/client/components/auth(auth)/auth.service(js).js b/app/templates/client/components/auth(auth)/auth.service(js).js index 2a1fcb480..e8c7c16b1 100644 --- a/app/templates/client/components/auth(auth)/auth.service(js).js +++ b/app/templates/client/components/auth(auth)/auth.service(js).js @@ -1,170 +1,189 @@ 'use strict'; -angular.module('<%= scriptAppName %>') - .factory('Auth', function Auth($http, User, $cookies, $q) { +(function() { + +function AuthService($location, $http, $cookies, $q, appConfig, Util, User) { + var safeCb = Util.safeCb; + var currentUser = {}; + var userRoles = appConfig.userRoles || []; + + if ($cookies.get('token') && $location.path() !== '/logout') { + currentUser = User.get(); + } + + var Auth = { + /** - * Return a callback or noop function + * Authenticate user and save token * - * @param {Function|*} cb - a 'potential' function - * @return {Function} + * @param {Object} user - login info + * @param {Function} callback - optional, function(error, user) + * @return {Promise} */ - var safeCb = function(cb) { - return (angular.isFunction(cb)) ? cb : angular.noop; + login: function(user, callback) { + return $http.post('/auth/local', { + email: user.email, + password: user.password + }) + .then(function(res) { + $cookies.put('token', res.data.token); + currentUser = User.get(); + return currentUser.$promise; + }) + .then(function(user) { + safeCb(callback)(null, user); + return user; + }) + .catch(function(err) { + Auth.logout(); + safeCb(callback)(err.data); + return $q.reject(err.data); + }); }, - currentUser = {}; - - if ($cookies.get('token')) { - currentUser = User.get(); - } + /** + * Delete access token and user info + */ + logout: function() { + $cookies.remove('token'); + currentUser = {}; + }, - return { - - /** - * Authenticate user and save token - * - * @param {Object} user - login info - * @param {Function} callback - optional, function(error, user) - * @return {Promise} - */ - login: function(user, callback) { - return $http.post('/auth/local', { - email: user.email, - password: user.password - }) - .then(function(res) { - $cookies.put('token', res.data.token); + /** + * Create a new user + * + * @param {Object} user - user info + * @param {Function} callback - optional, function(error, user) + * @return {Promise} + */ + createUser: function(user, callback) { + return User.save(user, + function(data) { + $cookies.put('token', data.token); currentUser = User.get(); - return currentUser.$promise; - }) + return safeCb(callback)(null, user); + }, + function(err) { + Auth.logout(); + return safeCb(callback)(err); + }).$promise; + }, + + /** + * Change password + * + * @param {String} oldPassword + * @param {String} newPassword + * @param {Function} callback - optional, function(error, user) + * @return {Promise} + */ + changePassword: function(oldPassword, newPassword, callback) { + return User.changePassword({ id: currentUser._id }, { + oldPassword: oldPassword, + newPassword: newPassword + }, function() { + return safeCb(callback)(null); + }, function(err) { + return safeCb(callback)(err); + }).$promise; + }, + + /** + * Gets all available info on a user + * (synchronous|asynchronous) + * + * @param {Function|*} callback - optional, funciton(user) + * @return {Object|Promise} + */ + getCurrentUser: function(callback) { + if (arguments.length === 0) { + return currentUser; + } + + var value = (currentUser.hasOwnProperty('$promise')) ? + currentUser.$promise : currentUser; + return $q.when(value) .then(function(user) { - safeCb(callback)(null, user); + safeCb(callback)(user); return user; - }) - .catch(function(err) { - this.logout(); - safeCb(callback)(err.data); - return $q.reject(err.data); - }.bind(this)); - }, - - /** - * Delete access token and user info - */ - logout: function() { - $cookies.remove('token'); - currentUser = {}; - }, - - /** - * Create a new user - * - * @param {Object} user - user info - * @param {Function} callback - optional, function(error, user) - * @return {Promise} - */ - createUser: function(user, callback) { - return User.save(user, - function(data) { - $cookies.put('token', data.token); - currentUser = User.get(); - return safeCb(callback)(null, user); - }, - function(err) { - this.logout(); - return safeCb(callback)(err); - }.bind(this)).$promise; - }, - - /** - * Change password - * - * @param {String} oldPassword - * @param {String} newPassword - * @param {Function} callback - optional, function(error, user) - * @return {Promise} - */ - changePassword: function(oldPassword, newPassword, callback) { - return User.changePassword({ id: currentUser._id }, { - oldPassword: oldPassword, - newPassword: newPassword }, function() { - return safeCb(callback)(null); - }, function(err) { - return safeCb(callback)(err); - }).$promise; - }, - - /** - * Gets all available info on a user - * (synchronous|asynchronous) - * - * @param {Function|*} callback - optional, funciton(user) - * @return {Object|Promise} - */ - getCurrentUser: function(callback) { - if (arguments.length === 0) { - return currentUser; - } - - var value = (currentUser.hasOwnProperty('$promise')) ? currentUser.$promise : currentUser; - return $q.when(value) - .then(function(user) { - safeCb(callback)(user); - return user; - }, function() { - safeCb(callback)({}); - return {}; - }); - }, - - /** - * Check if a user is logged in - * (synchronous|asynchronous) - * - * @param {Function|*} callback - optional, function(is) - * @return {Bool|Promise} - */ - isLoggedIn: function(callback) { - if (arguments.length === 0) { - return currentUser.hasOwnProperty('role'); - } - - return this.getCurrentUser(null) - .then(function(user) { - var is = user.hasOwnProperty('role'); - safeCb(callback)(is); - return is; - }); - }, - - /** - * Check if a user is an admin - * (synchronous|asynchronous) - * - * @param {Function|*} callback - optional, function(is) - * @return {Bool|Promise} - */ - isAdmin: function(callback) { - if (arguments.length === 0) { - return currentUser.role === 'admin'; - } - - return this.getCurrentUser(null) - .then(function(user) { - var is = user.role === 'admin'; - safeCb(callback)(is); - return is; - }); - }, - - /** - * Get auth token - * - * @return {String} - a token string used for authenticating - */ - getToken: function() { - return $cookies.get('token'); + safeCb(callback)({}); + return {}; + }); + }, + + /** + * Check if a user is logged in + * (synchronous|asynchronous) + * + * @param {Function|*} callback - optional, function(is) + * @return {Bool|Promise} + */ + isLoggedIn: function(callback) { + if (arguments.length === 0) { + return currentUser.hasOwnProperty('role'); + } + + return Auth.getCurrentUser(null) + .then(function(user) { + var is = user.hasOwnProperty('role'); + safeCb(callback)(is); + return is; + }); + }, + + /** + * Check if a user has a specified role or higher + * (synchronous|asynchronous) + * + * @param {String} role - the role to check against + * @param {Function|*} callback - optional, function(has) + * @return {Bool|Promise} + */ + hasRole: function(role, callback) { + var hasRole = function(r, h) { + return userRoles.indexOf(r) >= userRoles.indexOf(h); + }; + + if (arguments.length < 2) { + return hasRole(currentUser.role, role); } - }; - }); + + return Auth.getCurrentUser(null) + .then(function(user) { + var has = (user.hasOwnProperty('role')) ? + hasRole(user.role, role) : false; + safeCb(callback)(has); + return has; + }); + }, + + /** + * Check if a user is an admin + * (synchronous|asynchronous) + * + * @param {Function|*} callback - optional, function(is) + * @return {Bool|Promise} + */ + isAdmin: function() { + return Auth.hasRole + .apply(Auth, [].concat.apply(['admin'], arguments)); + }, + + /** + * Get auth token + * + * @return {String} - a token string used for authenticating + */ + getToken: function() { + return $cookies.get('token'); + } + }; + + return Auth; +} + +angular.module('<%= scriptAppName %>.auth') + .factory('Auth', AuthService); + +})(); diff --git a/app/templates/client/components/auth(auth)/interceptor.service(js).js b/app/templates/client/components/auth(auth)/interceptor.service(js).js new file mode 100644 index 000000000..5bfb4b174 --- /dev/null +++ b/app/templates/client/components/auth(auth)/interceptor.service(js).js @@ -0,0 +1,32 @@ +'use strict'; + +(function() { + +function authInterceptor($rootScope, $q, $cookies<% if (filters.ngroute) { %>, $location<% } if (filters.uirouter) { %>, $injector<% } %>, Util) { + <% if (filters.uirouter) { %>var state; + <% } %>return { + // Add authorization token to headers + request: function(config) { + config.headers = config.headers || {}; + if ($cookies.get('token') && Util.isSameOrigin(config.url)) { + config.headers.Authorization = 'Bearer ' + $cookies.get('token'); + } + return config; + }, + + // Intercept 401s and redirect you to login + responseError: function(response) { + if (response.status === 401) { + <% if (filters.ngroute) { %>$location.path('/login');<% } if (filters.uirouter) { %>(state || (state = $injector.get('$state'))).go('login');<% } %> + // remove any stale tokens + $cookies.remove('token'); + } + return $q.reject(response); + } + }; +} + +angular.module('<%= scriptAppName %>.auth') + .factory('authInterceptor', authInterceptor); + +})(); diff --git a/app/templates/client/components/auth(auth)/router.decorator(js).js b/app/templates/client/components/auth(auth)/router.decorator(js).js new file mode 100644 index 000000000..4e48f401a --- /dev/null +++ b/app/templates/client/components/auth(auth)/router.decorator(js).js @@ -0,0 +1,41 @@ +'use strict'; + +(function() { + +function routerDecorator(<%= filters.uirouter ? '$stateProvider' : '$provide' %>) { + var authDecorator = function(<%= filters.uirouter ? 'state' : 'route' %>) { + var auth = <%= filters.uirouter ? 'state' : 'route' %>.authenticate; + if (auth) { + <%= filters.uirouter ? 'state' : 'route' %>.resolve = <%= filters.uirouter ? 'state' : 'route' %>.resolve || {}; + <%= filters.uirouter ? 'state' : 'route' %>.resolve.user = function(<%= filters.uirouter ? '$state' : '$location' %>, $q, Auth) { + return Auth.getCurrentUser(true) + .then(function(user) { + if ((typeof auth !== 'string' && user._id) || + (typeof auth === 'string' && Auth.hasRole(auth))) { + return user; + }<% if (filters.ngroute) { %> + $location.path((user._id) ? '/' : '/login');<% } if (filters.uirouter) { %> + $state.go((user._id) ? 'main' : 'login');<% } %> + return $q.reject('not authorized'); + }); + }; + } + };<% if (filters.ngroute) { %> + + $provide.decorator('$route', function($delegate) { + for (var r in $delegate.routes) { + authDecorator($delegate.routes[r]); + } + return $delegate; + });<% } if (filters.uirouter) { %> + + $stateProvider.decorator('authenticate', function(state) { + authDecorator(state); + return state.authenticate; + });<% } %> +} + +angular.module('<%= scriptAppName %>.auth') + .config(routerDecorator); + +})(); diff --git a/app/templates/client/components/auth(auth)/user.service(js).js b/app/templates/client/components/auth(auth)/user.service(js).js index aad887945..f347c72a9 100644 --- a/app/templates/client/components/auth(auth)/user.service(js).js +++ b/app/templates/client/components/auth(auth)/user.service(js).js @@ -1,22 +1,28 @@ 'use strict'; -angular.module('<%= scriptAppName %>') - .factory('User', function ($resource) { - return $resource('/api/users/:id/:controller', { - id: '@_id' +(function() { + +function UserResource($resource) { + return $resource('/api/users/:id/:controller', { + id: '@_id' + }, + { + changePassword: { + method: 'PUT', + params: { + controller:'password' + } }, - { - changePassword: { - method: 'PUT', - params: { - controller:'password' - } - }, - get: { - method: 'GET', - params: { - id:'me' - } + get: { + method: 'GET', + params: { + id:'me' } - }); + } }); +} + +angular.module('<%= scriptAppName %>.auth') + .factory('User', UserResource); + +})(); diff --git a/app/templates/client/components/util/util.module.js b/app/templates/client/components/util/util.module.js new file mode 100644 index 000000000..690b12456 --- /dev/null +++ b/app/templates/client/components/util/util.module.js @@ -0,0 +1,3 @@ +'use strict'; + +angular.module('<%= scriptAppName %>.util', []); diff --git a/app/templates/client/components/util/util.service.js b/app/templates/client/components/util/util.service.js new file mode 100644 index 000000000..71f0c25e5 --- /dev/null +++ b/app/templates/client/components/util/util.service.js @@ -0,0 +1,62 @@ +'use strict'; + +(function() { + +/** + * The Util service is for thin, globally reusable, utility functions + */ +function UtilService($window) { + + var Util = { + + /** + * Return a callback or noop function + * + * @param {Function|*} cb - a 'potential' function + * @return {Function} + */ + safeCb: function(cb) { + return (angular.isFunction(cb)) ? cb : angular.noop; + }, + + /** + * Parse a given url with the use of an anchor element + * + * @param {String} url - the url to parse + * @return {Object} - the parsed url, anchor element + */ + urlParse: function(url) { + var a = document.createElement('a'); + a.href = url; + return a; + }, + + /** + * Test whether or not a given url is same origin + * + * @param {String} url - url to test + * @param {String|String[]} [origins] - additional origins to test against + * @return {Boolean} - true if url is same origin + */ + isSameOrigin: function(url, origins) { + url = Util.urlParse(url); + origins = (origins && [].concat(origins)) || []; + origins = origins.map(Util.urlParse); + origins.push($window.location); + origins = origins.filter(function(o) { + return url.hostname === o.hostname && + url.port === o.port && + url.protocol === o.protocol; + }); + return (origins.length >= 1); + } + + }; + + return Util; +} + +angular.module('<%= scriptAppName %>.util') + .factory('Util', UtilService); + +})(); diff --git a/test/test-file-creation.js b/test/test-file-creation.js index dda6bfbd1..5edd9ec84 100644 --- a/test/test-file-creation.js +++ b/test/test-file-creation.js @@ -182,6 +182,8 @@ describe('angular-fullstack generator', function () { 'client/components/navbar/navbar.' + markup, 'client/components/navbar/navbar.controller.' + script, 'client/components/navbar/navbar.directive.' + script, + 'client/components/util/util.module.' + script, + 'client/components/util/util.service.' + script, 'server/.jshintrc', 'server/.jshintrc-spec', 'server/app.js', @@ -267,7 +269,10 @@ describe('angular-fullstack generator', function () { 'client/app/admin/admin.' + stylesheet, 'client/app/admin/admin.' + script, 'client/app/admin/admin.controller.' + script, + 'client/components/auth/auth.module.' + script, 'client/components/auth/auth.service.' + script, + 'client/components/auth/interceptor.service.' + script, + 'client/components/auth/router.decorator.' + script, 'client/components/auth/user.service.' + script, 'client/components/mongoose-error/mongoose-error.directive.' + script, 'server/api/user/index.js',