Create profile settings page

Adding a profile settings page so readonly and operator
roles are able to change their own password.

Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: Iee9536255ad47f4df4af8746c1e01da37c407f2b
diff --git a/app/assets/icons/icon-avatar.svg b/app/assets/icons/icon-avatar.svg
new file mode 100644
index 0000000..665af99
--- /dev/null
+++ b/app/assets/icons/icon-avatar.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M16 2a14 14 0 1014 14A14 14 0 0016 2zm-6 24.38v-2A3.22 3.22 0 0113 21h6a3.22 3.22 0 013 3.39v2a11.92 11.92 0 01-12 0zm14-1.46v-.61A5.21 5.21 0 0019 19h-6a5.2 5.2 0 00-5 5.31v.59a12 12 0 1116 0z"/><path d="M16 7a5 5 0 105 5 5 5 0 00-5-5zm0 8a3 3 0 113-3 3 3 0 01-3 3z"/><path data-name="&lt;Transparent Rectangle&gt;" fill="none" d="M0 0h32v32H0z"/></svg>
\ No newline at end of file
diff --git a/app/common/directives/app-header.html b/app/common/directives/app-header.html
index bf4fb8f..ec03874 100644
--- a/app/common/directives/app-header.html
+++ b/app/common/directives/app-header.html
@@ -2,7 +2,16 @@
   <!-- HEADER -->
   <div class="header__info-section">
     <span class="header__title">OpenBMC</span>
-    <a href="" class="header__logout" ng-click="logout()">Log out</a>
+    <div class="header__actions" uib-dropdown>
+      <button id="user-actions" type="button" uib-dropdown-toggle>
+        <icon class="icon-user" file="icon-avatar.svg"></icon>
+        {{username}}
+      </button>
+      <ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="user-actions">
+        <li role="menuitem"><a href="#/profile-settings" class="btn">Profile settings</a></li>
+        <li role="menuitem"><button ng-click="logout()" type="button" class="btn">Log out</button></li>
+      </ul>
+    </div>
   </div>
   <div class="header__functions-section">
     <div class="logo__wrapper">
diff --git a/app/common/directives/app-header.js b/app/common/directives/app-header.js
index 98d210f..df39772 100644
--- a/app/common/directives/app-header.js
+++ b/app/common/directives/app-header.js
@@ -14,6 +14,7 @@
           function(
               $rootScope, $scope, dataService, userModel, $location, $route) {
             $scope.dataService = dataService;
+            $scope.username = '';
 
             try {
               // Create a secure websocket with URL as /subscribe
@@ -118,6 +119,7 @@
               $scope.loadNetworkInfo();
               $scope.loadServerHealth();
               $scope.loadSystemName();
+              $scope.username = dataService.getUser();
             }
 
             loadData();
diff --git a/app/common/styles/base/forms.scss b/app/common/styles/base/forms.scss
index c775c48..6699d44 100644
--- a/app/common/styles/base/forms.scss
+++ b/app/common/styles/base/forms.scss
@@ -1,3 +1,4 @@
+.label,
 label,
 legend {
   margin: 0;
diff --git a/app/common/styles/directives/dropdown.scss b/app/common/styles/directives/dropdown.scss
new file mode 100644
index 0000000..0c0add0
--- /dev/null
+++ b/app/common/styles/directives/dropdown.scss
@@ -0,0 +1,5 @@
+.dropdown.open {
+  .dropdown-menu {
+    display: block;
+  }
+}
\ No newline at end of file
diff --git a/app/common/styles/directives/index.scss b/app/common/styles/directives/index.scss
index 5d9de6f..1fcbb65 100644
--- a/app/common/styles/directives/index.scss
+++ b/app/common/styles/directives/index.scss
@@ -1 +1,2 @@
-@import "./app-navigation.scss";
\ No newline at end of file
+@import "./app-navigation.scss";
+@import "./dropdown.scss";
\ No newline at end of file
diff --git a/app/common/styles/layout/header.scss b/app/common/styles/layout/header.scss
index c034c82..b1665ca 100644
--- a/app/common/styles/layout/header.scss
+++ b/app/common/styles/layout/header.scss
@@ -21,35 +21,40 @@
   z-index: 300;
 }
 
-.header__title {
-  margin-left: 1em;
-  display: none;
-  float: left;
-  @include mediaQuery(x-small) {
-    display: inline-block;
-    position: absolute;
-    top: 50%;
-    transform: translateY(-50%);
-  }
-}
-
 .header__info-section {
   position: relative;
   background: $primary-dark;
   color: $primary-light;
-  overflow: hidden;
+  width: 100%;
+  height: 50px;
+  display: flex;
+  justify-content: space-between;
+  .dropdown-menu {
+    left: unset;
+    right: 0;
+    border-radius: 0;
+    font-size: 0.9rem;
+    .btn {
+      color: $primary-dark;
+    }
+  }
+  .dropdown-toggle {
+    color: $primary-light;
+    fill: $primary-light;
+    text-decoration: none;
+    font-weight: 400;
+    margin-right: 0.5rem;
+    height: 50px; //to vertically align in 50px header
+    &::after {
+      display: none; //hiding dropdown caret inserted by bootstrap
+    }
+  }
 }
 
-.header__logout {
-  float: right;
-  color: $primary-light;
-  font-size: 0.9em;
-  text-decoration: none;
-  padding: 1em;
-  font-weight: 400;
-  &:visited {
-    color: $primary-light;
-  }
+.header__title {
+  margin-left: 1rem;
+  display: block;
+  line-height: 50px; //to vertically align in 50px header
 }
 
 .header__functions-section {
diff --git a/app/index.js b/app/index.js
index 57d031b..156fab6 100644
--- a/app/index.js
+++ b/app/index.js
@@ -78,6 +78,9 @@
 import login_index from './login/index.js';
 import login_controller from './login/controllers/login-controller.js';
 
+import profile_settings_index from './profile-settings/index.js';
+import profile_settings_controller from './profile-settings/controllers/profile-settings-controller.js';
+
 import overview_index from './overview/index.js';
 import system_overview_controller from './overview/controllers/system-overview-controller.js';
 
@@ -133,7 +136,7 @@
             // Model resources
             'app.login', 'app.overview', 'app.serverControl',
             'app.serverHealth', 'app.configuration', 'app.accessControl',
-            'app.redfish'
+            'app.redfish', 'app.profileSettings'
           ])
       // Route configuration
       .config([
diff --git a/app/profile-settings/controllers/profile-settings-controller.html b/app/profile-settings/controllers/profile-settings-controller.html
new file mode 100644
index 0000000..365cf7f
--- /dev/null
+++ b/app/profile-settings/controllers/profile-settings-controller.html
@@ -0,0 +1,76 @@
+
+<div class="row column">
+  <div class="page-header">
+    <h1>Profile settings</h1>
+  </div>
+</div>
+<div class="row column">
+  <div class="column medium-12 large-5 xlarge-4">
+    <section class="section">
+      <div class="section-header">
+        <h2 class="section-title">Profile information</h2>
+      </div>
+      <dl>
+        <dt class="label">Username</dt>
+        <dd>{{username}}</dd>
+      </dl>
+    </section>
+  </div>
+</div>
+<div class="row column">
+  <div class="column medium-12 large-5 xlarge-4">
+    <section class="section">
+      <div class="section-header">
+        <h2 class="section-title">Change password</h2>
+      </div>
+      <form name="form">
+        <!-- Password -->
+        <div class="field-group-container">
+          <label for="password">New password</label>
+          <p class="label__helper-text">Password must between <span class="nowrap">{{minPasswordLength}} – {{maxPasswordLength}}</span> characters</p>
+          <input  id="password"
+                  name="password"
+                  type="password"
+                  required
+                  ng-minlength="minPasswordLength"
+                  ng-maxlength="maxPasswordLength"
+                  autocomplete="new-password"
+                  ng-model="password"
+                  password-visibility-toggle/>
+          <div ng-if="form.password.$invalid && form.password.$dirty" class="form__validation-message">
+            <span ng-show="form.password.$error.required">
+              Field is required</span>
+            <span ng-show="form.password.$error.minlength || form.password.$error.maxlength">
+              Length must be between <span class="nowrap">{{minPasswordLength}} – {{maxPasswordLength}}</span> characters</span>
+          </div>
+        </div>
+        <!-- Password confirm -->
+        <div class="field-group-container">
+          <label for="passwordConfirm">Confirm new password</label>
+          <input  id="passwordConfirm"
+                  name="passwordConfirm"
+                  type="password"
+                  required
+                  autocomplete="new-password"
+                  ng-model="passwordConfirm"
+                  password-visibility-toggle
+                  password-confirm
+                  first-password="form.password.$modelValue"/>
+          <div ng-if="form.passwordConfirm.$invalid && form.passwordConfirm.$dirty" class="form__validation-message">
+            <span ng-show="form.passwordConfirm.$error.required">
+              Field is required</span>
+            <span ng-show="form.passwordConfirm.$error.passwordConfirm"
+                  ng-hide="form.passwordConfirm.$error.required">
+              Passwords do not match</span>
+          </div>
+        </div>
+        <!-- Form actions -->
+        <div class="field-group-container">
+          <button class="btn btn-primary" type="submit" ng-click="onSubmit(form)">
+            Change password
+          </button>
+        </div>
+      </form>
+    </section>
+  </div>
+</div>
\ No newline at end of file
diff --git a/app/profile-settings/controllers/profile-settings-controller.js b/app/profile-settings/controllers/profile-settings-controller.js
new file mode 100644
index 0000000..404e055
--- /dev/null
+++ b/app/profile-settings/controllers/profile-settings-controller.js
@@ -0,0 +1,75 @@
+/**
+ * Controller for the profile settings page
+ *
+ * @module app/profile-settings/controllers/index
+ * @exports ProfileSettingsController
+ * @name ProfileSettingsController
+ */
+
+window.angular && (function(angular) {
+  'use strict';
+
+  angular.module('app.profileSettings')
+      .controller('profileSettingsController', [
+        '$scope', 'APIUtils', 'dataService', 'toastService',
+        function($scope, APIUtils, dataService, toastService) {
+          $scope.username;
+          $scope.minPasswordLength;
+          $scope.maxPasswordLength;
+          $scope.password;
+          $scope.passwordConfirm;
+
+          /**
+           * Make API call to update user password
+           * @param {string} password
+           */
+          const updatePassword = function(password) {
+            $scope.loading = true;
+            APIUtils.updateUser($scope.username, null, password)
+                .then(
+                    () => toastService.success(
+                        'Password has been updated successfully.'))
+                .catch((error) => {
+                  console.log(JSON.stringify(error));
+                  toastService.error('Unable to update password.')
+                })
+                .finally(() => {
+                  $scope.password = '';
+                  $scope.passwordConfirm = '';
+                  $scope.form.$setPristine();
+                  $scope.form.$setUntouched();
+                  $scope.loading = false;
+                })
+          };
+
+          /**
+           * API call to get account settings for min/max
+           * password length requirement
+           */
+          const getAllUserAccountProperties = function() {
+            APIUtils.getAllUserAccountProperties().then((accountSettings) => {
+              $scope.minPasswordLength = accountSettings.MinPasswordLength;
+              $scope.maxPasswordLength = accountSettings.MaxPasswordLength;
+            })
+          };
+
+          /**
+           * Callback after form submitted
+           */
+          $scope.onSubmit = function(form) {
+            if (form.$valid) {
+              const password = form.password.$viewValue;
+              updatePassword(password);
+            }
+          };
+
+          /**
+           * Callback after view loaded
+           */
+          $scope.$on('$viewContentLoaded', () => {
+            getAllUserAccountProperties();
+            $scope.username = dataService.getUser();
+          });
+        }
+      ]);
+})(angular);
diff --git a/app/profile-settings/index.js b/app/profile-settings/index.js
new file mode 100644
index 0000000..77947a7
--- /dev/null
+++ b/app/profile-settings/index.js
@@ -0,0 +1,25 @@
+/**
+ * A module for the Profile Settings page
+ *
+ * @module app/profile-settings/index
+ * @exports app/profile-settings/index
+ */
+
+window.angular && (function(angular) {
+  'use strict';
+
+  angular
+      .module('app.profileSettings', ['ngRoute', 'app.common.services'])
+      // Route configuration
+      .config([
+        '$routeProvider',
+        function($routeProvider) {
+          $routeProvider.when('/profile-settings', {
+            'template':
+                require('./controllers/profile-settings-controller.html'),
+            'controller': 'profileSettingsController',
+            authenticated: true
+          })
+        }
+      ]);
+})(window.angular);