Add local user manual unlock

Enables manual unlock from the GUI when a user is locked out
due to failed login attempts above allowed threshold.

Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: I63e28a4d6feed9eb6d4d09c0431d31e7bd6924c2
diff --git a/app/common/services/api-utils.js b/app/common/services/api-utils.js
index 90a8cb3..ebc2956 100644
--- a/app/common/services/api-utils.js
+++ b/app/common/services/api-utils.js
@@ -618,7 +618,7 @@
             data: data
           });
         },
-        updateUser: function(user, newUser, passwd, role, enabled) {
+        updateUser: function(user, newUser, passwd, role, enabled, locked) {
           var data = {};
           if ((newUser !== undefined) && (newUser != null)) {
             data['UserName'] = newUser;
@@ -632,6 +632,9 @@
           if ((passwd !== undefined) && (passwd != null)) {
             data['Password'] = passwd;
           }
+          if ((locked !== undefined) && (locked !== null)) {
+            data['Locked'] = locked
+          }
           return $http({
             method: 'PATCH',
             url: DataService.getHost() +
diff --git a/app/common/styles/elements/alerts.scss b/app/common/styles/elements/alerts.scss
index 947320c..433db6d 100644
--- a/app/common/styles/elements/alerts.scss
+++ b/app/common/styles/elements/alerts.scss
@@ -22,3 +22,37 @@
     margin-bottom: 0;
   }
 }
+
+.notification-banner {
+  padding: 8px 10px;
+  margin-bottom: 24px;
+  border-style: solid;
+  border-width: 1px;
+}
+
+.notification-banner__text {
+  font-size: 0.9em;
+  margin-bottom: 0;
+  line-height: 1.2;
+}
+
+.notification-banner--information {
+  background-color: $background-02;
+  border-color: $border-color-01;
+}
+
+.notification-banner--warning {
+  background-color: $accent-03--03;
+  border-color: $accent-03--02;
+  .notification-banner__text {
+    &::before {
+      content: '';
+      display: inline-block;
+      width: 18px;
+      height: 18px;
+      vertical-align: bottom;
+      margin-right: 0.5em;
+      background-image: url(../assets/images/icon-warning.svg);
+    }
+  }
+}
\ No newline at end of file
diff --git a/app/users/controllers/user-accounts-controller.js b/app/users/controllers/user-accounts-controller.js
index 11ba13d..3d63d69 100644
--- a/app/users/controllers/user-accounts-controller.js
+++ b/app/users/controllers/user-accounts-controller.js
@@ -34,6 +34,18 @@
       }
 
       /**
+       * Returns lockout method based on the lockout duration property
+       * If the lockoutDuration is greater than 0 the lockout method
+       * is automatic otherwise the lockout method is manual
+       * @param {number} lockoutDuration
+       * @returns {number} : returns the account lockout method
+       *                     1(automatic) / 0(manual)
+       */
+      function mapLockoutMethod(lockoutDuration) {
+        return lockoutDuration > 0 ? 1 : 0;
+      }
+
+      /**
        * API call to get all user accounts
        */
       function getLocalUsers() {
@@ -103,9 +115,12 @@
       /**
        * API call to update existing user
        */
-      function updateUser(originalUsername, username, password, role, enabled) {
+      function updateUser(
+          originalUsername, username, password, role, enabled, locked) {
         $scope.loading = true;
-        APIUtils.updateUser(originalUsername, username, password, role, enabled)
+        APIUtils
+            .updateUser(
+                originalUsername, username, password, role, enabled, locked)
             .then(() => {
               getLocalUsers();
               toastService.success('User has been updated successfully.')
@@ -176,11 +191,9 @@
               ariaLabelledBy: 'dialog_label',
               controllerAs: 'modalCtrl',
               controller: function() {
-                // If AccountLockoutDuration is not 0 the lockout
-                // method is automatic. If AccountLockoutDuration is 0 the
-                // lockout method is manual
-                const lockoutMethod =
-                    $scope.accountSettings.AccountLockoutDuration ? 1 : 0;
+                const lockoutMethod = mapLockoutMethod(
+                    $scope.accountSettings.AccountLockoutDuration);
+
                 this.settings = {};
                 this.settings.maxLogin =
                     $scope.accountSettings.AccountLockoutThreshold;
@@ -252,7 +265,11 @@
                 this.user.accountStatus = status;
                 this.user.username = newUser ? '' : user.UserName;
                 this.user.privilege = newUser ? '' : user.RoleId;
+                this.user.locked = newUser ? null : user.Locked;
 
+                this.manualUnlockProperty = false;
+                this.automaticLockout = mapLockoutMethod(
+                    $scope.accountSettings.AccountLockoutDuration);
                 this.privilegeRoles = $scope.userRoles;
                 this.existingUsernames = existingUsernames;
                 this.minPasswordLength = $scope.accountSettings ?
@@ -281,10 +298,14 @@
                                  form.accountStatus1.$pristine) ?
                     null :
                     form.accountStatus.$modelValue;
+                const locked = (form.lock && form.lock.$dirty) ?
+                    form.lock.$modelValue :
+                    null;
 
                 if (!newUser) {
                   updateUser(
-                      originalUsername, username, password, role, enabled);
+                      originalUsername, username, password, role, enabled,
+                      locked);
                 } else {
                   createUser(
                       username, password, role, form.accountStatus.$modelValue);
diff --git a/app/users/controllers/user-accounts-modal-user.html b/app/users/controllers/user-accounts-modal-user.html
index 7b380be..4e646b1 100644
--- a/app/users/controllers/user-accounts-modal-user.html
+++ b/app/users/controllers/user-accounts-modal-user.html
@@ -9,6 +9,29 @@
   </div>
   <form name="form">
     <div class="modal-body">
+      <!-- Manual unlock -->
+      <div class="row" ng-if="modalCtrl.user.locked && !modalCtrl.automaticLockout">
+        <div class="column medium-9">
+          <div class="notification-banner"
+               aria-live="polite"
+               ng-class="{'notification-banner--warning': !form.lock.$dirty,
+                          'notification-banner--information': form.lock.$dirty}">
+            <p class="notification-banner__text" ng-if="!form.lock.$dirty">Account locked</p>
+            <p class="notification-banner__text" ng-if="form.lock.$dirty">Click "Save" to unlock account</p>
+          </div>
+        </div>
+        <div class="column medium-3">
+          <input
+            type="hidden"
+            name="lock"
+            ng-model="modalCtrl.manualUnlockProperty"
+            value="false">
+          <button class="btn btn-primary"
+                  type="button"
+                  ng-click="form.lock.$setDirty()"
+                  ng-disabled="form.lock.$dirty">Unlock</button>
+        </div>
+      </div>
       <div class="row">
         <div class="column medium-6">
             <!-- Account Status -->