xref: /openbmc/webui-vue/src/views/SecurityAndAccess/Certificates/Certificates.vue (revision de23ea23d88451a2fa2774ec72053772603c23ae)
1<template>
2  <b-container fluid="xl">
3    <page-title />
4    <b-row>
5      <b-col xl="11">
6        <!-- Expired certificates banner -->
7        <alert :show="expiredCertificateTypes.length > 0" variant="danger">
8          <template v-if="expiredCertificateTypes.length > 1">
9            {{ $t('pageCertificates.alert.certificatesExpiredMessage') }}
10          </template>
11          <template v-else>
12            {{
13              $t('pageCertificates.alert.certificateExpiredMessage', {
14                certificate: expiredCertificateTypes[0],
15              })
16            }}
17          </template>
18        </alert>
19        <!-- Expiring certificates banner -->
20        <alert :show="expiringCertificateTypes.length > 0" variant="warning">
21          <template v-if="expiringCertificateTypes.length > 1">
22            {{ $t('pageCertificates.alert.certificatesExpiringMessage') }}
23          </template>
24          <template v-else>
25            {{
26              $t('pageCertificates.alert.certificateExpiringMessage', {
27                certificate: expiringCertificateTypes[0],
28              })
29            }}
30          </template>
31        </alert>
32      </b-col>
33    </b-row>
34    <b-row>
35      <b-col xl="11" class="text-right">
36        <b-button
37          v-b-modal.generate-csr
38          data-test-id="certificates-button-generateCsr"
39          variant="link"
40        >
41          <icon-add />
42          {{ $t('pageCertificates.generateCsr') }}
43        </b-button>
44        <b-button
45          variant="primary"
46          :disabled="certificatesForUpload.length === 0"
47          @click="initModalUploadCertificate(null)"
48        >
49          <icon-add />
50          {{ $t('pageCertificates.addNewCertificate') }}
51        </b-button>
52      </b-col>
53    </b-row>
54    <b-row>
55      <b-col xl="11">
56        <b-table
57          responsive="md"
58          show-empty
59          hover
60          :busy="isBusy"
61          :fields="fields"
62          :items="tableItems"
63          :empty-text="$t('global.table.emptyMessage')"
64        >
65          <template #cell(validFrom)="{ value }">
66            {{ $filters.formatDate(value) }}
67          </template>
68
69          <template #cell(validUntil)="{ value }">
70            <status-icon
71              v-if="getDaysUntilExpired(value) < 31"
72              :status="getIconStatus(value)"
73            />
74            {{ $filters.formatDate(value) }}
75          </template>
76
77          <template #cell(actions)="{ value, item }">
78            <table-row-action
79              v-for="(action, index) in value"
80              :key="index"
81              :value="action.value"
82              :title="action.title"
83              :enabled="action.enabled"
84              @click-table-action="onTableRowAction($event, item)"
85            >
86              <template #icon>
87                <icon-replace v-if="action.value === 'replace'" />
88                <icon-trashcan v-if="action.value === 'delete'" />
89              </template>
90            </table-row-action>
91          </template>
92        </b-table>
93      </b-col>
94    </b-row>
95
96    <!-- Modals -->
97    <modal-upload-certificate :certificate="modalCertificate" @ok="onModalOk" />
98    <modal-generate-csr />
99  </b-container>
100</template>
101
102<script>
103import IconAdd from '@carbon/icons-vue/es/add--alt/20';
104import IconReplace from '@carbon/icons-vue/es/renew/20';
105import IconTrashcan from '@carbon/icons-vue/es/trash-can/20';
106
107import ModalGenerateCsr from './ModalGenerateCsr';
108import ModalUploadCertificate from './ModalUploadCertificate';
109import PageTitle from '@/components/Global/PageTitle';
110import TableRowAction from '@/components/Global/TableRowAction';
111import StatusIcon from '@/components/Global/StatusIcon';
112import Alert from '@/components/Global/Alert';
113
114import BVToastMixin from '@/components/Mixins/BVToastMixin';
115import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
116import { useI18n } from 'vue-i18n';
117import i18n from '@/i18n';
118
119export default {
120  name: 'Certificates',
121  components: {
122    Alert,
123    IconAdd,
124    IconReplace,
125    IconTrashcan,
126    ModalGenerateCsr,
127    ModalUploadCertificate,
128    PageTitle,
129    StatusIcon,
130    TableRowAction,
131  },
132  mixins: [BVToastMixin, LoadingBarMixin],
133  beforeRouteLeave(to, from, next) {
134    this.hideLoader();
135    next();
136  },
137  data() {
138    return {
139      $t: useI18n().t,
140      isBusy: true,
141      modalCertificate: null,
142      fileTypeCorrect: undefined,
143      fields: [
144        {
145          key: 'certificate',
146          label: i18n.global.t('pageCertificates.table.certificate'),
147        },
148        {
149          key: 'issuedBy',
150          label: i18n.global.t('pageCertificates.table.issuedBy'),
151        },
152        {
153          key: 'issuedTo',
154          label: i18n.global.t('pageCertificates.table.issuedTo'),
155        },
156        {
157          key: 'validFrom',
158          label: i18n.global.t('pageCertificates.table.validFrom'),
159        },
160        {
161          key: 'validUntil',
162          label: i18n.global.t('pageCertificates.table.validUntil'),
163        },
164        {
165          key: 'actions',
166          label: '',
167          tdClass: 'text-right text-nowrap',
168        },
169      ],
170    };
171  },
172  computed: {
173    certificates() {
174      return this.$store.getters['certificates/allCertificates'];
175    },
176    tableItems() {
177      return this.certificates.map((certificate) => {
178        return {
179          ...certificate,
180          actions: [
181            {
182              value: 'replace',
183              title: i18n.global.t('pageCertificates.replaceCertificate'),
184            },
185            {
186              value: 'delete',
187              title: i18n.global.t('pageCertificates.deleteCertificate'),
188              enabled:
189                certificate.type === 'TrustStore Certificate' ? true : false,
190            },
191          ],
192        };
193      });
194    },
195    certificatesForUpload() {
196      return this.$store.getters['certificates/availableUploadTypes'];
197    },
198    bmcTime() {
199      return this.$store.getters['global/bmcTime'];
200    },
201    expiredCertificateTypes() {
202      return this.certificates.reduce((acc, val) => {
203        const daysUntilExpired = this.getDaysUntilExpired(val.validUntil);
204        if (daysUntilExpired < 1) {
205          acc.push(val.certificate);
206        }
207        return acc;
208      }, []);
209    },
210    expiringCertificateTypes() {
211      return this.certificates.reduce((acc, val) => {
212        const daysUntilExpired = this.getDaysUntilExpired(val.validUntil);
213        if (daysUntilExpired < 31 && daysUntilExpired > 0) {
214          acc.push(val.certificate);
215        }
216        return acc;
217      }, []);
218    },
219  },
220  async created() {
221    this.startLoader();
222    await this.$store.dispatch('global/getBmcTime');
223    this.$store.dispatch('certificates/getCertificates').finally(() => {
224      this.endLoader();
225      this.isBusy = false;
226    });
227  },
228  methods: {
229    onTableRowAction(event, rowItem) {
230      switch (event) {
231        case 'replace':
232          this.initModalUploadCertificate(rowItem);
233          break;
234        case 'delete':
235          this.initModalDeleteCertificate(rowItem);
236          break;
237        default:
238          break;
239      }
240    },
241    initModalUploadCertificate(certificate = null) {
242      this.modalCertificate = certificate;
243      this.$bvModal.show('upload-certificate');
244    },
245    initModalDeleteCertificate(certificate) {
246      this.$bvModal
247        .msgBoxConfirm(
248          i18n.global.t('pageCertificates.modal.deleteConfirmMessage', {
249            issuedBy: certificate.issuedBy,
250            certificate: certificate.certificate,
251          }),
252          {
253            title: i18n.global.t('pageCertificates.deleteCertificate'),
254            okTitle: i18n.global.t('global.action.delete'),
255            cancelTitle: i18n.global.t('global.action.cancel'),
256            autoFocusButton: 'ok',
257          },
258        )
259        .then((deleteConfirmed) => {
260          if (deleteConfirmed) this.deleteCertificate(certificate);
261        });
262    },
263    onModalOk({ addNew, file, type, location }) {
264      if (addNew) {
265        // Upload a new certificate
266        this.fileTypeCorrect = this.getIsFileTypeCorrect(file);
267        if (this.fileTypeCorrect) {
268          this.addNewCertificate(file, type);
269        } else {
270          this.errorToast(
271            i18n.global.t(
272              'pageCertificates.alert.incorrectCertificateFileType',
273            ),
274            {
275              title: i18n.global.t(
276                'pageCertificates.toast.errorAddCertificate',
277              ),
278            },
279          );
280        }
281      } else {
282        // Replace an existing certificate
283        this.replaceCertificate(file, type, location);
284      }
285    },
286    addNewCertificate(file, type) {
287      if (this.fileTypeCorrect === true) {
288        this.startLoader();
289        this.$store
290          .dispatch('certificates/addNewCertificate', { file, type })
291          .then((success) => this.successToast(success))
292          .catch(({ message }) => this.errorToast(message))
293          .finally(() => this.endLoader());
294      }
295    },
296    replaceCertificate(file, type, location) {
297      this.startLoader();
298      const reader = new FileReader();
299      reader.readAsBinaryString(file);
300      reader.onloadend = (event) => {
301        const certificateString = event.target.result;
302        this.$store
303          .dispatch('certificates/replaceCertificate', {
304            certificateString,
305            type,
306            location,
307          })
308          .then((success) => this.successToast(success))
309          .catch(({ message }) => this.errorToast(message))
310          .finally(() => this.endLoader());
311      };
312    },
313    deleteCertificate({ type, location }) {
314      this.startLoader();
315      this.$store
316        .dispatch('certificates/deleteCertificate', {
317          type,
318          location,
319        })
320        .then((success) => this.successToast(success))
321        .catch(({ message }) => this.errorToast(message))
322        .finally(() => this.endLoader());
323    },
324    getDaysUntilExpired(date) {
325      if (this.bmcTime) {
326        const validUntilMs = date.getTime();
327        const currentBmcTimeMs = this.bmcTime.getTime();
328        const oneDayInMs = 24 * 60 * 60 * 1000;
329        return Math.round((validUntilMs - currentBmcTimeMs) / oneDayInMs);
330      }
331      return new Date();
332    },
333    getIconStatus(date) {
334      const daysUntilExpired = this.getDaysUntilExpired(date);
335      if (daysUntilExpired < 1) {
336        return 'danger';
337      } else if (daysUntilExpired < 31) {
338        return 'warning';
339      }
340    },
341    getIsFileTypeCorrect(file) {
342      const fileTypeExtension = file.name.split('.').pop();
343      return fileTypeExtension === 'pem';
344    },
345  },
346};
347</script>
348