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