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/assets/icons/icon-replace.svg b/app/assets/icons/icon-replace.svg
new file mode 100644
index 0000000..edfd5f4
--- /dev/null
+++ b/app/assets/icons/icon-replace.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"><path fill-rule="nonzero" d="M5.452 9.452l-1.529.898-.54-.947L6.53 7.516l1.937 3.151-.87.562-1.07-1.7a3.323 3.323 0 0 0 3.689 4.997l.636 1.018a4.395 4.395 0 0 1-5.4-6.091zm8.224 2.945l1.467-.871.555 1.004-3.146 1.886-1.937-3.15.98-.624.938 1.545a3.328 3.328 0 0 0-2.626-4.136L9.25 7.002 9.395 7a4.395 4.395 0 0 1 4.281 5.397zM7 1H1.455C1.204 1 1 1.265 1 1.59v11.82c0 .325.204.59.455.59h3.263c.211.363.464.698.751 1H1.385C.62 15 0 14.225 0 13.27V1.73C0 .776.62 0 1.385 0h6c.122 0 .24.06.326.169l4.154 4.399a.657.657 0 0 1 .135.407v1.743a5.299 5.299 0 0 0-1-.45V5.011H7.727c-.401 0-.727-.556-.727-1.01V1zm1 .863v2.139h2.045L8 1.862z"/></svg>
\ No newline at end of file
diff --git a/app/common/directives/app-navigation.html b/app/common/directives/app-navigation.html
index 96c2138..eabb137 100644
--- a/app/common/directives/app-navigation.html
+++ b/app/common/directives/app-navigation.html
@@ -84,6 +84,10 @@
<a href="#/configuration/network" ng-click="closeSubnav()"
tabindex="{{(showSubMenu && firstLevel == 'configuration') ? 0 : -1}}">Network settings</a>
</li>
+ <li ng-class="{'active': (path == '/configuration' || path == '/configuration/certificate')}">
+ <a href="#/configuration/certificate" ng-click="closeSubnav()"
+ tabindex="{{(showSubMenu && firstLevel == 'configuration') ? 0 : -1}}">Certificate management</a>
+ </li>
<li ng-class="{'active': (path == '/configuration' || path == '/configuration/snmp')}">
<a href="#/configuration/snmp" ng-click="closeSubnav()"
tabindex="{{(showSubMenu && firstLevel == 'configuration') ? 0 : -1}}">SNMP settings</a>
diff --git a/app/common/directives/certificate.html b/app/common/directives/certificate.html
new file mode 100644
index 0000000..a46de69
--- /dev/null
+++ b/app/common/directives/certificate.html
@@ -0,0 +1,57 @@
+<div class="table__row-value row column">
+ <div class="certificate__type-cell bold">
+ {{cert.Description}}
+ </div>
+ <div class="certificate__title-inline">
+ Valid from:
+ </div>
+ <div class="certificate__date-cell">
+ {{cert.ValidNotBefore | localeDate}}
+ </div>
+ <div class="certificate__title-inline">
+ Valid until:
+ </div>
+ <div class="certificate__status-cell">
+ <span class="inline"
+ ng-class="{'icon__warning' : cert.isExpiring , 'icon__critical' : cert.isExpired}"
+ ng-if="cert.isExpired || cert.isExpiring"></span>
+ </div>
+ <div class="certificate__date-cell">
+ {{cert.ValidNotAfter | localeDate}}
+ </div>
+ <div class="certificate__buttons-cell">
+ <button type="button" class="btn btn-tertiary certificate__button">
+ <icon file="icon-replace.svg" ng-click="cert.upload = true"
+ aria-label="Replace certificate"></icon>
+ </button>
+ </div>
+ <div ng-show="cert.upload === true" class="upload__certificate">
+ <div class="certificate__upload-chooser row">
+ <div class="small-1 column">
+ <button type="button">
+ <icon file="icon-close.svg" ng-click="cert.upload=false"></icon>
+ </button>
+ </div>
+ <div class="small-2 column">
+ <label for='upload_{{cert.Description + cert.Id}}'>
+ <input name="upload_{{cert.Description + cert.Id}}"
+ id="upload_{{cert.Description + cert.Id}}"
+ type="file" file="cert.file" class="hide"/>
+ <span class="btn btn-secondary">Choose file</span>
+ </label>
+ </div>
+ <div class="small-6 column">
+ <span ng-if="!cert.file">No file selected</span>
+ <span>{{cert.file.name}}</span>
+ <button type="button" ng-if="cert.file.name" ng-click="cert.file = '';">
+ <icon file="icon-close.svg"></icon>
+ </button>
+ </div>
+ <div class="small-3 column">
+ <button type="button" ng-class="{disabled:!cert.file}"
+ class="btn btn-primary"
+ ng-click="replaceCertificate(cert)">Replace</button>
+ </div>
+ </div>
+ </div>
+</div>
\ No newline at end of file
diff --git a/app/common/directives/certificate.js b/app/common/directives/certificate.js
new file mode 100644
index 0000000..63dc594
--- /dev/null
+++ b/app/common/directives/certificate.js
@@ -0,0 +1,55 @@
+window.angular && (function(angular) {
+ 'use strict';
+
+ angular.module('app.common.directives').directive('certificate', [
+ 'APIUtils',
+ function(APIUtils) {
+ return {
+ 'restrict': 'E',
+ 'template': require('./certificate.html'),
+ 'scope': {'cert': '=', 'reload': '&'},
+ 'controller': [
+ '$scope', 'APIUtils', 'toastService',
+ function($scope, APIUtils, toastService) {
+ var certificateType = 'PEM';
+ $scope.replaceCertificate = function(certificate) {
+ $scope.loading = true;
+ if (certificate.file.name.split('.').pop() !==
+ certificateType.toLowerCase()) {
+ toastService.error(
+ 'Certificate must be replaced with a .pem file.');
+ return;
+ }
+ var file =
+ document
+ .getElementById(
+ 'upload_' + certificate.Description + certificate.Id)
+ .files[0];
+ var reader = new FileReader();
+ reader.onloadend = function(e) {
+ var data = {};
+ data.CertificateString = e.target.result;
+ data.CertificateUri = {'@odata.id': certificate['@odata.id']};
+ data.CertificateType = certificateType;
+ APIUtils.replaceCertificate(data).then(
+ function(data) {
+ $scope.loading = false;
+ toastService.success(
+ certificate.Description + ' was replaced.');
+ $scope.reload();
+ },
+ function(error) {
+ console.log(error);
+ $scope.loading = false;
+ toastService.error(
+ 'Unable to replace ' + certificate.Description);
+ });
+ };
+ reader.readAsBinaryString(file);
+ };
+ }
+ ]
+ };
+ }
+ ]);
+})(window.angular);
diff --git a/app/common/services/api-utils.js b/app/common/services/api-utils.js
index e796f43..46d7d4b 100644
--- a/app/common/services/api-utils.js
+++ b/app/common/services/api-utils.js
@@ -1355,6 +1355,51 @@
return response.data;
});
},
+ getCertificateLocations: function() {
+ return $http({
+ method: 'GET',
+ url: DataService.getHost() +
+ '/redfish/v1/CertificateService/CertificateLocations',
+ withCredentials: true
+ })
+ .then(function(response) {
+ return response.data;
+ });
+ },
+ getCertificate: function(location) {
+ return $http({
+ method: 'GET',
+ url: DataService.getHost() + location,
+ withCredentials: true
+ })
+ .then(function(response) {
+ return response.data;
+ });
+ },
+ addNewCertificate: function(file, type) {
+ return $http({
+ method: 'POST',
+ url: DataService.getHost() + type.location,
+ headers: {'Content-Type': 'application/x-pem-file'},
+ withCredentials: true,
+ data: file
+ })
+ .then(function(response) {
+ return response.data;
+ });
+ },
+ replaceCertificate: function(data) {
+ return $http({
+ method: 'POST',
+ url: DataService.getHost() +
+ '/redfish/v1/CertificateService/Actions/CertificateService.ReplaceCertificate',
+ withCredentials: true,
+ data: data
+ })
+ .then(function(response) {
+ return response.data;
+ });
+ },
getHardwares: function(callback) {
$http({
method: 'GET',
diff --git a/app/common/services/constants.js b/app/common/services/constants.js
index dd1012c..dabbc77 100644
--- a/app/common/services/constants.js
+++ b/app/common/services/constants.js
@@ -20,6 +20,17 @@
SUCCESS_STATUS: 'ok',
SUCCESS_MESSAGE: '200 OK'
},
+ CERTIFICATE_TYPES: [
+ {
+ 'Description': 'HTTPS Certificate',
+ 'location':
+ '/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates'
+ },
+ {
+ 'Description': 'LDAP Certificate',
+ 'location': '/redfish/v1/AccountService/LDAP/Certificates'
+ }
+ ],
CHASSIS_POWER_STATE: {
on: 'On',
on_code: 'xyz.openbmc_project.State.Chassis.PowerState.On',
diff --git a/app/common/styles/base/buttons.scss b/app/common/styles/base/buttons.scss
index 9aeb725..a04aebd 100644
--- a/app/common/styles/base/buttons.scss
+++ b/app/common/styles/base/buttons.scss
@@ -63,6 +63,7 @@
svg {
height: 1.2em;
width: auto;
+ max-width: 100%;
}
}
diff --git a/app/common/styles/base/colors.scss b/app/common/styles/base/colors.scss
index f5f93f0..bb07bd0 100644
--- a/app/common/styles/base/colors.scss
+++ b/app/common/styles/base/colors.scss
@@ -42,6 +42,11 @@
$purple: #5A3EC8;
$field__disabled: #e6e6e6;
$field__focus: #3C6DEF;
+$btn__disabled-txt: #a6a5a6;
+$btn__disabled-bg: #d8d8d8;
+$btn__disabled-border: #CCCCCC;
+$primebtn__disabled-txt: #999999;
+$primebtn__disabled-bg: #CCCCCC;
// Dark background
$darkbg__grey: #E3ECEC;
@@ -89,7 +94,8 @@
$critical-darkbg: #ff5c49;
$severity-medium-lightbg: #dc267f;
$medium-darkbg: #FF509E;
-$warning-lightbg: #ff836f;
+$warning-lightbg: #fff8e4;
+$warning-border: #ffdf99;
$warning-darkbg: #ffb000;
$low-lightbg: #c42cd6;
$normal: #00baa1;
@@ -125,5 +131,8 @@
$nav__second-level-color: #e6e9ed;
$nav__second-level-text-color: #4b5d78;
+//Upload
+$upload__background: #f0f2f5;
+
//Loader
$loaderColor: $color--blue-50;
diff --git a/app/common/styles/elements/alerts.scss b/app/common/styles/elements/alerts.scss
index 6242257..8853c70 100644
--- a/app/common/styles/elements/alerts.scss
+++ b/app/common/styles/elements/alerts.scss
@@ -1,10 +1,18 @@
//Fixed alerts
-.alert-danger{
+.alert-danger {
background-color: $alert__danger;
border-color: $critical-lightbg;
border-radius: 0;
- color: #333;
+ color: $black;
+ text-align: left;
+}
+
+.alert-warning {
+ background-color: $warning-lightbg;
+ border-color: $warning-border;
+ border-radius: 0;
+ color: $black;
text-align: left;
}
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";
diff --git a/app/index.js b/app/index.js
index e4061ed..5997457 100644
--- a/app/index.js
+++ b/app/index.js
@@ -43,6 +43,7 @@
import app_navigation from './common/directives/app-navigation.js';
import confirm from './common/directives/confirm.js';
import log_event from './common/directives/log-event.js';
+import certificate from './common/directives/certificate.js';
import log_filter from './common/directives/log-filter.js';
import log_search_control from './common/directives/log-search-control.js';
import toggle_flag from './common/directives/toggle-flag.js';
@@ -83,6 +84,7 @@
import redfish_controller from './redfish/controllers/redfish-controller.js';
import configuration_index from './configuration/index.js';
import date_time_controller from './configuration/controllers/date-time-controller.js';
+import certificate_controller from './configuration/controllers/certificate-controller.js';
import network_controller from './configuration/controllers/network-controller.js';
import snmp_controller from './configuration/controllers/snmp-controller.js';
import firmware_controller from './configuration/controllers/firmware-controller.js';