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 @@
+