Profile settings page

-To set the profile by setting password.
-This commit adds a profile page which allows the user to change their
password.
In the future, the profile page will also contain user settings like
language and timezone.

The API called to change the user's
password is '/redfish/v1/AccountService/Accounts/<userName>'

Signed-off-by: Sukanya Pandey <sukapan1@in.ibm.com>
Change-Id: Ie54a54beff8c85bc9ac5af21c35edc481b34cf44
diff --git a/src/assets/styles/vendor-overrides/bootstrap/_dropdown.scss b/src/assets/styles/vendor-overrides/bootstrap/_dropdown.scss
index 0eb310f..c7d3954 100644
--- a/src/assets/styles/vendor-overrides/bootstrap/_dropdown.scss
+++ b/src/assets/styles/vendor-overrides/bootstrap/_dropdown.scss
@@ -9,11 +9,12 @@
   }
 }
 
+// Adding component style to global stylesheet because
+// single-file component scoped styles aren't
+// being applied to dynamically appended elements
+// The overflow menu should be above the table
+
 .table-filter {
-  // Adding component style to global stylesheet because
-  // single-file component scoped styles aren't
-  // being applied to dynamically appended elements
-  // The overflow menu should be above the table
   .dropdown-menu {
     z-index: $zindex-dropdown + 1;
     padding: 0;
diff --git a/src/components/AppHeader/AppHeader.vue b/src/components/AppHeader/AppHeader.vue
index a755a62..39d52b8 100644
--- a/src/components/AppHeader/AppHeader.vue
+++ b/src/components/AppHeader/AppHeader.vue
@@ -43,11 +43,19 @@
               <icon-renew />
             </b-button>
           </li>
-          <li>
-            <b-button id="app-header-logout" variant="link" @click="logout">
-              {{ $t('appHeader.logOut') }}
-              <icon-avatar />
-            </b-button>
+          <li class="nav-item">
+            <b-dropdown id="app-header-user" variant="link" right>
+              <template v-slot:button-content>
+                <icon-avatar />
+                {{ username }}
+              </template>
+              <b-dropdown-item to="/profile-settings"
+                >{{ $t('appHeader.profileSettings') }}
+              </b-dropdown-item>
+              <b-dropdown-item @click="logout">{{
+                $t('appHeader.logOut')
+              }}</b-dropdown-item>
+            </b-dropdown>
           </li>
         </b-navbar-nav>
       </b-navbar>
@@ -110,6 +118,9 @@
         default:
           return 'secondary';
       }
+    },
+    username() {
+      return this.$store.getters['global/username'];
     }
   },
   created() {
@@ -142,64 +153,71 @@
 };
 </script>
 
-<style lang="scss" scoped>
+<style lang="scss">
 @import 'src/assets/styles/helpers';
 
-.link-skip-nav {
-  position: absolute;
-  top: -60px;
-  left: 0.5rem;
-  z-index: $zindex-popover;
-  transition: $duration--moderate-01 $exit-easing--expressive;
-  &:focus {
-    top: 0.5rem;
-    transition-timing-function: $entrance-easing--expressive;
+.app-header {
+  .link-skip-nav {
+    position: absolute;
+    top: -60px;
+    left: 0.5rem;
+    z-index: $zindex-popover;
+    transition: $duration--moderate-01 $exit-easing--expressive;
+    &:focus {
+      top: 0.5rem;
+      transition-timing-function: $entrance-easing--expressive;
+    }
   }
-}
-.navbar-dark {
-  .navbar-text,
-  .nav-link,
-  .btn-link {
-    color: $white !important;
-    fill: currentColor;
-  }
-}
-
-.nav-item {
-  fill: $light;
-}
-
-.navbar {
-  padding: 0;
-  height: $header-height;
-  overflow: hidden;
-
-  .btn-link {
-    padding: $spacer / 2;
-  }
-}
-
-.navbar-nav {
-  padding: 0 $spacer;
-}
-
-.nav-trigger {
-  fill: $light;
-  width: $header-height;
-  height: $header-height;
-  transition: none;
-
-  svg {
-    margin: 0;
+  .navbar-dark {
+    .navbar-text,
+    .nav-link,
+    .btn-link {
+      color: $white !important;
+      fill: currentColor;
+    }
   }
 
-  &:hover {
+  .nav-item {
     fill: $light;
-    background-color: $dark;
   }
 
-  @include media-breakpoint-up($responsive-layout-bp) {
-    display: none;
+  .navbar {
+    padding: 0;
+    height: $header-height;
+
+    .btn-link {
+      padding: $spacer / 2;
+    }
+  }
+
+  .navbar-nav {
+    padding: 0 $spacer;
+  }
+
+  .nav-trigger {
+    fill: $light;
+    width: $header-height;
+    height: $header-height;
+    transition: none;
+
+    svg {
+      margin: 0;
+    }
+
+    &:hover {
+      fill: $light;
+      background-color: $dark;
+    }
+
+    @include media-breakpoint-up($responsive-layout-bp) {
+      display: none;
+    }
+  }
+
+  .dropdown {
+    .dropdown-menu {
+      margin-top: 7px;
+    }
   }
 }
 </style>
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index 1ccc330..84a2a60 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -64,6 +64,7 @@
     "health": "Health",
     "logOut": "Log out",
     "power": "Power",
+    "profileSettings": "@:appPageTitle.profileSettings",
     "refresh": "Refresh",
     "skipToContent": "Skip to content"
   },
@@ -98,6 +99,7 @@
     "managePowerUsage": "Manage power usage",
     "networkSettings": "Network settings",
     "overview": "Overview",
+    "profileSettings":"Profile settings",
     "rebootBmc": "Reboot BMC",
     "sensors": "Sensors",
     "serverLed": "Server LED",
@@ -299,6 +301,15 @@
       "solConsole": "Serial over LAN console"
     }
   },
+  "profileSettings": {
+    "changePassword": "Change password",
+    "confirmPassword": "Confirm new password",
+    "newPassword": "New password",
+    "newPassLabelTextInfo": "Password must be between %{min} - %{max} characters",
+    "passwordsDoNotMatch": "Passwords do not match",
+    "profileInfoTitle": "Profile information",
+    "username": "Username"
+  },
   "pageManagePowerUsage": {
     "description": "Set a power cap to keep power consumption at or below the specified value in watts",
     "powerCapLabel": "Power cap value (in watts)",
diff --git a/src/router/index.js b/src/router/index.js
index f67d5ee..22662d7 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -24,6 +24,14 @@
         }
       },
       {
+        path: '/profile-settings',
+        name: 'profile-settings',
+        component: () => import('@/views/ProfileSettings'),
+        meta: {
+          title: 'appPageTitle.profileSettings'
+        }
+      },
+      {
         path: '/health/event-logs',
         name: 'event-logs',
         component: () => import('@/views/Health/EventLogs'),
diff --git a/src/store/modules/Authentication/AuthenticanStore.js b/src/store/modules/Authentication/AuthenticanStore.js
index 7a0c5ba..407c2b5 100644
--- a/src/store/modules/Authentication/AuthenticanStore.js
+++ b/src/store/modules/Authentication/AuthenticanStore.js
@@ -23,6 +23,7 @@
     },
     logout() {
       Cookies.remove('XSRF-TOKEN');
+      localStorage.removeItem('storedUsername');
     }
   },
   actions: {
diff --git a/src/store/modules/GlobalStore.js b/src/store/modules/GlobalStore.js
index 42e9e2b..55b0796 100644
--- a/src/store/modules/GlobalStore.js
+++ b/src/store/modules/GlobalStore.js
@@ -31,19 +31,22 @@
   state: {
     bmcTime: null,
     hostStatus: 'unreachable',
-    languagePreference: localStorage.getItem('storedLanguage') || 'en-US'
+    languagePreference: localStorage.getItem('storedLanguage') || 'en-US',
+    username: localStorage.getItem('storedUsername')
   },
   getters: {
     hostStatus: state => state.hostStatus,
     bmcTime: state => state.bmcTime,
-    languagePreference: state => state.languagePreference
+    languagePreference: state => state.languagePreference,
+    username: state => state.username
   },
   mutations: {
     setBmcTime: (state, bmcTime) => (state.bmcTime = bmcTime),
     setHostStatus: (state, hostState) =>
       (state.hostStatus = hostStateMapper(hostState)),
     setLanguagePreference: (state, language) =>
-      (state.languagePreference = language)
+      (state.languagePreference = language),
+    setUsername: (state, username) => (state.username = username)
   },
   actions: {
     async getBmcTime({ commit }) {
diff --git a/src/views/Login/Login.vue b/src/views/Login/Login.vue
index 4d8f248..3993800 100644
--- a/src/views/Login/Login.vue
+++ b/src/views/Login/Login.vue
@@ -138,6 +138,8 @@
         .then(() => this.$router.push('/'))
         .then(() => {
           localStorage.setItem('storedLanguage', i18n.locale);
+          localStorage.setItem('storedUsername', username);
+          this.$store.commit('global/setUsername', username);
           this.$store.commit('global/setLanguagePreference', i18n.locale);
         })
         .catch(error => console.log(error))
diff --git a/src/views/ProfileSettings/ProfileSettings.vue b/src/views/ProfileSettings/ProfileSettings.vue
new file mode 100644
index 0000000..df74b4b
--- /dev/null
+++ b/src/views/ProfileSettings/ProfileSettings.vue
@@ -0,0 +1,162 @@
+<template>
+  <b-container fluid="xl">
+    <page-title />
+
+    <b-row>
+      <b-col md="8" lg="8" xl="6">
+        <page-section :section-title="$t('profileSettings.profileInfoTitle')">
+          <dl>
+            <dt>{{ $t('profileSettings.username') }}</dt>
+            <dd>
+              {{ username }}
+            </dd>
+          </dl>
+        </page-section>
+      </b-col>
+    </b-row>
+
+    <b-form @submit.prevent="submitForm">
+      <b-row>
+        <b-col sm="8" md="6" xl="3">
+          <page-section :section-title="$t('profileSettings.changePassword')">
+            <b-form-group
+              id="input-group-1"
+              :label="$t('profileSettings.newPassword')"
+              label-for="input-1"
+            >
+              <b-form-text id="password-help-block">
+                {{
+                  $t('pageLocalUserManagement.modal.passwordMustBeBetween', {
+                    min: passwordRequirements.minLength,
+                    max: passwordRequirements.maxLength
+                  })
+                }}
+              </b-form-text>
+              <input-password-toggle>
+                <b-form-input
+                  id="password"
+                  v-model="form.newPassword"
+                  type="password"
+                  aria-describedby="password-help-block"
+                  :state="getValidationState($v.form.newPassword)"
+                  @input="$v.form.newPassword.$touch()"
+                />
+                <b-form-invalid-feedback role="alert">
+                  <template v-if="!$v.form.newPassword.required">
+                    {{ $t('global.form.fieldRequired') }}
+                  </template>
+                  <template
+                    v-if="
+                      !$v.form.newPassword.minLength ||
+                        !$v.form.newPassword.maxLength
+                    "
+                  >
+                    {{
+                      $t('profileSettings.newPassLabelTextInfo', {
+                        min: passwordRequirements.minLength,
+                        max: passwordRequirements.maxLength
+                      })
+                    }}
+                  </template>
+                </b-form-invalid-feedback>
+              </input-password-toggle>
+            </b-form-group>
+            <b-form-group
+              id="input-group-2"
+              :label="$t('profileSettings.confirmPassword')"
+              label-for="input-2"
+            >
+              <input-password-toggle>
+                <b-form-input
+                  id="password-confirmation"
+                  v-model="form.confirmPassword"
+                  type="password"
+                  :state="getValidationState($v.form.confirmPassword)"
+                  @input="$v.form.confirmPassword.$touch()"
+                />
+                <b-form-invalid-feedback role="alert">
+                  <template v-if="!$v.form.confirmPassword.required">
+                    {{ $t('global.form.fieldRequired') }}
+                  </template>
+                  <template v-else-if="!$v.form.confirmPassword.sameAsPassword">
+                    {{ $t('profileSettings.passwordsDoNotMatch') }}
+                  </template>
+                </b-form-invalid-feedback>
+              </input-password-toggle>
+            </b-form-group>
+          </page-section>
+        </b-col>
+      </b-row>
+      <b-button variant="primary" type="submit">
+        {{ $t('global.action.save') }}
+      </b-button>
+    </b-form>
+  </b-container>
+</template>
+
+<script>
+import PageTitle from '@/components/Global/PageTitle';
+import PageSection from '@/components/Global/PageSection';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+import InputPasswordToggle from '@/components/Global/InputPasswordToggle';
+import {
+  maxLength,
+  minLength,
+  required,
+  sameAs
+} from 'vuelidate/lib/validators';
+
+export default {
+  name: 'ProfileSettings',
+  components: { PageTitle, PageSection, InputPasswordToggle },
+  mixins: [BVToastMixin, VuelidateMixin],
+  data() {
+    return {
+      passwordRequirements: {
+        minLength: 8,
+        maxLength: 20
+      },
+      form: {
+        newPassword: '',
+        confirmPassword: ''
+      }
+    };
+  },
+  validations() {
+    return {
+      form: {
+        newPassword: {
+          required,
+          minLength: minLength(this.passwordRequirements.minLength),
+          maxLength: maxLength(this.passwordRequirements.maxLength)
+        },
+        confirmPassword: {
+          required,
+          sameAsPassword: sameAs('newPassword')
+        }
+      }
+    };
+  },
+  computed: {
+    username() {
+      return this.$store.getters['global/username'];
+    }
+  },
+  methods: {
+    submitForm() {
+      this.$v.$touch();
+      if (this.$v.$invalid) return;
+      let userData = {
+        originalUsername: this.username,
+        password: this.form.newPassword
+      };
+
+      this.$store
+        .dispatch('localUsers/updateUser', userData)
+        .then(message => this.successToast(message))
+        .catch(({ message }) => this.errorToast(message));
+    }
+  }
+};
+</script>
diff --git a/src/views/ProfileSettings/index.js b/src/views/ProfileSettings/index.js
new file mode 100644
index 0000000..d6589c7
--- /dev/null
+++ b/src/views/ProfileSettings/index.js
@@ -0,0 +1,2 @@
+import ProfileSettings from './ProfileSettings.vue';
+export default ProfileSettings;