Add account settings to local user page

Adds ability to change account LockoutThreshold and
LockoutDuration properties from the GUI.

Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: Ieeb75aa83c07b3de840bccdfc28e2d6e87512e2e
diff --git a/src/locales/en.json b/src/locales/en.json
index dd3d588..f392f4e 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -1,7 +1,9 @@
 {
   "global": {
     "formField": {
-      "validator": "Field required"
+      "fieldRequired": "Field required",
+      "valueMustBeBetween": "Value must be between %{min} – %{max}",
+      "mustBeAtLeast": "Must be at least %{value}"
     },
     "on": "on",
     "off": "off",
@@ -13,6 +15,7 @@
       "confirm": "Confirm",
       "cancel": "Cancel",
       "delete": "Delete",
+      "save": "Save",
       "selected": "Selected"
     },
     "response": {
@@ -49,11 +52,11 @@
     },
     "password": {
       "label": "Password",
-      "validator": "@:global.formField.validator"
+      "validator": "@:global.formField.fieldRequired"
     },
     "username": {
       "label": "Username",
-      "validator": "@:global.formField.validator"
+      "validator": "@:global.formField.fieldRequired"
     }
   },
   "overview": {
@@ -103,6 +106,7 @@
     }
   },
   "localUserManagement": {
+    "accountPolicySettings": "Account policy settings",
     "tableActions": {
       "delete": "@:global.actions.delete",
       "enable": "Enable",
@@ -114,7 +118,16 @@
       "successEnableUsers": "Successfully enabled %{count} user. | Successfully enabled %{count} users.",
       "errorEnableUsers": "Error enabling %{count} user. | Error enabling %{count} users.",
       "successDisableUsers": "Successfully disabled %{count} user. | Successfully disabled %{count} users.",
-      "errorDisableUsers": "Error disabling %{count} user. | Error disabling %{count} users."
+      "errorDisableUsers": "Error disabling %{count} user. | Error disabling %{count} users.",
+      "successSaveSettings": "Successfully saved account settings.",
+      "errorSaveSettings": "Error saving account settings."
+    },
+    "modals": {
+      "manual": "Manual",
+      "automaticAfterTimeout": "Automatic after timeout",
+      "timeoutDurationSeconds": "Timeout duration (seconds)",
+      "maxFailedLoginAttempts": "Max failed login attempts",
+      "userUnlockMethod": "User unlock method"
     }
   }
 }
\ No newline at end of file
diff --git a/src/store/modules/AccessControl/LocalUserMangementStore.js b/src/store/modules/AccessControl/LocalUserMangementStore.js
index 67c3a1e..7ad3ff5 100644
--- a/src/store/modules/AccessControl/LocalUserMangementStore.js
+++ b/src/store/modules/AccessControl/LocalUserMangementStore.js
@@ -257,6 +257,33 @@
             return toastMessages;
           })
         );
+    },
+    async saveAccountSettings(
+      { dispatch },
+      { lockoutThreshold, lockoutDuration }
+    ) {
+      const data = {};
+      if (lockoutThreshold !== undefined) {
+        data.AccountLockoutThreshold = lockoutThreshold;
+      }
+      if (lockoutDuration !== undefined) {
+        data.AccountLockoutDuration = lockoutDuration;
+      }
+
+      return await api
+        .patch('/redfish/v1/AccountService', data)
+        //GET new settings to update view
+        .then(() => dispatch('getAccountSettings'))
+        .then(() =>
+          i18n.t('localUserManagement.toastMessages.successSaveSettings')
+        )
+        .catch(error => {
+          console.log(error);
+          const message = i18n.t(
+            'localUserManagement.toastMessages.errorSaveSettings'
+          );
+          throw new Error(message);
+        });
     }
   }
 };
diff --git a/src/views/AccessControl/LocalUserManagement/LocalUserManagement.vue b/src/views/AccessControl/LocalUserManagement/LocalUserManagement.vue
index 97b00e4..ee2ec43 100644
--- a/src/views/AccessControl/LocalUserManagement/LocalUserManagement.vue
+++ b/src/views/AccessControl/LocalUserManagement/LocalUserManagement.vue
@@ -75,7 +75,7 @@
       </b-col>
     </b-row>
     <!-- Modals -->
-    <modal-settings :settings="settings" />
+    <modal-settings :settings="settings" @ok="saveAccountSettings" />
     <modal-user
       :user="activeUser"
       :password-requirements="passwordRequirements"
@@ -216,11 +216,7 @@
         });
     },
     initModalSettings() {
-      if (this.settings) {
-        this.$bvModal.show('modal-settings');
-      } else {
-        // fetch settings then show modal
-      }
+      this.$bvModal.show('modal-settings');
     },
     saveUser({ isNewUser, userData }) {
       if (isNewUser) {
@@ -288,6 +284,12 @@
         default:
           break;
       }
+    },
+    saveAccountSettings(settings) {
+      this.$store
+        .dispatch('localUsers/saveAccountSettings', settings)
+        .then(message => this.successToast(message))
+        .catch(({ message }) => this.errorToast(message));
     }
   }
 };
diff --git a/src/views/AccessControl/LocalUserManagement/ModalSettings.vue b/src/views/AccessControl/LocalUserManagement/ModalSettings.vue
index afe2d95..2e41b29 100644
--- a/src/views/AccessControl/LocalUserManagement/ModalSettings.vue
+++ b/src/views/AccessControl/LocalUserManagement/ModalSettings.vue
@@ -1,14 +1,195 @@
 <template>
-  <b-modal id="modal-settings" title="Account policy settings"> </b-modal>
+  <b-modal
+    id="modal-settings"
+    ref="modal"
+    :title="$t('localUserManagement.accountPolicySettings')"
+    :ok-title="$t('global.actions.save')"
+    @ok="onOk"
+    @hidden="resetForm"
+  >
+    <b-form novalidate @submit="handleSubmit">
+      <b-container>
+        <b-row>
+          <b-col>
+            <b-form-group
+              :label="$t('localUserManagement.modals.maxFailedLoginAttempts')"
+              label-for="lockout-threshold"
+            >
+              <b-form-text id="lockout-threshold-help-block">
+                {{
+                  $t('global.formField.valueMustBeBetween', {
+                    min: 0,
+                    max: 65535
+                  })
+                }}
+              </b-form-text>
+              <b-form-input
+                id="lockout-threshold"
+                v-model.number="form.lockoutThreshold"
+                type="number"
+                aria-describedby="lockout-threshold-help-block"
+                :state="getValidationState($v.form.lockoutThreshold)"
+                @input="$v.form.lockoutThreshold.$touch()"
+              />
+              <b-form-invalid-feedback role="alert">
+                <template v-if="!$v.form.lockoutThreshold.required">
+                  {{ $t('global.formField.fieldRequired') }}
+                </template>
+                <template
+                  v-if="
+                    !$v.form.lockoutThreshold.minLength ||
+                      !$v.form.lockoutThreshold.maxLength
+                  "
+                >
+                  {{
+                    $t('global.formField.valueMustBeBetween', {
+                      min: 0,
+                      max: 65535
+                    })
+                  }}
+                </template>
+              </b-form-invalid-feedback>
+            </b-form-group>
+          </b-col>
+          <b-col>
+            <b-form-group
+              :label="$t('localUserManagement.modals.userUnlockMethod')"
+            >
+              <b-form-radio
+                v-model="form.unlockMethod"
+                name="unlock-method"
+                class="mb-2"
+                :value="0"
+                @input="$v.form.unlockMethod.$touch()"
+              >
+                {{ $t('localUserManagement.modals.manual') }}
+              </b-form-radio>
+              <b-form-radio
+                v-model="form.unlockMethod"
+                name="unlock-method"
+                :value="1"
+                @input="$v.form.unlockMethod.$touch()"
+              >
+                {{ $t('localUserManagement.modals.automaticAfterTimeout') }}
+              </b-form-radio>
+              <div class="mt-3 ml-4">
+                <b-form-text id="lockout-duration-help-block">
+                  {{ $t('localUserManagement.modals.timeoutDurationSeconds') }}
+                </b-form-text>
+                <b-form-input
+                  v-model.number="form.lockoutDuration"
+                  aria-describedby="lockout-duration-help-block"
+                  type="number"
+                  :state="getValidationState($v.form.lockoutDuration)"
+                  :readonly="$v.form.unlockMethod.$model === 0"
+                  @input="$v.form.lockoutDuration.$touch()"
+                />
+                <b-form-invalid-feedback role="alert">
+                  <template v-if="!$v.form.lockoutDuration.required">
+                    {{ $t('global.formField.fieldRequired') }}
+                  </template>
+                  <template v-else-if="!$v.form.lockoutDuration.minvalue">
+                    {{ $t('global.formField.mustBeAtLeast', { value: 1 }) }}
+                  </template>
+                </b-form-invalid-feedback>
+              </div>
+            </b-form-group>
+          </b-col>
+        </b-row>
+      </b-container>
+    </b-form>
+  </b-modal>
 </template>
 
 <script>
+import VuelidateMixin from '../../../components/Mixins/VuelidateMixin.js';
+import {
+  required,
+  requiredIf,
+  minValue,
+  maxValue
+} from 'vuelidate/lib/validators';
+
 export default {
+  mixins: [VuelidateMixin],
   props: {
     settings: {
       type: Object,
       required: true
     }
+  },
+  data() {
+    return {
+      form: {
+        lockoutThreshold: 0,
+        unlockMethod: 0,
+        lockoutDuration: null
+      }
+    };
+  },
+  watch: {
+    settings: function({ lockoutThreshold, lockoutDuration }) {
+      this.form.lockoutThreshold = lockoutThreshold;
+      this.form.unlockMethod = lockoutDuration ? 1 : 0;
+      this.form.lockoutDuration = lockoutDuration ? lockoutDuration : null;
+    }
+  },
+  validations: {
+    form: {
+      lockoutThreshold: {
+        minValue: minValue(0),
+        maxValue: maxValue(65535),
+        required
+      },
+      unlockMethod: { required },
+      lockoutDuration: {
+        minValue: function(value) {
+          return this.form.unlockMethod === 0 || value > 0;
+        },
+        required: requiredIf(function() {
+          return this.form.unlockMethod === 1;
+        })
+      }
+    }
+  },
+  methods: {
+    handleSubmit() {
+      this.$v.$touch();
+      if (this.$v.$invalid) return;
+
+      let lockoutThreshold;
+      let lockoutDuration;
+      if (this.$v.form.lockoutThreshold.$dirty) {
+        lockoutThreshold = this.form.lockoutThreshold;
+      }
+      if (this.$v.form.unlockMethod.$dirty) {
+        lockoutDuration = this.form.unlockMethod
+          ? this.form.lockoutDuration
+          : 0;
+      }
+
+      this.$emit('ok', { lockoutThreshold, lockoutDuration });
+      this.closeModal();
+    },
+    onOk(bvModalEvt) {
+      // prevent modal close
+      bvModalEvt.preventDefault();
+      this.handleSubmit();
+    },
+    closeModal() {
+      this.$nextTick(() => {
+        this.$refs.modal.hide();
+      });
+    },
+    resetForm() {
+      // Reset form models
+      this.form.lockoutThreshold = this.settings.lockoutThreshold;
+      this.form.unlockMethod = this.settings.lockoutDuration ? 1 : 0;
+      this.form.lockoutDuration = this.settings.lockoutDuration
+        ? this.settings.lockoutDuration
+        : null;
+      this.$v.$reset(); // clear validations
+    }
   }
 };
 </script>