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