Create certificate management page

Displays certificates and the ability to add up to one
of each type of certificate (as limited by the backend
implementation). HTTPS certificate and LDAP client cert
are implemented in this commit, with the ability to add
more types as needed by adding them to the constants.js
CERTIFICATE_TYPES array.

Also provides the ability to replace a certificate once
it is added.

Resolves openbmc/phosphor-webui#43

Tested: loaded onto a witherspoon and able to view and
        replace both the HTTPS certificate and the
        LDAP certificate. GUI only allows to upload an
        LDAP certificate if one doesn't already exist.
        The GUI limits the user to one file per type as
        expected at this time and provides the
        appropriate validation messages. Alert messages
        appear above the table if the certificate is
        expired or within 30 days of expiring.

Change-Id: I345267280ecd3cb257e9304886cde9ebb69b1240
Signed-off-by: beccabroek <beccabroek@gmail.com>
Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
diff --git a/app/configuration/controllers/certificate-controller.html b/app/configuration/controllers/certificate-controller.html
new file mode 100644
index 0000000..6490bb0
--- /dev/null
+++ b/app/configuration/controllers/certificate-controller.html
@@ -0,0 +1,93 @@
+<loader loading="loading"></loader>
+<div id="configuration-cert">
+  <div class="row column">
+    <h1>SSL certificates</h1>
+  </div>
+  <div ng-repeat="certificate in certificates|filter:{isExpiring:true}" class="row column">
+    <div class="small-12 alert alert-warning" role="alert">
+        <div class="icon__warning inline"></div> The uploaded {{certificate.Description}} is expiring in {{getDays(certificate.ValidNotAfter) === 0 ? 'less than one day!' : getDays(certificate.ValidNotAfter) + ' days!'}} Consider replacing it with a new certificate.
+    </div>
+  </div>
+  <div ng-repeat="certificate in certificates|filter:{isExpired:true}" class="row column">
+    <div class="small-12 alert alert-danger" role="alert">
+      <div class="icon__critical inline"></div> The uploaded {{certificate.Description}} has expired! Consider replacing it with a new certificate.
+    </div>
+  </div>
+  <div class="row column">
+    <button type="button" class="btn  btn-tertiary"
+      ng-disabled="availableCertificateTypes.length === 0"
+      ng-click="addCertificateModal=true">
+      <icon file="icon-plus.svg"></icon>
+      Add new certificate
+    </button>
+  </div>
+  <div class="row column">
+    <div class="small-12 certificate__table">
+      <div class="table__row-header">
+        <div class="row column">
+          <div class="certificate__type-header">
+            Certificate
+          </div>
+          <div class="certificate__date-header">
+            Valid from
+          </div>
+          <div class="certificate__status-header">
+          </div>
+          <div class="certificate__date-header">
+            Valid until
+          </div>
+        </div>
+      </div>
+      <div ng-if="certificates.length < 1" class="empty__logs" >There have been no certificates added.</div>
+      <div ng-repeat="certificate in certificates">
+        <certificate cert="certificate" reload="loadCertificates()")></certificate>
+      </div>
+    </div>
+  </div>
+  <section class="modal add__certificate__modal" aria-hidden="true" role="dialog" ng-class="{'active': addCertificateModal}">
+    <div class="page-header ">
+      <h3>Add new certificate</h3>
+    </div>
+    <div class="modal__content">
+      <form name="add__cert__form" id="add__cert__form" ng-class="{'submitted': submitted}">
+        <div class="row column">
+          <div class="small-12">
+          <label for="cert__type">Certificate type</label>
+          <select id="cert__type" name="cert__type" ng-model="newCertificate.selectedType" required>
+            <option class="courier-bold" ng-value="">Select an option</option>
+            <option class="courier-bold" ng-value="type" ng-repeat="type in availableCertificateTypes">{{type.Description}}</option>
+          </select>
+          <div ng-messages="add__cert__form.cert__type.$error" class="form-error" ng-class="{'visible' : add__cert__form.cert__type.$touched || submitted}">
+            <p ng-message="required">Field is required</p>
+          </div>
+          </div>
+        </div>
+        <div class="row column">
+          <div class="small-12">
+            <label class ="select__new-label" for="upload_cert_new">Certificate file</label>
+          </div>
+          <div class="row column file__upload">
+            <label for='upload_cert_new'>
+              <input name="upload_cert_new" id="upload_cert_new" type="file" file="newCertificate.file" class="hide"/>
+              <span class="btn btn-secondary select__new-button">Choose file</span>
+            </label>
+          </div>
+          <div class="row column">
+            <div ng-if="newCertificate.file" class="small-7 file__name">
+              <span>{{newCertificate.file.name}}</span>
+              <icon file="icon-close.svg"
+                ng-if="newCertificate.file.name"
+                ng-click="newCertificate.file = '';"
+                class="float-right"></icon>
+            </div>
+          </div>
+        </div>
+      </form>
+    </div>
+    <div class="modal__button-wrapper">
+      <button class="btn btn-secondary" ng-click="addCertificateModal = false; newCertificate={};add__cert__form.$setUntouched();">Cancel</button>
+      <button class="btn btn-primary" ng-class="{'disabled' : add__cert__form.$invalid || !newCertificate.file}" ng-click="submitted = true; uploadCertificate();">Save</button>
+    </div>
+  </section>
+  <div class="modal-overlay" tabindex="-1" ng-class="{'active': addCertificateModal}"></div>
+</div>
diff --git a/app/configuration/controllers/certificate-controller.js b/app/configuration/controllers/certificate-controller.js
new file mode 100644
index 0000000..7fa42a1
--- /dev/null
+++ b/app/configuration/controllers/certificate-controller.js
@@ -0,0 +1,121 @@
+/**
+ * Controller for Certificate Management
+ *
+ * @module app/configuration
+ * @exports certificateController
+ * @name certificateController
+ */
+
+window.angular && (function(angular) {
+  'use strict';
+
+  angular.module('app.configuration').controller('certificateController', [
+    '$scope', 'APIUtils', '$q', 'Constants', 'toastService',
+    function($scope, APIUtils, $q, Constants, toastService) {
+      $scope.loading = false;
+      $scope.certificates = [];
+      $scope.availableCertificateTypes = [];
+      $scope.addCertificateModal = false;
+      $scope.newCertificate = {};
+      $scope.submitted = false;
+
+      $scope.loadCertificates = function() {
+        $scope.certificates = [];
+        $scope.availableCertificateTypes = Constants.CERTIFICATE_TYPES;
+        $scope.loading = true;
+        // Use Certificate Service to get the locations of all the certificates,
+        // then add a promise for fetching each certificate
+        APIUtils.getCertificateLocations().then(
+            function(data) {
+              var promises = [];
+              var locations = data.Links.Certificates;
+              for (var i in locations) {
+                var location = locations[i];
+                promises.push(getCertificatePromise(location['@odata.id']));
+              }
+              $q.all(promises)
+                  .catch(function(error) {
+                    toastService.error('Failed to load certificates.');
+                    console.log(JSON.stringify(error));
+                  })
+                  .finally(function() {
+                    $scope.loading = false;
+                  });
+            },
+            function(error) {
+              $scope.loading = false;
+              $scope.availableCertificateTypes = [];
+              toastService.error('Failed to load certificates.');
+              console.log(JSON.stringify(error));
+            });
+      };
+
+      $scope.uploadCertificate = function() {
+        if ($scope.newCertificate.file.name.split('.').pop() !== 'pem') {
+          toastService.error('Certificate must be a .pem file.');
+          return;
+        }
+        $scope.addCertificateModal = false;
+        APIUtils
+            .addNewCertificate(
+                $scope.newCertificate.file, $scope.newCertificate.selectedType)
+            .then(
+                function(data) {
+                  toastService.success(
+                      $scope.newCertificate.selectedType.Description +
+                      ' was uploaded.');
+                  $scope.newCertificate = {};
+                  $scope.loadCertificates();
+                },
+                function(error) {
+                  toastService.error(
+                      $scope.newCertificate.selectedType.Description +
+                      ' failed upload.');
+                  console.log(JSON.stringify(error));
+                });
+      };
+
+      var getCertificatePromise = function(url) {
+        var promise = APIUtils.getCertificate(url).then(function(data) {
+          var certificate = data;
+          isExpiring(certificate);
+          updateAvailableTypes(certificate);
+          $scope.certificates.push(certificate);
+        });
+        return promise;
+      };
+
+      var isExpiring = function(certificate) {
+        // if ValidNotAfter is less than or equal to 30 days from today
+        // (2592000000), isExpiring. If less than or equal to 0, is expired.
+        var difference = certificate.ValidNotAfter - new Date();
+        if (difference <= 0) {
+          certificate.isExpired = true;
+        } else if (difference <= 2592000000) {
+          certificate.isExpiring = true;
+        } else {
+          certificate.isExpired = false;
+          certificate.isExpiring = false;
+        }
+      };
+
+      var updateAvailableTypes = function(certificate) {
+        // TODO: at this time only one of each type of certificate is allowed.
+        // When this changes, this will need to be updated.
+        // Removes certificate type from available types to be added.
+        $scope.availableCertificateTypes =
+            $scope.availableCertificateTypes.filter(function(type) {
+              return type.Description !== certificate.Description;
+            });
+      };
+
+      $scope.getDays = function(endDate) {
+        // finds number of days until certificate expiration
+        var ms = endDate - new Date();
+        return Math.floor(ms / (24 * 60 * 60 * 1000));
+      };
+
+      $scope.loadCertificates();
+    }
+  ]);
+})(angular);
diff --git a/app/configuration/index.js b/app/configuration/index.js
index db35ce6..8a1acda 100644
--- a/app/configuration/index.js
+++ b/app/configuration/index.js
@@ -41,6 +41,12 @@
                 'controller': 'virtualMediaController',
                 authenticated: true
               })
+              .when('/configuration/certificate', {
+                'template':
+                    require('./controllers/certificate-controller.html'),
+                'controller': 'certificateController',
+                authenticated: true
+              })
               .when('/configuration/firmware', {
                 'template': require('./controllers/firmware-controller.html'),
                 'controller': 'firmwareController',
diff --git a/app/configuration/styles/certificate.scss b/app/configuration/styles/certificate.scss
new file mode 100644
index 0000000..e3ead19
--- /dev/null
+++ b/app/configuration/styles/certificate.scss
@@ -0,0 +1,112 @@
+.certificate__table {
+  border-left: 1px solid $medgrey;
+  border-right: 1px solid $medgrey;
+  margin-top: .5em;
+  .table__row-header {
+    width: 100%;
+    border-bottom: 1px solid $medgrey;
+      background-color: $darkbg__primary;
+      color: $white;
+      font-weight: 700;
+  }
+  .table__row-value {
+    width: 100%;
+    border-bottom: 1px solid $medgrey;
+  }
+  .certificate__type-header {
+    @include mediaQuery(small) {
+      width: 25%;
+      background: transparent;
+    }
+    width: 100%;
+    padding: .8em;
+    padding-left: 1.5em;
+  }
+  .certificate__date-header {
+    padding:.8em;
+    @include mediaQuery(small) {
+      width: 30%;
+      display: block;
+    }
+    display: none;
+  }
+  .certificate__status-header {
+    padding: .8em;
+    @include mediaQuery(small) {
+      width: 5%;
+      display: block;
+    }
+    display: none;
+  }
+  .certificate__type-cell {
+    width: 100%;
+    padding: .8em .8em .8em 1.5em;
+    word-wrap: break-word;
+    background: $lightgrey;
+    @include mediaQuery(small) {
+      width: 25%;
+      background: transparent;
+    }
+  }
+  .certificate__date-cell {
+    padding: .8em ;
+    word-wrap: break-word;
+    @include mediaQuery(small) {
+      width: 30%;
+    }
+    width: 70%;
+  }
+  .certificate__status-cell {
+    padding: .8em;
+    @include mediaQuery(small) {
+      display: block;
+      width: 5%;
+    }
+    display: none;
+  }
+  .certificate__buttons-cell {
+    @include mediaQuery(small) {
+      width: 10%;
+      padding-top: .8em;
+    }
+    width: 100%;
+    text-align: right;
+    padding: 0 1.5em .8em 0;
+  }
+  .certificate__title-inline {
+    @include mediaQuery(small) {
+      display: none;
+    }
+    width: 30%;
+    display: block;
+    padding: .8em .8em .8em 1.5em;
+  }
+  .upload__certificate {
+    border-top: 1px solid $medgrey;
+    width: 100%;
+    background: $lightgrey;
+    padding: .8em;
+  }
+}
+.certificate__upload-chooser {
+  background: $lightgrey;
+}
+
+.add__certificate__modal {
+  select {
+    margin-bottom: 0;
+  }
+  .file__upload {
+    margin-bottom: 2em;
+  }
+  .select__new-label {
+    margin-bottom: 1em;
+  }
+  .select__new-button {
+    font-size: 1.2em;
+  }
+  .file__name {
+    background-color: $upload__background;
+    padding: .5em;
+  }
+}
diff --git a/app/configuration/styles/index.scss b/app/configuration/styles/index.scss
index f035580..a8bd1a8 100644
--- a/app/configuration/styles/index.scss
+++ b/app/configuration/styles/index.scss
@@ -3,3 +3,4 @@
 @import "./date-time.scss";
 @import "./firmware.scss";
 @import "./virtual-media.scss";
+@import "./certificate.scss";