2 * ============LICENSE_START==========================================
4 * ===================================================================
5 * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6 * ===================================================================
8 * Unless otherwise specified, all software contained herein is licensed
9 * under the Apache License, Version 2.0 (the "License");
10 * you may not use this software except in compliance with the License.
11 * You may obtain a copy of the License at
13 * http://www.apache.org/licenses/LICENSE-2.0
15 * Unless required by applicable law or agreed to in writing, software
16 * distributed under the License is distributed on an "AS IS" BASIS,
17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 * See the License for the specific language governing permissions and
19 * limitations under the License.
21 * Unless otherwise specified, all documentation contained herein is licensed
22 * under the Creative Commons License, Attribution 4.0 Intl. (the "License");
23 * you may not use this documentation except in compliance with the License.
24 * You may obtain a copy of the License at
26 * https://creativecommons.org/licenses/by/4.0/
28 * Unless required by applicable law or agreed to in writing, documentation
29 * distributed under the License is distributed on an "AS IS" BASIS,
30 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31 * See the License for the specific language governing permissions and
32 * limitations under the License.
34 * ============LICENSE_END============================================
39 * bulk user upload controller
43 class BulkUserModalCtrl {
44 constructor($scope, $log, $filter, $q, usersService, applicationsService, confirmBoxService, functionalMenuService, ngDialog,$modal) {
46 // Set to true for copious console output
48 // Roles fetched from app service
49 var appRolesResult = [];
50 // Users fetched from user service
51 var userCheckResult = [];
52 // Requests for user-role assignment built by validator
53 var appUserRolesRequest = [];
57 $log.debug('BulkUserModalCtrl::init');
58 // Angular insists on this.
59 $scope.fileModel = {};
60 // Model for drop-down
61 $scope.adminApps = [];
62 // Enable modal controls
64 this.fileSelected = false;
66 // Flag that indicates background work is proceeding
67 $scope.isProcessing = true;
69 $scope.isProcessedRecords = false;
71 // Load user's admin applications
72 applicationsService.getAdminApps().promise().then(apps => {
74 $log.debug('BulkUserModalCtrl::init: getAdminApps returned' + JSON.stringify(apps));
75 if (!apps || typeof(apps) != 'object') {
76 $log.error('BulkUserModalCtrl::init: getAdminApps returned unexpected data');
80 $log.debug('BulkUserModalCtrl::init: admin apps length is ', apps.length);
82 // Sort app names and populate the drop-down model
83 let sortedApps = apps.sort(getSortOrder('name', true));
84 for (let i = 0; i < sortedApps.length; ++i) {
85 $scope.adminApps.push({
88 value: sortedApps[i].name,
89 title: sortedApps[i].name
92 // Pick the first one in the list
93 $scope.selectedApplication = $scope.adminApps[0];
95 $scope.isProcessing = false;
96 $scope.isProcessedRecords = false;
98 $log.error('BulkUserModalCtrl::init: getAdminApps threw', err);
99 $scope.isProcessing = false;
100 $scope.isProcessedRecords = false;
105 // Answers a function that compares properties with the specified name.
106 let getSortOrder = (prop, foldCase) => {
107 return function(a, b) {
108 let aProp = foldCase ? a[prop].toLowerCase() : a[prop];
109 let bProp = foldCase ? b[prop].toLowerCase() : b[prop];
112 else if (aProp < bProp)
119 //This is a fix for dropdown selection, due to b2b dropdown only update value field
120 $scope.$watch('selectedApplication.value', (newVal, oldVal) => {
121 for(var i=0;i<$scope.adminApps.length;i++){
122 if($scope.adminApps[i].value==newVal){
123 $scope.selectedApplication=angular.copy($scope.adminApps[i]);;
128 // Invoked when user picks an app on the drop-down.
129 $scope.appSelected = () => {
131 $log.debug('BulkUserModalCtrl::appSelected: selectedApplication.id is ' + $scope.selectedApplication.id);
132 this.appSelected = true;
135 // Caches the file name supplied by the event handler.
136 $scope.fileChangeHandler = (event, files) => {
137 var fileName = files[0].name;
138 var validFormats = ['csv', 'txt'];
140 var ext = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
141 //Check for valid format
142 if(validFormats.indexOf(ext) == -1){
143 this.fileSelected = false;
145 this.fileSelected = true;
146 this.fileToRead = files[0];
149 $log.debug("BulkUserModalCtrl::fileChangeHandler: file is ", this.fileToRead);
150 }; // file change handler
153 * Reads the contents of the file, calls portal endpoints
154 * to validate roles, userIds and existing role assignments;
155 * ultimately builds array of requests to be sent.
156 * Creates scope variable with input file contents for
157 * communication with functions.
159 * This function performs a synchronous step-by-step process
160 * using asynchronous promises. The code could all be inline
161 * here but the nesting becomes unwieldy.
163 $scope.readValidateFile = () => {
164 $scope.isProcessing = true;
165 $scope.conformMsg = '';
166 $scope.isProcessedRecords = true;
167 $scope.progressMsg = 'Reading upload file..';
168 var reader = new FileReader();
169 reader.onload = function(event) {
170 $scope.uploadFile = $filter('csvToObj')(reader.result);
172 $log.debug('BulkUserModalCtrl::readValidateFile onload: data length is ' + $scope.uploadFile.length);
173 // sort input by orgUserId
174 $scope.uploadFile.sort(getSortOrder('orgUserId', true));
176 let appid = $scope.selectedApplication.id;
177 $scope.progressMsg = 'Fetching application roles..';
178 functionalMenuService.getManagedRolesMenu(appid).then(function (rolesObj) {
180 $log.debug("BulkUserModalCtrl::readValidateFile: managedRolesMenu returned " + JSON.stringify(rolesObj));
181 appRolesResult = rolesObj;
182 $scope.progressMsg = 'Validating application roles..';
183 $scope.verifyRoles();
185 let userPromises = $scope.buildUserChecks();
187 $log.debug('BulkUserModalCtrl::readValidateFile: userPromises length is ' + userPromises.length);
188 $scope.progressMsg = 'Validating Org Users..';
189 $q.all(userPromises).then(function() {
191 $log.debug('BulkUserModalCtrl::readValidateFile: userCheckResult length is ' + userCheckResult.length);
192 $scope.evalUserCheckResults();
194 let appPromises = $scope.buildAppRoleChecks();
196 $log.debug('BulkUserModalCtrl::readValidateFile: appPromises length is ' + appPromises.length);
197 $scope.progressMsg = 'Querying application for user roles..';
198 $q.all(appPromises).then( function() {
200 $log.debug('BulkUserModalCtrl::readValidateFile: appUserRolesRequest length is ' + appUserRolesRequest.length);
201 $scope.evalAppRoleCheckResults();
203 // Re sort by line for the confirmation dialog
204 $scope.uploadFile.sort(getSortOrder('line', false));
205 // We're done, confirm box may show the table
207 $log.debug('BulkUserModalCtrl::readValidateFile inner-then ends');
208 $scope.progressMsg = 'Done.';
209 $scope.isProcessing = false;
210 $scope.isProcessedRecords = false;
213 $log.error('BulkUserModalCtrl::readValidateFile: failed retrieving user-app roles');
214 $scope.isProcessing = false;
215 $scope.isProcessedRecords = false;
217 ); // then of app promises
220 $log.error('BulkUserModalCtrl::readValidateFile: failed retrieving user info');
221 $scope.isProcessing = false;
222 $scope.isProcessedRecords = false;
224 ); // then of user promises
227 $log.error('BulkUserModalCtrl::readValidateFile: failed retrieving app role info');
228 $scope.isProcessing = false;
229 $scope.isProcessedRecords = false;
231 ); // then of role promise
235 // Invoke the reader on the selected file
236 reader.readAsText(this.fileToRead);
240 * Evaluates the result set returned by the app role service.
241 * Sets an uploadFile array element status if a role is not defined.
242 * Reads and writes scope variable uploadFile.
243 * Reads closure variable appRolesResult.
245 $scope.verifyRoles = () => {
247 $log.debug('BulkUserModalCtrl::verifyRoles: appRoles is ' + JSON.stringify(appRolesResult));
248 // check roles in upload file against defined app roles
249 $scope.uploadFile.forEach( function (uploadRow) {
250 // skip rows that already have a defined status: headers etc.
251 if (uploadRow.status) {
253 $log.debug('BulkUserModalCtrl::verifyRoles: skip row ' + uploadRow.line);
256 uploadRow.role = uploadRow.role.trim();
258 for (var i=0; i < appRolesResult.length; i++) {
259 if (uploadRow.role.toUpperCase() === appRolesResult[i].rolename.trim().toUpperCase()) {
261 $log.debug('BulkUserModalCtrl::verifyRoles: match on role ' + uploadRow.role);
268 $log.debug('BulkUserModalCtrl::verifyRoles: NO match on role ' + uploadRow.role);
269 uploadRow.status = 'Invalid role';
275 * Builds and returns an array of promises to invoke the
276 * searchUsers service for each unique Org User UID in the input.
277 * Reads and writes scope variable uploadFile, which must be sorted by Org User UID.
278 * The promise function writes to closure variable userCheckResult
280 $scope.buildUserChecks = () => {
282 $log.debug('BulkUserModalCtrl::buildUserChecks: uploadFile length is ' + $scope.uploadFile.length);
283 userCheckResult = [];
286 $scope.uploadFile.forEach(function (uploadRow) {
287 if (uploadRow.status) {
289 $log.debug('BulkUserModalCtrl::buildUserChecks: skip row ' + uploadRow.line);
292 // detect repeated UIDs
293 if (prevRow == null || prevRow.orgUserId.toLowerCase() !== uploadRow.orgUserId.toLowerCase()) {
295 $log.debug('BulkUserModalCtrl::buildUserChecks: create request for orgUserId ' + uploadRow.orgUserId);
296 let userPromise = usersService.searchUsers(uploadRow.orgUserId).promise().then( (usersList) => {
297 if (typeof usersList[0] !== "undefined") {
298 userCheckResult.push({
299 orgUserId: usersList[0].orgUserId,
300 firstName: usersList[0].firstName,
301 lastName: usersList[0].lastName,
302 jobTitle: usersList[0].jobTitle
308 $log.debug('BulkUserModalCtrl::buildUserChecks: searchUsers returned null');
311 $log.error('BulkUserModalCtrl::buildUserChecks: searchUsers failed ' + JSON.stringify(error));
313 promises.push(userPromise);
317 $log.debug('BulkUserModalCtrl::buildUserChecks: skip repeated orgUserId ' + uploadRow.orgUserId);
322 }; // buildUserChecks
325 * Evaluates the result set returned by the user service to set
326 * the uploadFile array element status if the user was not found.
327 * Reads and writes scope variable uploadFile.
328 * Reads closure variable userCheckResult.
330 $scope.evalUserCheckResults = () => {
332 $log.debug('BulkUserModalCtrl::evalUserCheckResult: uploadFile length is ' + $scope.uploadFile.length);
333 $scope.uploadFile.forEach(function (uploadRow) {
334 if (uploadRow.status) {
336 $log.debug('BulkUserModalCtrl::evalUserCheckResults: skip row ' + uploadRow.line);
339 let foundorgUserId = false;
340 userCheckResult.forEach(function(userItem) {
341 if (uploadRow.orgUserId.toLowerCase() === userItem.orgUserId.toLowerCase()) {
343 $log.debug('BulkUserModalCtrl::evalUserCheckResults: found orgUserId ' + uploadRow.orgUserId);
347 if (!foundorgUserId) {
349 $log.debug('BulkUserModalCtrl::evalUserCheckResults: NO match on orgUserId ' + uploadRow.orgUserId);
350 uploadRow.status = 'Invalid orgUserId';
353 }; // evalUserCheckResults
356 * Builds and returns an array of promises to invoke the getUserAppRoles
357 * service for each unique Org User in the input file.
358 * Each promise creates an update to be sent to the remote application
359 * with all role names.
360 * Reads scope variable uploadFile, which must be sorted by Org User.
361 * The promise function writes to closure variable appUserRolesRequest
363 $scope.buildAppRoleChecks = () => {
365 $log.debug('BulkUserModalCtrl::buildAppRoleChecks: uploadFile length is ' + $scope.uploadFile.length);
366 appUserRolesRequest = [];
367 let appId = $scope.selectedApplication.id;
370 $scope.uploadFile.forEach( function (uploadRow) {
371 if (uploadRow.status) {
373 $log.debug('BulkUserModalCtrl::buildAppRoleChecks: skip row ' + uploadRow.line);
376 // Because the input is sorted, generate only one request for each Org User
377 if (prevRow == null || prevRow.orgUserId.toLowerCase() !== uploadRow.orgUserId.toLowerCase()) {
379 $log.debug('BulkUserModalCtrl::buildAppRoleChecks: create request for orgUserId ' + uploadRow.orgUserId);
380 let appPromise = usersService.getUserAppRoles(appId, uploadRow.orgUserId,true, false).promise().then( (userAppRolesResult) => {
381 // Reply for unknown user has all defined roles with isApplied=false on each.
382 if (typeof userAppRolesResult[0] !== "undefined") {
384 $log.debug('BulkUserModalCtrl::buildAppRoleChecks: adding result '
385 + JSON.stringify(userAppRolesResult));
386 appUserRolesRequest.push({
387 orgUserId: uploadRow.orgUserId,
388 userAppRoles: userAppRolesResult
391 $log.error('BulkUserModalCtrl::buildAppRoleChecks: getUserAppRoles returned ' + JSON.stringify(userAppRolesResult));
394 $log.error('BulkUserModalCtrl::buildAppRoleChecks: getUserAppRoles failed ', error);
396 promises.push(appPromise);
399 $log.debug('BulkUserModalCtrl::buildAppRoleChecks: duplicate orgUserId, skip: '+ uploadRow.orgUserId);
404 }; // buildAppRoleChecks
407 * Evaluates the result set returned by the app service and adjusts
408 * the list of updates to be sent to the remote application by setting
409 * isApplied=true for each role name found in the upload file.
410 * Reads and writes scope variable uploadFile.
411 * Reads closure variable appUserRolesRequest.
413 $scope.evalAppRoleCheckResults = () => {
415 $log.debug('BulkUserModalCtrl::evalAppRoleCheckResults: uploadFile length is ' + $scope.uploadFile.length);
416 $scope.uploadFile.forEach(function (uploadRow) {
417 if (uploadRow.status) {
419 $log.debug('BulkUserModalCtrl::evalAppRoleCheckResults: skip row ' + uploadRow.line);
422 // Search for the match in the app-user-roles array
423 appUserRolesRequest.forEach( function (appUserRoleObj) {
424 if (uploadRow.orgUserId.toLowerCase() === appUserRoleObj.orgUserId.toLowerCase()) {
426 $log.debug('BulkUserModalCtrl::evalAppRoleCheckResults: match on orgUserId ' + uploadRow.orgUserId);
427 let roles = appUserRoleObj.userAppRoles;
428 roles.forEach(function (appRoleItem) {
430 // $log.debug('BulkUserModalCtrl::evalAppRoleCheckResults: checking uploadRow.role='
431 // + uploadRow.role + ', appRoleItem.roleName= ' + appRoleItem.roleName);
432 if (uploadRow.role === appRoleItem.roleName) {
433 if (appRoleItem.isApplied) {
435 $log.debug('BulkUserModalCtrl::evalAppRoleCheckResults: existing role '
436 + appRoleItem.roleName);
437 uploadRow.status = 'Role exists';
441 $log.debug('BulkUserModalCtrl::evalAppRoleCheckResults: new role '
442 + appRoleItem.roleName);
443 // After much back-and-forth I decided a clear indicator
444 // is better than blank in the table status column.
445 uploadRow.status = 'OK';
446 appRoleItem.isApplied = true;
448 // This count is not especially interesting.
449 // numberUserRolesSucceeded++;
453 }); // for each result
455 }; // evalAppRoleCheckResults
458 * Sends requests to Portal requesting user role assignment.
459 * That endpoint handles creation of the user at the remote app if necessary.
460 * Reads closure variable appUserRolesRequest.
461 * Invoked by the Next button on the confirmation dialog.
463 $scope.updateDB = () => {
464 $scope.isProcessing = true;
465 $scope.conformMsg = '';
466 $scope.isProcessedRecords = true;
467 $scope.progressMsg = 'Sending requests to application..';
469 $log.debug('BulkUserModalCtrl::updateDB: request length is ' + appUserRolesRequest.length);
470 var numberUsersSucceeded = 0;
472 appUserRolesRequest.forEach(function(appUserRoleObj) {
474 $log.debug('BulkUserModalCtrl::updateDB: appUserRoleObj is ' + JSON.stringify(appUserRoleObj));
475 let updateRequest = {
476 orgUserId: appUserRoleObj.orgUserId,
477 appId: $scope.selectedApplication.id,
478 appRoles: appUserRoleObj.userAppRoles
481 $log.debug('BulkUserModalCtrl::updateDB: updateRequest is ' + JSON.stringify(updateRequest));
482 let updatePromise = usersService.updateUserAppRoles(updateRequest).promise().then(res => {
484 $log.debug('BulkUserModalCtrl::updateDB: updated successfully: ' + JSON.stringify(res));
485 numberUsersSucceeded++;
487 // What to do if one of many fails??
488 $log.error('BulkUserModalCtrl::updateDB failed: ', err);
489 confirmBoxService.showInformation(
490 'Failed to update the user application roles. ' +
491 'Error: ' + err.status).then(isConfirmed => { });
493 // $log.debug('BulkUserModalCtrl::updateDB: finally()');
495 promises.push(updatePromise);
498 // Run all the promises
499 $q.all(promises).then(function(){
500 $scope.conformMsg = 'Processed ' + numberUsersSucceeded + ' users.';
501 $scope.isProcessing = false;
502 $scope.isProcessedRecords = true;
503 $scope.uploadFile = [];
508 // Sets the variable that hides/reveals the user controls
509 $scope.step2 = () => {
510 this.fileSelected = false;
511 $scope.selectedFile = null;
512 $scope.fileModel = null;
516 // Navigate between dialog screens using step number: 1,2,...
517 $scope.navigateBack = () => {
519 this.fileSelected = false;
522 // Opens a dialog to show the data to be uploaded.
523 // Invoked by the upload button on the bulk user dialog.
524 $scope.confirmUpload = () => {
526 $scope.readValidateFile();
527 // Dialog shows progress
529 templateUrl: 'app/views/users/new-user-dialogs/bulk-user.confirm.html',
531 sizeClass: 'modal-medium',
537 // Invoked by the Cancel button on the confirmation dialog.
538 $scope.cancelUpload = () => {
545 BulkUserModalCtrl.$inject = ['$scope', '$log', '$filter', '$q', 'usersService', 'applicationsService', 'confirmBoxService', 'functionalMenuService', 'ngDialog','$modal'];
546 angular.module('ecompApp').controller('BulkUserModalCtrl', BulkUserModalCtrl);
548 angular.module('ecompApp').directive('fileChange', ['$parse', function($parse){
552 link : function($scope, element, attrs, ngModel) {
553 var attrHandler = $parse(attrs['fileChange']);
554 var handler=function(e) {
555 $scope.$apply(function() {
556 attrHandler($scope, { $event:e, files:e.target.files } );
557 $scope.selectedFile = e.target.files[0].name;
560 element[0].addEventListener('change',handler,false);
565 angular.module('ecompApp').filter('csvToObj',function() {
566 return function(input) {
569 var lines = input.split('\n');
570 // Need 1-based index below
571 for (len = lines.length, i = 1; i <= len; ++i) {
572 // Use 0-based index for array
573 line = lines[i - 1].trim();
574 if (line.length == 0) {
575 // console.log("Skipping blank line");
585 if (o.length !== 2) {
586 // other lengths not valid for upload
591 status: 'Failed to find 2 comma-separated values'
595 // console.log("Valid line: ", val);
600 // leave status undefined, this could be valid.
602 if (o[0].toLowerCase() === 'orgUserId') {
603 // not valid for upload, so set status
604 entry.status = 'Header';
606 else if (o[0].trim() == '' || o[1].trim() == '') {
607 // defend against line with only a single comma etc.
608 entry.status = 'Failed to find 2 non-empty values';