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            {{ value | formatDate }}
67          </template>
68
69          <template #cell(validUntil)="{ value }">
70            <status-icon
71              v-if="getDaysUntilExpired(value) < 31"
72              :status="getIconStatus(value)"
73            />
74            {{ value | formatDate }}
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';
116
117export default {
118  name: 'Certificates',
119  components: {
120    Alert,
121    IconAdd,
122    IconReplace,
123    IconTrashcan,
124    ModalGenerateCsr,
125    ModalUploadCertificate,
126    PageTitle,
127    StatusIcon,
128    TableRowAction,
129  },
130  mixins: [BVToastMixin, LoadingBarMixin],
131  beforeRouteLeave(to, from, next) {
132    this.hideLoader();
133    next();
134  },
135  data() {
136    return {
137      isBusy: true,
138      modalCertificate: null,
139      fileTypeCorrect: undefined,
140      fields: [
141        {
142          key: 'certificate',
143          label: this.$t('pageCertificates.table.certificate'),
144        },
145        {
146          key: 'issuedBy',
147          label: this.$t('pageCertificates.table.issuedBy'),
148        },
149        {
150          key: 'issuedTo',
151          label: this.$t('pageCertificates.table.issuedTo'),
152        },
153        {
154          key: 'validFrom',
155          label: this.$t('pageCertificates.table.validFrom'),
156        },
157        {
158          key: 'validUntil',
159          label: this.$t('pageCertificates.table.validUntil'),
160        },
161        {
162          key: 'actions',
163          label: '',
164          tdClass: 'text-right text-nowrap',
165        },
166      ],
167    };
168  },
169  computed: {
170    certificates() {
171      return this.$store.getters['certificates/allCertificates'];
172    },
173    tableItems() {
174      return this.certificates.map((certificate) => {
175        return {
176          ...certificate,
177          actions: [
178            {
179              value: 'replace',
180              title: this.$t('pageCertificates.replaceCertificate'),
181            },
182            {
183              value: 'delete',
184              title: this.$t('pageCertificates.deleteCertificate'),
185              enabled:
186                certificate.type === 'TrustStore Certificate' ? true : false,
187            },
188          ],
189        };
190      });
191    },
192    certificatesForUpload() {
193      return this.$store.getters['certificates/availableUploadTypes'];
194    },
195    bmcTime() {
196      return this.$store.getters['global/bmcTime'];
197    },
198    expiredCertificateTypes() {
199      return this.certificates.reduce((acc, val) => {
200        const daysUntilExpired = this.getDaysUntilExpired(val.validUntil);
201        if (daysUntilExpired < 1) {
202          acc.push(val.certificate);
203        }
204        return acc;
205      }, []);
206    },
207    expiringCertificateTypes() {
208      return this.certificates.reduce((acc, val) => {
209        const daysUntilExpired = this.getDaysUntilExpired(val.validUntil);
210        if (daysUntilExpired < 31 && daysUntilExpired > 0) {
211          acc.push(val.certificate);
212        }
213        return acc;
214      }, []);
215    },
216  },
217  async created() {
218    this.startLoader();
219    await this.$store.dispatch('global/getBmcTime');
220    this.$store.dispatch('certificates/getCertificates').finally(() => {
221      this.endLoader();
222      this.isBusy = false;
223    });
224  },
225  methods: {
226    onTableRowAction(event, rowItem) {
227      switch (event) {
228        case 'replace':
229          this.initModalUploadCertificate(rowItem);
230          break;
231        case 'delete':
232          this.initModalDeleteCertificate(rowItem);
233          break;
234        default:
235          break;
236      }
237    },
238    initModalUploadCertificate(certificate = null) {
239      this.modalCertificate = certificate;
240      this.$bvModal.show('upload-certificate');
241    },
242    initModalDeleteCertificate(certificate) {
243      this.$bvModal
244        .msgBoxConfirm(
245          this.$t('pageCertificates.modal.deleteConfirmMessage', {
246            issuedBy: certificate.issuedBy,
247            certificate: certificate.certificate,
248          }),
249          {
250            title: this.$t('pageCertificates.deleteCertificate'),
251            okTitle: this.$t('global.action.delete'),
252            cancelTitle: this.$t('global.action.cancel'),
253            autoFocusButton: 'ok',
254          },
255        )
256        .then((deleteConfirmed) => {
257          if (deleteConfirmed) this.deleteCertificate(certificate);
258        });
259    },
260    onModalOk({ addNew, file, type, location }) {
261      if (addNew) {
262        // Upload a new certificate
263        this.fileTypeCorrect = this.getIsFileTypeCorrect(file);
264        if (this.fileTypeCorrect) {
265          this.addNewCertificate(file, type);
266        } else {
267          this.errorToast(
268            this.$t('pageCertificates.alert.incorrectCertificateFileType'),
269            {
270              title: this.$t('pageCertificates.toast.errorAddCertificate'),
271            },
272          );
273        }
274      } else {
275        // Replace an existing certificate
276        this.replaceCertificate(file, type, location);
277      }
278    },
279    addNewCertificate(file, type) {
280      if (this.fileTypeCorrect === true) {
281        this.startLoader();
282        this.$store
283          .dispatch('certificates/addNewCertificate', { file, type })
284          .then((success) => this.successToast(success))
285          .catch(({ message }) => this.errorToast(message))
286          .finally(() => this.endLoader());
287      }
288    },
289    replaceCertificate(file, type, location) {
290      this.startLoader();
291      const reader = new FileReader();
292      reader.readAsBinaryString(file);
293      reader.onloadend = (event) => {
294        const certificateString = event.target.result;
295        this.$store
296          .dispatch('certificates/replaceCertificate', {
297            certificateString,
298            type,
299            location,
300          })
301          .then((success) => this.successToast(success))
302          .catch(({ message }) => this.errorToast(message))
303          .finally(() => this.endLoader());
304      };
305    },
306    deleteCertificate({ type, location }) {
307      this.startLoader();
308      this.$store
309        .dispatch('certificates/deleteCertificate', {
310          type,
311          location,
312        })
313        .then((success) => this.successToast(success))
314        .catch(({ message }) => this.errorToast(message))
315        .finally(() => this.endLoader());
316    },
317    getDaysUntilExpired(date) {
318      if (this.bmcTime) {
319        const validUntilMs = date.getTime();
320        const currentBmcTimeMs = this.bmcTime.getTime();
321        const oneDayInMs = 24 * 60 * 60 * 1000;
322        return Math.round((validUntilMs - currentBmcTimeMs) / oneDayInMs);
323      }
324      return new Date();
325    },
326    getIconStatus(date) {
327      const daysUntilExpired = this.getDaysUntilExpired(date);
328      if (daysUntilExpired < 1) {
329        return 'danger';
330      } else if (daysUntilExpired < 31) {
331        return 'warning';
332      }
333    },
334    getIsFileTypeCorrect(file) {
335      const fileTypeExtension = file.name.split('.').pop();
336      return fileTypeExtension === 'pem';
337    },
338  },
339};
340</script>
341