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