import { HttpClient } from '@angular/common/http'; import { Component, OnInit } from '@angular/core'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateService } from '@ngx-translate/core'; import * as _ from 'lodash'; import { filter } from 'rxjs/operators'; import { helptextSystemCa } from 'app/helptext/system/ca'; import { helptextSystemCertificates } from 'app/helptext/system/certificates'; import { CertificateAuthority } from 'app/interfaces/certificate-authority.interface'; import { Certificate } from 'app/interfaces/certificate.interface'; import { Option } from 'app/interfaces/option.interface'; import { WebsocketError } from 'app/interfaces/websocket-error.interface'; import { DialogFormConfiguration } from 'app/pages/common/entity/entity-dialog/dialog-form-configuration.interface'; import { EntityDialogComponent } from 'app/pages/common/entity/entity-dialog/entity-dialog.component'; import { FieldConfig } from 'app/pages/common/entity/entity-form/models/field-config.interface'; import { EntityFormService } from 'app/pages/common/entity/entity-form/services/entity-form.service'; import { EntityJobComponent } from 'app/pages/common/entity/entity-job/entity-job.component'; import { AppTableAction, AppTableConfig, TableComponent } from 'app/pages/common/entity/table/table.component'; import { TableService } from 'app/pages/common/entity/table/table.service'; import { EntityUtils } from 'app/pages/common/entity/utils'; import { AcmednsFormComponent } from 'app/pages/credentials/certificates-dash/forms/acmedns-form.component'; import { SystemGeneralService, WebSocketService, DialogService, StorageService, ModalServiceMessage, } from 'app/services'; import { ModalService } from 'app/services/modal.service'; import { CertificateAuthorityAddComponent } from './forms/ca-add.component'; import { CertificateAuthorityEditComponent } from './forms/ca-edit.component'; import { CertificateAcmeAddComponent } from './forms/certificate-acme-add.component'; import { CertificateAddComponent } from './forms/certificate-add.component'; import { CertificateEditComponent } from './forms/certificate-edit.component'; @UntilDestroy() @Component({ selector: 'app-certificates-dash', templateUrl: './certificates-dash.component.html', providers: [EntityFormService], }) export class CertificatesDashComponent implements OnInit { cards: { name: string; tableConf: AppTableConfig<CertificatesDashComponent> }[]; protected dialogRef: MatDialogRef<EntityJobComponent>; private downloadActions: AppTableAction[]; private unsignedCAs: Option[] = []; private caId: number; constructor( private modalService: ModalService, private ws: WebSocketService, private dialog: MatDialog, private systemGeneralService: SystemGeneralService, private dialogService: DialogService, private storage: StorageService, private http: HttpClient, private tableService: TableService, private translate: TranslateService, ) { } ngOnInit(): void { this.getCards(); this.modalService.refreshTable$.pipe(untilDestroyed(this)).subscribe(() => { this.getCards(); }); this.modalService.message$.pipe(untilDestroyed(this)).subscribe((res: ModalServiceMessage) => { if (res['action'] === 'open' && res['component'] === 'acmeComponent') { this.openForm(res['row']); } }); this.systemGeneralService.getUnsignedCertificates().pipe(untilDestroyed(this)).subscribe((res) => { res.forEach((item) => { this.unsignedCAs.push( { label: item.name, value: item.id }, ); }); }); } getCards(): void { this.cards = [ { name: 'certificates', tableConf: { title: this.translate.instant('Certificates'), queryCall: 'certificate.query', deleteCall: 'certificate.delete', deleteCallIsJob: true, complex: true, dataSourceHelper: this.certificatesDataSourceHelper, getActions: this.certificateActions.bind(this), isActionVisible: (actionId: string, certificate: Certificate) => { if (actionId === 'revoke') { return certificate.can_be_revoked; } return true; }, columns: [ { name: this.translate.instant('Name'), prop1: 'name', name2: this.translate.instant('Issuer'), prop2: 'issuer', }, { name: this.translate.instant('From'), prop1: 'from', name2: this.translate.instant('Until'), prop2: 'until', }, { name: this.translate.instant('CN'), prop1: 'common', name2: this.translate.instant('SAN'), prop2: 'san', }, { name: this.translate.instant('Status'), prop1: 'revoked', iconTooltip: this.translate.instant('Revoked'), getIcon: (element: Certificate, prop: keyof Certificate): string => { if (element[prop]) { return 'block'; } return ''; }, }, ], parent: this, add() { this.parent.modalService.openInSlideIn(CertificateAddComponent); }, edit(row: Certificate) { this.parent.modalService.openInSlideIn(CertificateEditComponent, row.id); }, }, }, { name: 'CSRs', tableConf: { title: this.translate.instant('Certificate Signing Requests'), queryCall: 'certificate.query', deleteCall: 'certificate.delete', deleteCallIsJob: true, complex: true, dataSourceHelper: this.csrDataSourceHelper, getActions: this.csrActions.bind(this), columns: [ { name: this.translate.instant('Name'), prop1: 'name', name2: this.translate.instant('Issuer'), prop2: 'issuer', }, { name: this.translate.instant('CN'), prop1: 'common', name2: this.translate.instant('SAN'), prop2: 'san', }, ], parent: this, add() { this.parent.modalService.openInSlideIn(CertificateAddComponent, 'csr'); }, edit(row: Certificate) { this.parent.modalService.openInSlideIn(CertificateEditComponent, row.id); }, }, }, { name: 'certificate-authorities', tableConf: { title: this.translate.instant('Certificate Authorities'), queryCall: 'certificateauthority.query', deleteCall: 'certificateauthority.delete', complex: true, dataSourceHelper: this.caDataSourceHelper, getActions: this.caActions.bind(this), columns: [ { name: this.translate.instant('Name'), prop1: 'name', name2: this.translate.instant('Issuer'), prop2: 'issuer', }, { name: this.translate.instant('From'), prop1: 'from', name2: this.translate.instant('Until'), prop2: 'until', }, { name: this.translate.instant('CN'), prop1: 'common', name2: this.translate.instant('SAN'), prop2: 'san', }, { name: this.translate.instant('Status'), prop1: 'revoked', iconTooltip: this.translate.instant('Revoked'), getIcon: (element: Certificate, prop: keyof Certificate): string => { if (element[prop]) { return 'block'; } return ''; }, }, ], parent: this, add() { this.parent.modalService.openInSlideIn(CertificateAuthorityAddComponent); }, edit(row: CertificateAuthority) { this.parent.modalService.openInSlideIn(CertificateAuthorityEditComponent, row.id); }, delete(row: CertificateAuthority, table: TableComponent) { if (row.signed_certificates > 0) { this.parent.dialogService.confirm({ title: helptextSystemCa.delete_error.title, message: helptextSystemCa.delete_error.message, hideCheckBox: true, buttonMsg: helptextSystemCa.delete_error.button, hideCancel: true, }); } else { this.parent.tableService.delete(table, row); } }, }, }, { name: 'acme-dns', tableConf: { title: this.translate.instant('ACME DNS-Authenticators'), queryCall: 'acme.dns.authenticator.query', deleteCall: 'acme.dns.authenticator.delete', complex: false, columns: [ { name: this.translate.instant('Name'), prop: 'name' }, { name: this.translate.instant('Authenticator'), prop: 'authenticator' }, ], parent: this, add() { this.parent.modalService.openInSlideIn(AcmednsFormComponent); }, edit(row: CertificateAuthority) { this.parent.modalService.openInSlideIn(AcmednsFormComponent, row.id); }, }, }, ]; } certificatesDataSourceHelper(res: Certificate[]): Certificate[] { res.forEach((certificate) => { if (_.isObject(certificate.issuer)) { certificate.issuer = certificate.issuer.name; } }); return res.filter((item) => item.certificate !== null); } csrDataSourceHelper(res: Certificate[]): Certificate[] { return res.filter((item) => item.CSR !== null); } caDataSourceHelper(res: CertificateAuthority[]): CertificateAuthority[] { res.forEach((row) => { if (_.isObject(row.issuer)) { row.issuer = row.issuer.name; } }); return res; } certificateActions(): AppTableAction[] { this.downloadActions = [ { icon: 'save_alt', matTooltip: this.translate.instant('Download'), name: 'download', onClick: (rowinner: Certificate) => { const path = rowinner.CSR ? rowinner.csr_path : rowinner.certificate_path; const fileName = rowinner.name + '.crt'; // what about for a csr? this.ws.call('core.download', ['filesystem.get', [path], fileName]).pipe(untilDestroyed(this)).subscribe( (res) => { const url = res[1]; const mimetype = 'application/x-x509-user-cert'; this.storage.streamDownloadFile(this.http, url, fileName, mimetype) .pipe(untilDestroyed(this)) .subscribe((file) => { this.storage.downloadBlob(file, fileName); }, (err) => { this.dialogService.errorReport(helptextSystemCertificates.list.download_error_dialog.title, helptextSystemCertificates.list.download_error_dialog.cert_message, `${err.status} - ${err.statusText}`); }); }, (err) => { new EntityUtils().handleWsError(this, err, this.dialogService); }, ); const keyName = rowinner.name + '.key'; this.ws.call('core.download', ['filesystem.get', [rowinner.privatekey_path], keyName]).pipe(untilDestroyed(this)).subscribe( (res) => { const url = res[1]; const mimetype = 'text/plain'; this.storage.streamDownloadFile(this.http, url, keyName, mimetype) .pipe(untilDestroyed(this)) .subscribe((file) => { this.storage.downloadBlob(file, keyName); }, (err) => { this.dialogService.errorReport(helptextSystemCertificates.list.download_error_dialog.title, helptextSystemCertificates.list.download_error_dialog.key_message, `${err.status} - ${err.statusText}`); }); }, (err) => { new EntityUtils().handleWsError(this, err, this.dialogService); }, ); }, }, ]; const revokeAction = { icon: 'undo', name: 'revoke', matTooltip: this.translate.instant('Revoke'), onClick: (rowInner: Certificate) => { this.dialogService.confirm({ title: this.translate.instant('Revoke Certificate'), message: this.translate.instant('This is a one way action and cannot be reversed. Are you sure you want to revoke this Certificate?'), buttonMsg: this.translate.instant('Revoke'), cancelMsg: this.translate.instant('Cancel'), hideCheckBox: true, }) .pipe(filter(Boolean), untilDestroyed(this)) .subscribe(() => { this.dialogRef = this.dialog.open(EntityJobComponent, { data: { title: this.translate.instant('Revoking Certificate') } }); this.dialogRef.componentInstance.setCall('certificate.update', [rowInner.id, { revoked: true }]); this.dialogRef.componentInstance.submit(); this.dialogRef.componentInstance.success.pipe(untilDestroyed(this)).subscribe(() => { this.dialog.closeAll(); }); this.dialogRef.componentInstance.failure.pipe(untilDestroyed(this)).subscribe((res) => { this.dialog.closeAll(); new EntityUtils().handleWsError(null, res, this.dialogService); }); }); }, }; return [revokeAction, ...this.downloadActions]; } csrActions(): AppTableAction<Certificate>[] { const csrRowActions = [...this.downloadActions]; const acmeAction = { icon: 'build', name: 'create_ACME', matTooltip: this.translate.instant('Create ACME Certificate'), onClick: (rowinner: Certificate) => { this.modalService.openInSlideIn(CertificateAcmeAddComponent, rowinner.id); }, }; return [acmeAction, ...csrRowActions]; } caActions(): AppTableAction<CertificateAuthority>[] { const caRowActions = [...this.downloadActions]; const acmeAction = { icon: 'beenhere', name: 'sign_CSR', matTooltip: helptextSystemCa.list.action_sign, onClick: (rowinner: CertificateAuthority) => { this.dialogService.dialogForm(this.signCSRFormConf); this.caId = rowinner.id; }, }; const revokeAction = { icon: 'undo', name: 'revoke', matTooltip: 'Revoke', onClick: (rowInner: CertificateAuthority) => { this.dialogService.confirm({ title: this.translate.instant('Revoke Certificate Authority'), message: this.translate.instant('Revoking this CA will revoke the complete CA chain. This is a one way action and cannot be reversed. Are you sure you want to revoke this CA?'), buttonMsg: this.translate.instant('Revoke'), cancelMsg: this.translate.instant('Cancel'), hideCheckBox: true, }) .pipe(filter(Boolean), untilDestroyed(this)) .subscribe(() => { this.dialogRef = this.dialog.open(EntityJobComponent, { data: { title: this.translate.instant('Revoking Certificate') } }); this.dialogRef.componentInstance.setCall('certificateauthority.update', [rowInner.id, { revoked: true }]); this.dialogRef.componentInstance.submit(); this.dialogRef.componentInstance.success.pipe(untilDestroyed(this)).subscribe(() => { this.dialog.closeAll(); }); this.dialogRef.componentInstance.failure.pipe(untilDestroyed(this)).subscribe((res) => { this.dialog.closeAll(); new EntityUtils().handleWsError(null, res, this.dialogService); }); }); }, }; return [acmeAction, revokeAction, ...caRowActions]; } openForm(id: number): void { setTimeout(() => { this.modalService.openInSlideIn(CertificateAcmeAddComponent, id); }, 200); } protected signCSRFieldConf: FieldConfig[] = [ { type: 'select', name: 'csr_cert_id', placeholder: helptextSystemCa.sign.csr_cert_id.placeholder, tooltip: helptextSystemCa.sign.csr_cert_id.tooltip, required: true, options: this.unsignedCAs, }, { type: 'input', name: 'name', placeholder: helptextSystemCa.edit.name.placeholder, tooltip: helptextSystemCa.sign.name.tooltip, }, ]; signCSRFormConf: DialogFormConfiguration = { title: helptextSystemCa.sign.fieldset_certificate, fieldConfig: this.signCSRFieldConf, method_ws: 'certificateauthority.ca_sign_csr', saveButtonText: helptextSystemCa.sign.sign, customSubmit: (entityDialog) => this.doSignCsr(entityDialog), parent: this, }; doSignCsr(entityDialog: EntityDialogComponent<this>): void { const payload = { ca_id: this.caId, csr_cert_id: entityDialog.formGroup.controls.csr_cert_id.value, name: entityDialog.formGroup.controls.name.value, }; entityDialog.loader.open(); entityDialog.ws.call('certificateauthority.ca_sign_csr', [payload]).pipe(untilDestroyed(this)).subscribe(() => { entityDialog.loader.close(); this.dialogService.closeAllDialogs(); this.getCards(); }, (err: WebsocketError) => { entityDialog.loader.close(); this.dialogService.errorReport(helptextSystemCa.error, err.reason, err.trace.formatted); }); } }