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