diff --git a/awx/ui/client/lib/theme/index.less b/awx/ui/client/lib/theme/index.less index b5ac04dfca..584be08ca6 100644 --- a/awx/ui/client/lib/theme/index.less +++ b/awx/ui/client/lib/theme/index.less @@ -136,6 +136,7 @@ @import '../../src/tooltip/tooltip.block.less'; @import '../../src/workflow-results/workflow-status-bar/workflow-status-bar.block.less'; @import '../../src/workflow-results/workflow-results.block.less'; +@import '../../src/users/token-modal.block.less'; /** diff --git a/awx/ui/client/src/login/authenticationServices/authentication.service.js b/awx/ui/client/src/login/authenticationServices/authentication.service.js index addeab821e..8aea962847 100644 --- a/awx/ui/client/src/login/authenticationServices/authentication.service.js +++ b/awx/ui/client/src/login/authenticationServices/authentication.service.js @@ -53,7 +53,7 @@ export default return $http({ method: 'POST', url: `/api/login/`, - data: `username=${username}&password=${password}&csrfmiddlewaretoken=${csrfmiddlewaretoken}&next=%2fapi%2f`, + data: `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&csrfmiddlewaretoken=${csrfmiddlewaretoken}&next=%2fapi%2f`, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 6444370b3b..892b156d5c 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -166,14 +166,18 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat // Also wraps mess of generated HTML in a .Panel wrapPanel(html, ignorePanel){ if(ignorePanel) { - return `
+ return ` +
+
${html}
`; } else { - return `
+ return ` +
+
${html}
diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index 4e2fe0609a..3dbb9514c4 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -583,7 +583,8 @@ export default ['$compile', 'Attr', 'Icon', }, wrapPanel: function(html){ - return `
${html}
`; + return ` +
${html}
`; }, insertFormView: function(){ diff --git a/awx/ui/client/src/users/edit/users-edit.controller.js b/awx/ui/client/src/users/edit/users-edit.controller.js index 289f2d5556..0accb1bdcf 100644 --- a/awx/ui/client/src/users/edit/users-edit.controller.js +++ b/awx/ui/client/src/users/edit/users-edit.controller.js @@ -30,6 +30,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest', init(); function init() { + $scope.isCurrentlyLoggedInUser = (parseInt(id) === $rootScope.current_user.id); $scope.hidePagination = false; $scope.hideSmartSearch = false; $scope.user_type_options = user_type_options; diff --git a/awx/ui/client/src/users/main.js b/awx/ui/client/src/users/main.js index aa883c7001..95765ffa3a 100644 --- a/awx/ui/client/src/users/main.js +++ b/awx/ui/client/src/users/main.js @@ -9,6 +9,11 @@ import UsersAdd from './add/users-add.controller'; import UsersEdit from './edit/users-edit.controller'; import UserForm from './users.form'; import UserList from './users.list'; +import UserTokensListRoute from './users-tokens-list.route'; +import UserTokensAddRoute from './users-tokens-add.route'; +import UserTokensAddApplicationRoute from './users-tokens-add-application.route'; +import TokensStrings from './tokens.strings'; + import { N_ } from '../i18n'; export default @@ -18,16 +23,15 @@ angular.module('Users', []) .controller('UsersEdit', UsersEdit) .factory('UserForm', UserForm) .factory('UserList', UserList) - .config(['$stateProvider', 'stateDefinitionsProvider', - function($stateProvider, stateDefinitionsProvider) { - let stateDefinitions = stateDefinitionsProvider.$get(); + .service('TokensStrings', TokensStrings) - // lazily generate a tree of substates which will replace this node in ui-router's stateRegistry - // see: stateDefinition.factory for usage documentation - $stateProvider.state({ - name: 'users.**', - url: '/users', - lazyLoad: () => stateDefinitions.generateTree({ + .config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider', + function($stateProvider, stateDefinitionsProvider, $stateExtenderProvider) { + let stateDefinitions = stateDefinitionsProvider.$get(); + let stateExtender = $stateExtenderProvider.$get(); + + function generateStateTree() { + let userTree = stateDefinitions.generateTree({ parent: 'users', modes: ['add', 'edit'], list: 'UserList', @@ -44,7 +48,28 @@ angular.module('Users', []) ncyBreadcrumb: { label: N_('USERS') } - }) + }); + + return Promise.all([ + userTree + ]).then((generated) => { + return { + states: _.reduce(generated, (result, definition) => { + return result.concat(definition.states); + }, [ + stateExtender.buildDefinition(UserTokensListRoute), + stateExtender.buildDefinition(UserTokensAddRoute), + stateExtender.buildDefinition(UserTokensAddApplicationRoute) + ]) + }; + }); + } + + $stateProvider.state({ + name: 'users.**', + url: '/users', + lazyLoad: () => generateStateTree() }); + } ]); diff --git a/awx/ui/client/src/users/token-modal.block.less b/awx/ui/client/src/users/token-modal.block.less new file mode 100644 index 0000000000..d913b3e3ef --- /dev/null +++ b/awx/ui/client/src/users/token-modal.block.less @@ -0,0 +1,12 @@ +/** @define TokenModal */ +.TokenModal { + display: flex; +} + +.TokenModal-label { + font-weight: bold; + width: 130px; +} + +.TokenModal-value { +} diff --git a/awx/ui/client/src/users/tokens.strings.js b/awx/ui/client/src/users/tokens.strings.js new file mode 100644 index 0000000000..2780661e79 --- /dev/null +++ b/awx/ui/client/src/users/tokens.strings.js @@ -0,0 +1,30 @@ +function TokensStrings (BaseString) { + BaseString.call(this, 'tokens'); + + const { t } = this; + const ns = this.tokens; + + ns.state = { + LIST_BREADCRUMB_LABEL: t.s('TOKENS'), + ADD_BREADCRUMB_LABEL: t.s('CREATE TOKEN'), + USER_LIST_BREADCRUMB_LABEL: t.s('TOKENS') + }; + + ns.tab = { + DETAILS: t.s('Details') + }; + + ns.add = { + PANEL_TITLE: t.s('CREATE TOKEN') + }; + + ns.list = { + ROW_ITEM_LABEL_EXPIRED: t.s('DESCRIPTION'), + ROW_ITEM_LABEL_EXPIRED: t.s('EXPIRATION'), + ROW_ITEM_LABEL_USED: t.s('LAST USED') + }; +} + +TokensStrings.$inject = ['BaseStringService']; + +export default TokensStrings; diff --git a/awx/ui/client/src/users/users-tokens-add-application.route.js b/awx/ui/client/src/users/users-tokens-add-application.route.js new file mode 100644 index 0000000000..4ab5998d74 --- /dev/null +++ b/awx/ui/client/src/users/users-tokens-add-application.route.js @@ -0,0 +1,71 @@ +export default { + name: 'users.edit.tokens.add.application', + url: '/application?selected', + searchPrefix: 'application', + params: { + application_search: { + value: { + page_size: 5, + order_by: 'name' + }, + dynamic: true, + squash: '' + } + }, + data: { + basePath: 'applications', + formChildState: true + }, + ncyBreadcrumb: { + skip: true + }, + views: { + 'application@users.edit.tokens.add': { + templateProvider: (ListDefinition, generateList) => { + const html = generateList.build({ + mode: 'lookup', + list: ListDefinition, + input_type: 'radio' + }); + + return `${html}`; + } + } + }, + resolve: { + ListDefinition: [() => { + return { + name: 'applications', + iterator: 'application', + hover: true, + index: false, + fields: { + name: { + key: true, + label: 'Name', + columnClass: 'col-lg-4 col-md-6 col-sm-8 col-xs-8', + awToolTip: '{{application.description | sanitize}}', + dataPlacement: 'top' + }, + }, + actions: { + }, + fieldActions: { + } + } + }], + Dataset: ['QuerySet', 'GetBasePath', '$stateParams', 'ListDefinition', + (qs, GetBasePath, $stateParams, list) => qs.search( + GetBasePath('applications'), + $stateParams[`${list.iterator}_search`] + ) + ] + }, + onExit ($state) { + if ($state.transition) { + $('#form-modal').modal('hide'); + $('.modal-backdrop').remove(); + $('body').removeClass('modal-open'); + } + } +} diff --git a/awx/ui/client/src/users/users-tokens-add.controller.js b/awx/ui/client/src/users/users-tokens-add.controller.js new file mode 100644 index 0000000000..7b63a8ce61 --- /dev/null +++ b/awx/ui/client/src/users/users-tokens-add.controller.js @@ -0,0 +1,99 @@ +function AddTokensController (models, $state, strings, Rest, Alert, Wait, GetBasePath, $filter) { + const vm = this || {}; + const { application } = models; + + vm.mode = 'add'; + vm.strings = strings; + vm.panelTitle = strings.get('add.PANEL_TITLE'); + + vm.form = {}; + + vm.form.application = { + type: 'field', + label: 'Application', + id: 'application' + }; + vm.form.description = { + type: 'String', + label: 'Description', + id: 'description' + }; + + vm.form.application._resource = 'application'; + vm.form.application._route = 'users.edit.tokens.add.application'; + vm.form.application._model = application; + vm.form.application._placeholder = strings.get('SELECT AN APPLICATION'); + vm.form.application.required = true; + + vm.form.description.required = false; + + vm.form.scope = { + choices: ['', 'read', 'write'], + help_text: 'Specify a scope for the token\'s access', + id: 'scope', + label: 'Scope', + required: true, + _component: 'at-input-select', + _data: ['', 'read', 'write'], + _exp: 'choice for (index, choice) in state._data', + _format: 'array' + } + + vm.form.save = data => { + Rest.setUrl(GetBasePath('users') + $state.params.user_id + '/authorized_tokens'); + return Rest.post(data) + .then(({data}) => { + Alert('TOKEN INFORMATION', ` +
+
+ TOKEN +
+
+ ${data.token} +
+
+
+
+ REFRESH TOKEN +
+
+ ${data.refresh_token} +
+
+
+
+ EXPIRES +
+
+ ${$filter('longDate')(data.expires)} +
+
+ `, null, null, null, null, null, true); + Wait('stop'); + }) + .catch(({data, status}) => { + ProcessErrors($scope, data, status, null, { + hdr: 'COULD NOT CREATE TOKEN', + msg: `Returned status: ${status}` + }); + Wait('stop'); + }); + }; + + vm.form.onSaveSuccess = res => { + $state.go('^', { user_id: $state.params.user_id }, { reload: true }); + }; +} + +AddTokensController.$inject = [ + 'resolvedModels', + '$state', + 'ApplicationsStrings', + 'Rest', + 'Alert', + 'Wait', + 'GetBasePath', + '$filter' +]; + +export default AddTokensController; diff --git a/awx/ui/client/src/users/users-tokens-add.partial.html b/awx/ui/client/src/users/users-tokens-add.partial.html new file mode 100644 index 0000000000..21f5a8be04 --- /dev/null +++ b/awx/ui/client/src/users/users-tokens-add.partial.html @@ -0,0 +1,18 @@ + + + {{ vm.panelTitle }} + + + + + + + + + + + + + + + diff --git a/awx/ui/client/src/users/users-tokens-add.route.js b/awx/ui/client/src/users/users-tokens-add.route.js new file mode 100644 index 0000000000..e3832133a9 --- /dev/null +++ b/awx/ui/client/src/users/users-tokens-add.route.js @@ -0,0 +1,38 @@ +import { N_ } from '../i18n'; +import AddController from './users-tokens-add.controller'; + +const addTemplate = require('~src/users/users-tokens-add.partial.html'); + +function TokensDetailResolve ($q, Application) { + + const promises = {}; + + promises.application = new Application('options'); + + return $q.all(promises); +} + +TokensDetailResolve.$inject = [ + '$q', + 'ApplicationModel' +]; + +export default { + url: "/add-token", + name: 'users.edit.tokens.add', + params: { + }, + ncyBreadcrumb: { + label: N_("ADD TOKEN") + }, + views: { + 'preFormView@users.edit': { + templateUrl: addTemplate, + controller: AddController, + controllerAs: 'vm' + } + }, + resolve: { + resolvedModels: TokensDetailResolve + } +}; diff --git a/awx/ui/client/src/users/users-tokens-list.controller.js b/awx/ui/client/src/users/users-tokens-list.controller.js new file mode 100644 index 0000000000..bee39f45ae --- /dev/null +++ b/awx/ui/client/src/users/users-tokens-list.controller.js @@ -0,0 +1,119 @@ +/** *********************************************** + * Copyright (c) 2018 Ansible, Inc. + * + * All Rights Reserved + ************************************************ */ +function ListTokensController ( + $filter, + $scope, + $state, + Dataset, + strings, + ProcessErrors, + Rest, + GetBasePath, + Prompt, + Wait +) { + const vm = this || {}; + + vm.strings = strings; + vm.activeId = $state.params.token_id; + + $scope.canAdd = true; + + // smart-search + const name = 'tokens'; + const iterator = 'token'; + const key = 'token_dataset'; + + $scope.list = { iterator, name, basePath: 'tokens' }; + $scope.collection = { iterator }; + $scope[key] = Dataset.data; + vm.tokensCount = Dataset.data.count; + $scope[name] = Dataset.data.results; + $scope.$on('updateDataset', (e, dataset) => { + $scope[key] = dataset; + $scope[name] = dataset.results; + vm.tokensCount = dataset.count; + }); + + vm.getLastUsed = token => { + const lastUsed = _.get(token, 'last_used'); + + if (!lastUsed) { + return undefined; + } + + let html = $filter('longDate')(lastUsed); + + const { username, id } = _.get(user, 'summary_fields.last_used', {}); + + if (username && id) { + html += ` by ${$filter('sanitize')(username)}`; + } + + return html; + }; + + vm.deleteToken = (tok) => { + const action = () => { + $('#prompt-modal').modal('hide'); + Wait('start'); + Rest.setUrl(`${GetBasePath('tokens')}${tok.id}`); + Rest.destroy() + .then(() => { + let reloadListStateParams = null; + + if ($scope.tokens.length === 1 && $state.params.token_search && + !_.isEmpty($state.params.token_search.page) && + $state.params.token_search.page !== '1') { + const page = `${(parseInt(reloadListStateParams + .token_search.page, 10) - 1)}`; + reloadListStateParams = _.cloneDeep($state.params); + reloadListStateParams.token_search.page = page; + } + + if (parseInt($state.params.token_id, 10) === tok.id) { + $state.go('^', reloadListStateParams, { reload: true }); + } else { + $state.go('.', reloadListStateParams, { reload: true }); + } + }) + .catch(({ data, status }) => { + ProcessErrors($scope, data, status, null, { + hdr: strings.get('error.HEADER'), + msg: strings.get('error.CALL', { path: `${GetBasePath('tokens')}${tok.id}`, status }) + }); + }) + .finally(() => { + Wait('stop'); + }); + }; + + const deleteModalBody = `
${strings.get('deleteResource.CONFIRM', 'token')}
`; + + Prompt({ + hdr: strings.get('deleteResource.HEADER'), + resourceName: 'token', + body: deleteModalBody, + action, + actionText: 'DELETE' + }); + }; +} + +ListTokensController.$inject = [ + '$filter', + '$scope', + '$state', + 'Dataset', + 'TokensStrings', + 'ProcessErrors', + 'Rest', + 'GetBasePath', + 'Prompt', + 'Wait' +]; + +export default ListTokensController; diff --git a/awx/ui/client/src/users/users-tokens-list.partial.html b/awx/ui/client/src/users/users-tokens-list.partial.html new file mode 100644 index 0000000000..390361faba --- /dev/null +++ b/awx/ui/client/src/users/users-tokens-list.partial.html @@ -0,0 +1,47 @@ +
+ + +
+ +
+
+ + + +
+ + + + + + + + +
+
+ + +
+
+
diff --git a/awx/ui/client/src/users/users-tokens-list.route.js b/awx/ui/client/src/users/users-tokens-list.route.js new file mode 100644 index 0000000000..81ec099adc --- /dev/null +++ b/awx/ui/client/src/users/users-tokens-list.route.js @@ -0,0 +1,48 @@ +import { N_ } from '../i18n'; + +import ListController from './users-tokens-list.controller'; + +const listTemplate = require('~src/users/users-tokens-list.partial.html'); + +export default { + url: "/tokens", + name: 'users.edit.tokens', + params: { + }, + ncyBreadcrumb: { + label: N_("TOKENS") + }, + views: { + 'related': { + templateUrl: listTemplate, + controller: ListController, + controllerAs: 'vm' + } + }, + searchPrefix: 'token', + params: { + token_search: { + value: { + page_size: 5, + order_by: 'application' + } + } + }, + resolve: { + Dataset: [ + '$stateParams', + 'Wait', + 'GetBasePath', + 'QuerySet', + ($stateParams, Wait, GetBasePath, qs) => { + const searchParam = $stateParams.token_search; + const searchPath = GetBasePath('users') + $stateParams.user_id + '/tokens'; + Wait('start'); + return qs.search(searchPath, searchParam) + .finally(() => { + Wait('stop'); + }); + } + ], + } +}; diff --git a/awx/ui/client/src/users/users.form.js b/awx/ui/client/src/users/users.form.js index 521a2cb75e..78ea4f33e4 100644 --- a/awx/ui/client/src/users/users.form.js +++ b/awx/ui/client/src/users/users.form.js @@ -227,6 +227,11 @@ export default ['i18n', function(i18n) { } }, //hideOnSuperuser: true // RBAC defunct + }, + tokens: { + ngIf: 'isCurrentlyLoggedInUser', + title: i18n._('Tokens'), + skipGenerator: true, } }