diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index cc80427b2b..75dabd23f7 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -79,7 +79,8 @@ angular.module('ansible', [ 'InventorySummaryHelpDefinition', 'InventoryHostsHelpDefinition', 'TreeSelector', - 'CredentialsHelper' + 'CredentialsHelper', + 'TimerService' ]) .config(['$routeProvider', function($routeProvider) { $routeProvider. @@ -248,25 +249,38 @@ angular.module('ansible', [ otherwise({redirectTo: '/home'}); }]) .run(['$cookieStore', '$rootScope', 'CheckLicense', '$location', 'Authorization','LoadBasePaths', 'ViewLicense', - function($cookieStore, $rootScope, CheckLicense, $location, Authorization, LoadBasePaths, ViewLicense) { + 'Timer', + function($cookieStore, $rootScope, CheckLicense, $location, Authorization, LoadBasePaths, ViewLicense, + Timer) { LoadBasePaths(); - if ( !(typeof $AnsibleConfig.refresh_rate == 'number' && $AnsibleConfig.refresh_rate >= 3 - && $AnsibleConfig.refresh_rate <= 99) ) { - $AnsibleConfig.refresh_rate = 10; - } - $rootScope.breadcrumbs = new Array(); $rootScope.crumbCache = new Array(); - + $rootScope.sessionTimer = Timer.init(); + $rootScope.$on("$routeChangeStart", function(event, next, current) { // On each navigation request, check that the user is logged in + + var tst = /login/; + var path = $location.path(); + if ( !tst.test($location.path()) ) { + // capture most recent URL, excluding login + $rootScope.lastPath = path; + $cookieStore.put('lastPath', path); + } + if (Authorization.isUserLoggedIn() == false) { if ( next.templateUrl != (urlPrefix + 'partials/login.html') ) { $location.path('/login'); } } + else if ($rootScope.sessionTimer.isExpired()) { + if ( next.templateUrl != (urlPrefix + 'partials/login.html') ) { + $rootScope.sessionTimer.expireSession(); + $location.path('/login'); + } + } else { if ($rootScope.current_user == undefined || $rootScope.current_user == null) { Authorization.restoreUserInfo(); //user must have hit browser refresh diff --git a/awx/ui/static/js/config.js b/awx/ui/static/js/config.js index 430042ceac..ae6fbec83d 100644 --- a/awx/ui/static/js/config.js +++ b/awx/ui/static/js/config.js @@ -14,13 +14,13 @@ var $AnsibleConfig = debug_mode: true, // Enable console logging messages - refresh_rate: 10, // Number of seconds before refreshing a page. Integer between 3 and 99, inclusive. - // Used by awRefresh directive to automatically refresh Jobs and Projects pages. - - password_strength: 45 // User password strength. Integer between 0 and 100, 100 being impossibly strong. + password_strength: 45, // User password strength. Integer between 0 and 100, 100 being impossibly strong. // This value controls progress bar colors: // 0 to password_strength - 15 = red; // password_strength - 15 to password_strength = yellow // > password_strength = green // It also controls password validation. Passwords are rejected if the score is not > password_strength. + + session_timeout: 15 // Number of seconds before an inactive session is automatically timed out and forced to log in again. + // Separate from time out value set in API. } diff --git a/awx/ui/static/js/controllers/Authentication.js b/awx/ui/static/js/controllers/Authentication.js index 43562943ea..bb1a7ae34f 100644 --- a/awx/ui/static/js/controllers/Authentication.js +++ b/awx/ui/static/js/controllers/Authentication.js @@ -10,12 +10,33 @@ 'use strict'; -function Authenticate($cookieStore, $window, $scope, $rootScope, $location, Authorization, ToggleClass, Alert, Wait) +function Authenticate($cookieStore, $window, $scope, $rootScope, $location, Authorization, ToggleClass, Alert, Wait, + Timer, Empty) { var setLoginFocus = function() { $('#login-username').focus(); }; + var sessionExpired = function() { + return (Empty($rootScope.sessionExpired)) ? $cookieStore.get('sessionExpired') : $rootScope.sessionExpired; + }(); + + var lastPath = function() { + return (Empty($rootScope.lastPath)) ? $cookieStore.get('lastPath') : $rootScope.lastPath; + }(); + + if ($AnsibleConfig.debug_mode && console) { + console.log('User session expired: ' + sessionExpired); + console.log('Last URL: ' + lastPath); + } + + // Hide any lingering modal dialogs + $('.modal[aria-hidden=false]').each( function() { + if ($(this).attr('id') !== 'login-modal') { + $(this).modal('hide'); + } + }); + // Just in case, make sure the wait widget is not active Wait('stop'); @@ -70,6 +91,7 @@ function Authenticate($cookieStore, $window, $scope, $rootScope, $location, Auth $('#login-modal').modal('hide'); token = data.token; Authorization.setToken(data.token, data.expires); + $rootScope.sessionTimer = Timer.init(); // Get all the profile/access info regarding the logged in user Authorization.getUser() .success(function(data, status, headers, config) { @@ -77,7 +99,13 @@ function Authenticate($cookieStore, $window, $scope, $rootScope, $location, Auth Authorization.getLicense() .success(function(data, status, headers, config) { Authorization.setLicense(data['license_info']); - $location.url('/home?login=true'); + if (lastPath) { + // Go back to most recent navigation path + $location.path(lastPath); + } + else { + $location.url('/home?login=true'); + } }) .error(function(data, status, headers, config) { Alert('Error', 'Failed to access user information. GET returned status: ' + status, 'alert-danger', setLoginFocus); @@ -113,5 +141,6 @@ function Authenticate($cookieStore, $window, $scope, $rootScope, $location, Auth } } -Authenticate.$inject = ['$cookieStore', '$window', '$scope', '$rootScope', '$location', 'Authorization', 'ToggleClass', 'Alert', 'Wait']; +Authenticate.$inject = ['$cookieStore', '$window', '$scope', '$rootScope', '$location', 'Authorization', 'ToggleClass', 'Alert', 'Wait', + 'Timer', 'Empty']; diff --git a/awx/ui/static/js/forms/JobTemplates.js b/awx/ui/static/js/forms/JobTemplates.js index 329ebb969e..438b7d9cee 100644 --- a/awx/ui/static/js/forms/JobTemplates.js +++ b/awx/ui/static/js/forms/JobTemplates.js @@ -78,8 +78,8 @@ angular.module('JobTemplateFormDefinition', []) sourceModel: 'credential', sourceField: 'name', ngClick: 'lookUpCredential()', - addRequired: false, - editRequired: false, + addRequired: true, + editRequired: true, column: 1 }, cloud_credential: { diff --git a/awx/ui/static/js/helpers/search.js b/awx/ui/static/js/helpers/search.js index 6e42d74ae0..40cd8d46b1 100644 --- a/awx/ui/static/js/helpers/search.js +++ b/awx/ui/static/js/helpers/search.js @@ -65,7 +65,7 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper']) var f = scope[iterator + 'SearchField'] if (list.fields[f].searchType && ( list.fields[f].searchType == 'boolean' - || list.fields[f].searchType == 'select')) { + || list.fields[f].searchType == 'select')) { scope[iterator + 'SelectShow'] = true; scope[iterator + 'SearchSelectOpts'] = list.fields[f].searchOptions; } diff --git a/awx/ui/static/lib/ansible/AuthService.js b/awx/ui/static/lib/ansible/AuthService.js index c973a3adff..f104298a64 100644 --- a/awx/ui/static/lib/ansible/AuthService.js +++ b/awx/ui/static/lib/ansible/AuthService.js @@ -51,7 +51,7 @@ angular.module('AuthService', ['ngCookies', 'Utilities']) $rootScope.$destroy(); $cookieStore.remove('accordions'); $cookieStore.remove('token'); - $cookieStore.remove('token_expire'); + $cookieStore.remove('token_expires'); $cookieStore.remove('current_user'); $cookieStore.put('userLoggedIn', false); $cookieStore.put('sessionExpired', false); @@ -60,7 +60,7 @@ angular.module('AuthService', ['ngCookies', 'Utilities']) $rootScope.userLoggedIn = false; $rootScope.sessionExpired = false; $rootScope.token = null; - $rootScope.token_expire = new Date(1970, 0, 1, 0, 0, 0, 0); + $rootScope.token_expires = null; }, getLicense: function() { diff --git a/awx/ui/static/lib/ansible/RestServices.js b/awx/ui/static/lib/ansible/RestServices.js index f2ea985d0a..2ef2ed2926 100644 --- a/awx/ui/static/lib/ansible/RestServices.js +++ b/awx/ui/static/lib/ansible/RestServices.js @@ -5,13 +5,16 @@ * */ angular.module('RestServices',['ngCookies','AuthService']) - .factory('Rest', ['$http','$rootScope','$cookieStore','Authorization', function($http, $rootScope, $cookieStore, Authorization) { +.factory('Rest', ['$http','$rootScope','$cookieStore', '$q', 'Authorization', +function($http, $rootScope, $cookieStore, $q, Authorization) { return { setUrl: function (url) { this.url = url; }, - + checkExpired: function() { + return $rootScope.sessionTimer.isExpired(); + }, pReplace: function() { //in our url, replace :xx params with a value, assuming //we can find it in user supplied params. @@ -24,69 +27,107 @@ angular.module('RestServices',['ngCookies','AuthService']) } } }, - + createResponse: function(data, status) { + // Simulate an http response when a token error occurs + // http://stackoverflow.com/questions/18243286/angularjs-promises-simulate-http-promises + + var promise = $q.reject({ data: data, status: status }); + promise.success = function(fn){ + promise.then(function(response){ fn(response.data, response.status) }, null); + return promise + }; + promise.error = function(fn){ + promise.then(null, function(response){ fn(response.data, response.status) }); + return promise; + }; + return promise; + }, get: function(args) { args = (args) ? args : {}; this.params = (args.params) ? args.params : null; this.pReplace(); + var expired = this.checkExpired(); var token = Authorization.getToken(); - if (token) { - return $http({method: 'GET', - url: this.url, - headers: { 'Authorization': 'Token ' + token }, - params: this.params - }); + if (expired) { + return this.createResponse({ detail: 'Token is expired' }, 401); + } + else if (token) { + return $http({method: 'GET', + url: this.url, + headers: { 'Authorization': 'Token ' + token }, + params: this.params + }); } else { - return false; + return this.createResponse({ detail: 'Invalid token' }, 401); } }, post: function(data) { var token = Authorization.getToken(); - if (token) { - return $http({method: 'POST', - url: this.url, - headers: { 'Authorization': 'Token ' + token }, - data: data }); + var expired = this.checkExpired(); + if (expired) { + return this.createResponse({ detail: 'Token is expired' }, 401); + } + else if (token) { + return $http({ + method: 'POST', + url: this.url, + headers: { 'Authorization': 'Token ' + token }, + data: data }); } else { - return false; + return this.createResponse({ detail: 'Invalid token' }, 401); } }, put: function(data) { var token = Authorization.getToken(); - if (token) { - return $http({method: 'PUT', - url: this.url, - headers: { 'Authorization': 'Token ' + token }, - data: data }); + var expired = this.checkExpired(); + if (expired) { + return this.createResponse({ detail: 'Token is expired' }, 401); + } + else if (token) { + return $http({ + method: 'PUT', + url: this.url, + headers: { 'Authorization': 'Token ' + token }, + data: data }); } else { - return false; + return this.createResponse({ detail: 'Invalid token' }, 401); } }, destroy: function(data) { var token = Authorization.getToken(); - if (token) { - return $http({method: 'DELETE', - url: this.url, - headers: { 'Authorization': 'Token ' + token }, - data: data}); + var expired = this.checkExpired(); + if (expired) { + return this.createResponse({ detail: 'Token is expired' }, 401); } + else if (token) { + return $http({ + method: 'DELETE', + url: this.url, + headers: { 'Authorization': 'Token ' + token }, + data: data }); + } else { - return false; + return this.createResponse({ detail: 'Invalid token' }, 401); } }, options: function() { var token = Authorization.getToken(); - if (token) { - return $http({method: 'OPTIONS', - url: this.url, - headers: { 'Authorization': 'Token ' + token } - }); + var expired = this.checkExpired(); + if (expired) { + return this.createResponse({ detail: 'Token is expired' }, 401); + } + else if (token) { + return $http({ + method: 'OPTIONS', + url: this.url, + headers: { 'Authorization': 'Token ' + token } + }); } else { - return false; + return this.createResponse({ detail: 'Invalid token' }, 401); } } } diff --git a/awx/ui/static/lib/ansible/Timer.js b/awx/ui/static/lib/ansible/Timer.js new file mode 100644 index 0000000000..6e79e7dbde --- /dev/null +++ b/awx/ui/static/lib/ansible/Timer.js @@ -0,0 +1,55 @@ +/************************************************** + * Copyright (c) 2013 AnsibleWorks, Inc. + * + * Timer.js + * + * Use to track user idle time and expire session. Timeout + * duration set in config.js + * + */ +angular.module('TimerService', ['ngCookies', 'Utilities']) + .factory('Timer', ['$rootScope', '$cookieStore', '$location', 'GetBasePath', 'Empty', + function($rootScope, $cookieStore, $location, GetBasePath, Empty) { + return { + + sessionTime: null, + timeout: null, + + getSessionTime: function() { + return (this.sessionTime) ? this.sessionTime : $cookieStore.get('sessionTime'); + }, + + isExpired: function() { + var stime = this.getSessionTime(); + var now = new Date().getTime(); + if ((stime - now) <= 0) { + //expired + return true; + } + else { + // not expired. move timer forward. + this.moveForward(); + return false; + } + }, + + expireSession: function() { + this.sessionTime = 0; + $rootScope.sessionExpired = true; + $cookieStore.put('sessionExpired', true); + }, + + moveForward: function() { + var t = new Date().getTime() + ($AnsibleConfig.session_timeout * 1000); + this.sessionTime = t; + $cookieStore.put('sessionTime', t); + $rootScope.sessionExpired = false; + $cookieStore.put('sessionExpired', false); + }, + + init: function() { + this.moveForward(); + return this; + } + } + }]); \ No newline at end of file diff --git a/awx/ui/static/lib/ansible/Utilities.js b/awx/ui/static/lib/ansible/Utilities.js index 1b5585945d..bf39e22598 100644 --- a/awx/ui/static/lib/ansible/Utilities.js +++ b/awx/ui/static/lib/ansible/Utilities.js @@ -91,8 +91,8 @@ angular.module('Utilities',['RestServices', 'Utilities']) } }]) - .factory('ProcessErrors', ['$cookieStore', '$log', '$location', '$rootScope', 'Alert', - function($cookieStore, $log, $location, $rootScope, Alert) { + .factory('ProcessErrors', ['$rootScope', '$cookieStore', '$log', '$location', 'Alert', + function($rootScope, $cookieStore, $log, $location, Alert) { return function(scope, data, status, form, defaultMsg) { if ($AnsibleConfig.debug_mode && console) { console.log('Debug status: ' + status); @@ -110,15 +110,13 @@ angular.module('Utilities',['RestServices', 'Utilities']) Alert(defaultMsg.hdr, msg); } else if (status == 401 && data.detail && data.detail == 'Token is expired') { - $rootScope.sessionExpired = true; - $cookieStore.put('sessionExpired', true); - $location.path('/login'); + $rootScope.sessionTimer.expireSession(); + window.location = '/#/login'; //resetting location so that we drop search params } else if (status == 401 && data.detail && data.detail == 'Invalid token') { // should this condition be treated as an expired session?? Yes, for now. - $rootScope.sessionExpired = true; - $cookieStore.put('sessionExpired', true); - $location.path('/login'); + $rootScope.sessionTimer.expireSession(); + window.location = '/#/login'; //resetting location so that we drop search params } else if (data.non_field_errors) { Alert('Error!', data.non_field_errors); diff --git a/awx/ui/static/lib/ansible/form-generator.js b/awx/ui/static/lib/ansible/form-generator.js index c955a061bf..404174412a 100644 --- a/awx/ui/static/lib/ansible/form-generator.js +++ b/awx/ui/static/lib/ansible/form-generator.js @@ -9,10 +9,10 @@ */ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities']) - .factory('GenerateForm', [ '$location', '$cookieStore', '$compile', 'SearchWidget', 'PaginateWidget', 'Attr', 'Icon', 'Column', - 'NavigationLink', 'HelpCollapse', 'Button', 'DropDown', 'Empty', - function($location, $cookieStore, $compile, SearchWidget, PaginateWidget, Attr, Icon, Column, NavigationLink, HelpCollapse, Button, - DropDown, Empty) { + .factory('GenerateForm', ['$rootScope', '$location', '$cookieStore', '$compile', 'SearchWidget', 'PaginateWidget', 'Attr', + 'Icon', 'Column', 'NavigationLink', 'HelpCollapse', 'Button', 'DropDown', 'Empty', + function($rootScope, $location, $cookieStore, $compile, SearchWidget, PaginateWidget, Attr, Icon, Column, NavigationLink, + HelpCollapse, Button, DropDown, Empty) { return { setForm: function(form) { @@ -26,14 +26,15 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities']) accordion_count: 0, has: function(key) { - return (this.form[key] && this.form[key] != null && this.form[key] != undefined) ? true : false; - }, - + return (this.form[key] && this.form[key] != null && this.form[key] != undefined) ? true : false; + }, + inject: function(form, options) { // // Use to inject the form as html into the view. View MUST have an ng-bind for 'htmlTemplate'. // Returns scope of form. // + var element; if (options.modal) { if (options.modal_body_id) { @@ -164,7 +165,9 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities']) if (!this.scope.$$phase) { this.scope.$digest(); } + return this.scope; + }, applyDefaults: function() { @@ -181,42 +184,46 @@ angular.module('FormGenerator', ['GeneratorHelpers', 'ngCookies', 'Utilities']) }, reset: function() { - // The form field values cannot be reset with jQuery. Each field is tied to a model, so to clear the field - // value, you have to clear the model. - this.scope[this.form.name + '_form'].$setPristine(); - for (var fld in this.form.fields) { - if (this.form.fields[fld].type == 'checkbox_group') { + // The form field values cannot be reset with jQuery. Each field is tied to a model, so to clear the field + // value, you have to clear the model. + + if (this.scope[this.form.name + '_form']) { + this.scope[this.form.name + '_form'].$setPristine(); + } + + for (var fld in this.form.fields) { + if (this.form.fields[fld].type == 'checkbox_group') { for (var i=0; i < this.form.fields[fld].fields.length; i++) { this.scope[this.form.fields[fld].fields[i].name] = ''; this.scope[this.form.fields[fld].fields[i].name + '_api_error'] = ''; } - } - else { + } + else { this.scope[fld] = ''; this.scope[fld + '_api_error'] = ''; - } - if (this.form.fields[fld].sourceModel) { + } + if (this.form.fields[fld].sourceModel) { this.scope[this.form.fields[fld].sourceModel + '_' + this.form.fields[fld].sourceField] = ''; this.scope[this.form.fields[fld].sourceModel + '_' + this.form.fields[fld].sourceField + '_api_error'] = ''; - } - if ( this.form.fields[fld].type == 'lookup' && + } + if ( this.form.fields[fld].type == 'lookup' && this.scope[this.form.name + '_form'][this.form.fields[fld].sourceModel + '_' + this.form.fields[fld].sourceField] ) { this.scope[this.form.name + '_form'][this.form.fields[fld].sourceModel + '_' + this.form.fields[fld].sourceField].$setPristine(); - } - if (this.scope[this.form.name + '_form'][fld]) { + } + if (this.scope[this.form.name + '_form'][fld]) { this.scope[this.form.name + '_form'][fld].$setPristine(); - } - //if (this.scope.fields[fld].awPassMatch) { - // this.scope[this.form.name + '_form'][fld].$setValidity('awpassmatch', true); - //} - if (this.form.fields[fld].ask) { + } + //if (this.scope.fields[fld].awPassMatch) { + // this.scope[this.form.name + '_form'][fld].$setValidity('awpassmatch', true); + //} + if (this.form.fields[fld].ask) { this.scope[fld + '_ask'] = false; - } - } - if (this.mode == 'add') { - this.applyDefaults(); - } - }, + } + } + if (this.mode == 'add') { + this.applyDefaults(); + } + }, addListeners: function() { diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index 607bcd6db2..5bf43b29b1 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -40,6 +40,7 @@ +