From 5a33735fd7186a7725d188c89b9414bde8a668b8 Mon Sep 17 00:00:00 2001 From: Yunlong Zhang Date: Wed, 14 Mar 2018 16:37:53 -0700 Subject: [PATCH 01/17] added basic certificate model --- app/models/certificate.ts | 56 +++++++++++++++++++++++++++++++++++++++ app/models/index.ts | 1 + 2 files changed, 57 insertions(+) create mode 100644 app/models/certificate.ts diff --git a/app/models/certificate.ts b/app/models/certificate.ts new file mode 100644 index 0000000000..9b5533226c --- /dev/null +++ b/app/models/certificate.ts @@ -0,0 +1,56 @@ +import { Model, Prop, Record } from "@batch-flask/core"; +import { NameValuePair } from "."; + +export interface DeleteCertificateError { + code: string; + message: string; + values: NameValuePair[]; +} + +export interface CertificateAttributes { + url: string; + thumbprint: string; + thumbprintAlgorithm: string; + publicData: string; + state: CertificateState; + stateTransitionTime: Date; + previousState: CertificateState; + previousStateTransitionTime: Date; + deleteCertificateError: DeleteCertificateError; +} +/** + * Class for displaying Batch certificate information. + */ +@Model() +export class Certificate extends Record { + @Prop() public url: string; + @Prop() public thumbprint: string; + @Prop() public thumbprintAlgorithm: string; + @Prop() public publicData: string; + @Prop() public state: CertificateState; + @Prop() public stateTransitionTime: Date; + @Prop() public previousState: CertificateState; + @Prop() public previousStateTransitionTime: Date; + @Prop() public deleteCertificateError: DeleteCertificateError; + + /** + * If the certificate can be cancelled a failed deletion + * i.e. A active and deleting certificate cannot be reactived. + */ + public readonly reactivable: boolean; + + constructor(data: Partial = {}) { + super(data); + this.reactivable = this.state === CertificateState.deletefailed; + } + + public get routerLink(): string[] { + return ["/certificates", this.thumbprint]; + } +} + +export enum CertificateState { + active = "active", + deletefailed = "deletefailed", + deleting = "deleting", +} diff --git a/app/models/index.ts b/app/models/index.ts index 21c588395f..a5d291a96b 100644 --- a/app/models/index.ts +++ b/app/models/index.ts @@ -14,6 +14,7 @@ export * from "./auto-user"; export * from "./autoscale-formula"; export * from "./batch-quotas"; export * from "./blob-container"; +export * from "./certificate"; export * from "./certificate-reference"; export * from "./cloud-service-configuration"; export * from "./compute-node-error"; From bea3902d99ab75e13172c348cfa909891834b1af Mon Sep 17 00:00:00 2001 From: Yunlong Zhang Date: Wed, 14 Mar 2018 17:11:22 -0700 Subject: [PATCH 02/17] -Added certificate proxy class -Fixed a small error previously made in job schedule proxy --- app/services/batch-api/batch-client-proxy.ts | 7 +++ app/services/batch-api/certificateProxy.ts | 64 ++++++++++++++++++++ app/services/batch-api/jobScheduleProxy.ts | 2 +- 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 app/services/batch-api/certificateProxy.ts diff --git a/app/services/batch-api/batch-client-proxy.ts b/app/services/batch-api/batch-client-proxy.ts index 7f317d434b..d7213d6c02 100644 --- a/app/services/batch-api/batch-client-proxy.ts +++ b/app/services/batch-api/batch-client-proxy.ts @@ -1,6 +1,7 @@ import { BatchServiceClient } from "azure-batch"; import AccountProxy from "./accountProxy"; +import { CertificateProxy } from "./certificateProxy"; import { FileProxy } from "./fileProxy"; import { JobProxy } from "./jobProxy"; import { JobScheduleProxy } from "./jobScheduleProxy"; @@ -22,6 +23,7 @@ export class BatchClientProxy { private _job: JobProxy; private _jobSchedule: JobScheduleProxy; private _pool: PoolProxy; + private _certificate: CertificateProxy; private _task: TaskProxy; private _node: NodeProxy; @@ -33,6 +35,7 @@ export class BatchClientProxy { this._job = new JobProxy(this.client); this._jobSchedule = new JobScheduleProxy(this.client); this._pool = new PoolProxy(this.client); + this._certificate = new CertificateProxy(this.client); this._task = new TaskProxy(this.client); this._node = new NodeProxy(this.client); } @@ -61,6 +64,10 @@ export class BatchClientProxy { return this.checkProxy(this._pool); } + get certificate(): CertificateProxy { + return this.checkProxy(this._certificate); + } + get node(): NodeProxy { return this.checkProxy(this._node); } diff --git a/app/services/batch-api/certificateProxy.ts b/app/services/batch-api/certificateProxy.ts new file mode 100644 index 0000000000..187483b4eb --- /dev/null +++ b/app/services/batch-api/certificateProxy.ts @@ -0,0 +1,64 @@ +import { BatchServiceClient, BatchServiceModels } from "azure-batch"; + +import { ListProxy, mapGet, wrapOptions } from "./shared"; + +export class CertificateProxy { + + constructor(private client: BatchServiceClient) { + } + + /** + * Adds a certificate to the specified account. + * http://azure.github.io/azure-sdk-for-node/azure-batch/latest/CertificateOperations.html#add + * @param certificate: The certificate object + * @param options: Optional Parameters. + */ + public add(certificate: any, options?: any): Promise { + return this.client.certificate.add(certificate, wrapOptions(options)); + } + + /** + * Deletes a certificate from the specified account. + * http://azure.github.io/azure-sdk-for-node/azure-batch/latest/CertificateOperations.html#deleteMethod + * @param thumbprintAlgorithm: The algorithm used to derive the thumbprint parameter. This must be sha1. + * @param thumbprint: The thumbprint of the certificate to be deleted. + * @param options: Optional Parameters. + */ + public delete(thumbprintAlgorithm: string, thumbprint: string, options?: any): Promise { + return this.client.certificate.deleteMethod(thumbprintAlgorithm, thumbprint, wrapOptions(options)); + } + + /** + * Cancels a failed deletion of a certificate from the specified account. + * http://azure.github.io/azure-sdk-for-node/azure-batch/latest/CertificateOperations.html#cancelDeletion + * @param thumbprintAlgorithm: The algorithm used to derive the thumbprint parameter. This must be sha1. + * @param thumbprint: The thumbprint of the certificate being deleted + * @param options: Optional Parameters. + */ + public cancelDeletion(thumbprintAlgorithm: string, thumbprint: string, options?: any): Promise { + return this.client.certificate.cancelDeletion(thumbprintAlgorithm, thumbprint, wrapOptions(options)); + } + + /** + * Gets information about the specified certificate. + * http://azure.github.io/azure-sdk-for-node/azure-batch/latest/CertificateOperations.html#get + * @param thumbprintAlgorithm: The algorithm used to derive the thumbprint parameter. This must be sha1. + * @param thumbprint: The thumbprint of the certificate to get. + * @param options: Optional Parameters. + */ + public get(thumbprintAlgorithm: string, thumbprint: string, options?: BatchServiceModels.CertificateGetOptions) + : Promise { + return mapGet(this.client.certificate.get(thumbprintAlgorithm, thumbprint, wrapOptions({ + certificateGetOptions: options, + }))); + } + + /** + * Lists all of the certificates that have been added to the specified + * http://azure.github.io/azure-sdk-for-node/azure-batch/latest/CertificateOperations.html#list + * @param options: Optional Parameters. + */ + public list(options?: BatchServiceModels.CertificateListOptions) { + return new ListProxy(this.client.certificate, null, wrapOptions({ certificateListOptions: options })); + } +} diff --git a/app/services/batch-api/jobScheduleProxy.ts b/app/services/batch-api/jobScheduleProxy.ts index 8d8f525270..c08c9624af 100644 --- a/app/services/batch-api/jobScheduleProxy.ts +++ b/app/services/batch-api/jobScheduleProxy.ts @@ -63,7 +63,7 @@ export class JobScheduleProxy { * @param options: Optional Parameters. */ public list(options?: BatchServiceModels.JobScheduleListOptions) { - return new ListProxy(this.client.jobSchedule, null, wrapOptions({ jobListOptions: options })); + return new ListProxy(this.client.jobSchedule, null, wrapOptions({ jobScheduleListOptions: options })); } /** From a873991b376f94b722216fcb3bd48af0cea621f7 Mon Sep 17 00:00:00 2001 From: Yunlong Zhang Date: Wed, 14 Mar 2018 17:24:20 -0700 Subject: [PATCH 03/17] Added certificate service class and dto model --- app/app.module.ts | 2 + app/models/certificate.ts | 10 +- app/models/dtos/certificate-create.dto.ts | 15 +++ app/models/dtos/index.ts | 1 + app/services/certificate.service.ts | 125 ++++++++++++++++++ app/services/index.ts | 1 + .../core/record/navigable-record.ts | 1 + 7 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 app/models/dtos/certificate-create.dto.ts create mode 100644 app/services/certificate.service.ts diff --git a/app/app.module.ts b/app/app.module.ts index 8bffdf0579..275cfa12e7 100644 --- a/app/app.module.ts +++ b/app/app.module.ts @@ -52,6 +52,7 @@ import { BatchClientService, BatchLabsService, CacheDataService, + CertificateService, CommandService, ComputeService, FileService, @@ -134,6 +135,7 @@ const graphApiServices = [AADApplicationService, AADGraphHttpService, MsGraphHtt BatchClientService, BatchLabsService, CacheDataService, + CertificateService, CommandService, CommonModule, ComputeService, diff --git a/app/models/certificate.ts b/app/models/certificate.ts index 9b5533226c..6d9510970d 100644 --- a/app/models/certificate.ts +++ b/app/models/certificate.ts @@ -1,4 +1,4 @@ -import { Model, Prop, Record } from "@batch-flask/core"; +import { Model, NavigableRecord, Prop, Record } from "@batch-flask/core"; import { NameValuePair } from "."; export interface DeleteCertificateError { @@ -22,7 +22,7 @@ export interface CertificateAttributes { * Class for displaying Batch certificate information. */ @Model() -export class Certificate extends Record { +export class Certificate extends Record implements NavigableRecord { @Prop() public url: string; @Prop() public thumbprint: string; @Prop() public thumbprintAlgorithm: string; @@ -44,6 +44,12 @@ export class Certificate extends Record { this.reactivable = this.state === CertificateState.deletefailed; } + // There is no id field in certificate, we need mock an id field for + // pin/unpin favorite function passing NavigableRecord + public get id(): string { + return this.thumbprint; + } + public get routerLink(): string[] { return ["/certificates", this.thumbprint]; } diff --git a/app/models/dtos/certificate-create.dto.ts b/app/models/dtos/certificate-create.dto.ts new file mode 100644 index 0000000000..f2d2810519 --- /dev/null +++ b/app/models/dtos/certificate-create.dto.ts @@ -0,0 +1,15 @@ +import { Dto, DtoAttr } from "@batch-flask/core"; + +export type CertificateFormat = "cer" | "pfx"; + +export class CertificateCreateDto extends Dto { + @DtoAttr() public thumbprintAlgorithm?: string; + + @DtoAttr() public thumbprint?: string; + + @DtoAttr() public password?: string; + + @DtoAttr() public data?: string; + + @DtoAttr() public certificateFormat?: CertificateFormat; +} diff --git a/app/models/dtos/index.ts b/app/models/dtos/index.ts index 9f639d381c..eb3bc1718e 100644 --- a/app/models/dtos/index.ts +++ b/app/models/dtos/index.ts @@ -5,6 +5,7 @@ export * from "./account-create.dto"; export * from "./account-patch.dto"; export * from "./application-package-reference.dto"; +export * from "./certificate-create.dto"; export * from "./container-setup.dto"; export * from "./file-group-create.dto"; export * from "./file-group-options.dto"; diff --git a/app/services/certificate.service.ts b/app/services/certificate.service.ts new file mode 100644 index 0000000000..5deada3222 --- /dev/null +++ b/app/services/certificate.service.ts @@ -0,0 +1,125 @@ +import { Injectable } from "@angular/core"; +import { List } from "immutable"; +import { Observable, Subject } from "rxjs"; + +import { Certificate } from "app/models"; +import { CertificateCreateDto } from "app/models/dtos"; +import { Constants, log } from "app/utils"; +import { BatchClientService } from "./batch-client.service"; +import { + BatchEntityGetter, BatchListGetter, ContinuationToken, + DataCache, EntityView, ListOptionsAttributes, ListView, +} from "./core"; +import { ServiceBase } from "./service-base"; + +export interface CertificateListParams { +} + +export interface CertificateParams { + thumbprint?: string; + thumbprintAlgorithm?: string; +} + +export interface CertificateListOptions extends ListOptionsAttributes { +} + +const defaultThumbprintAlgorithm = "sha1"; + +@Injectable() +export class CertificateService extends ServiceBase { + /** + * Triggered only when a certificate is added through this app. + * Used to notify the list of a new item + */ + public onCertificateAdded = new Subject(); + public cache = new DataCache(); + + private _basicProperties: string = "thumbprint,thumbprintAlgorithm,state,stateTransitionTime"; + private _getter: BatchEntityGetter; + private _listGetter: BatchListGetter; + + constructor(batchService: BatchClientService) { + super(batchService); + + this._getter = new BatchEntityGetter(Certificate, this.batchService, { + cache: () => this.cache, + getFn: (client, params: CertificateParams) => { + const algorithm = params.thumbprintAlgorithm || defaultThumbprintAlgorithm; + return client.certificate.get(algorithm, params.thumbprint); + }, + }); + + this._listGetter = new BatchListGetter(Certificate, this.batchService, { + cache: () => this.cache, + list: (client, params: CertificateListParams, options) => { + return client.certificate.list({ certificateListOptions: options }); + }, + listNext: (client, nextLink: string) => client.certificate.listNext(nextLink), + }); + } + + public get basicProperties(): string { + return this._basicProperties; + } + + public list(options?: any, forceNew?: boolean); + public list(nextLink: ContinuationToken); + public list(nextLinkOrOptions: any, options = {}, forceNew = false) { + if (nextLinkOrOptions.nextLink) { + return this._listGetter.fetch(nextLinkOrOptions); + } else { + return this._listGetter.fetch({}, options, forceNew); + } + } + + public listView(options: ListOptionsAttributes = {}): ListView { + return new ListView({ + cache: () => this.cache, + getter: this._listGetter, + initialOptions: options, + }); + } + + public listAll(options: CertificateListOptions = {}): Observable> { + return this._listGetter.fetchAll({}, options); + } + + public get(thumbprint: string, options: any = {}, thumbprintAlgorithm?: string): Observable { + const algorithm = thumbprintAlgorithm || defaultThumbprintAlgorithm; + return this._getter.fetch({ thumbprintAlgorithm: algorithm, thumbprint: thumbprint }); + } + + /** + * Create an entity view for a certificate + */ + public view(): EntityView { + return new EntityView({ + cache: () => this.cache, + getter: this._getter, + poll: Constants.PollRate.entity, + }); + } + + public delete(thumbprint: string, options: any = {}, thumbprintAlgorithm?: string) + : Observable<{}> { + return this.callBatchClient((client) => { + const algorithm = thumbprintAlgorithm || defaultThumbprintAlgorithm; + return client.certificate.delete(algorithm, thumbprint, options); + }, (error) => { + log.error(`Error cancel delete certificate: ${thumbprint}`, error); + }); + } + + public add(certificate: CertificateCreateDto, options: any = {}): Observable<{}> { + return this.callBatchClient((client) => client.certificate.add(certificate.toJS(), options)); + } + + public cancelDelete(thumbprint: string, options: any = {}, thumbprintAlgorithm?: string) { + return this.callBatchClient((client) => { + const algorithm = thumbprintAlgorithm || defaultThumbprintAlgorithm; + return client.certificate.cancelDelete(algorithm, thumbprint, options); + }, (error) => { + log.error(`Error cancel delete certificate: ${thumbprint}`, error); + }); + } +} diff --git a/app/services/index.ts b/app/services/index.ts index 3cdbee402b..0c1e7e673a 100644 --- a/app/services/index.ts +++ b/app/services/index.ts @@ -7,6 +7,7 @@ export * from "./azure-http.service"; export * from "./arm-http.service"; export * from "./batch-labs.service"; export * from "./cache-data.service"; +export * from "./certificate.service"; export * from "./compute.service"; export * from "./file-service"; export * from "./fs.service"; diff --git a/src/@batch-flask/core/record/navigable-record.ts b/src/@batch-flask/core/record/navigable-record.ts index e70c0f38c8..fe3ee7d1d6 100644 --- a/src/@batch-flask/core/record/navigable-record.ts +++ b/src/@batch-flask/core/record/navigable-record.ts @@ -3,6 +3,7 @@ export enum PinnedEntityType { Job, JobSchedule, Pool, + Certificate, FileGroup, } From 2bf6d41ebe096d1d04ee15678d0c460bce09f5ce Mon Sep 17 00:00:00 2001 From: Yunlong Zhang Date: Thu, 15 Mar 2018 10:03:10 -0700 Subject: [PATCH 04/17] added certificate and related decorater added certificate components in app module and routes --- app/app.module.ts | 3 +- app/app.routes.ts | 11 +++++ .../certificate/certificate.module.ts | 29 +++++++++++++ app/components/shared/main-navigation.html | 5 +++ .../decorators/certificate-decorator.ts | 43 +++++++++++++++++++ .../delete-certificate-error-decorator.ts | 39 +++++++++++++++++ app/models/decorators/index.ts | 2 + 7 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 app/components/certificate/certificate.module.ts create mode 100644 app/models/decorators/certificate-decorator.ts create mode 100644 app/models/decorators/delete-certificate-error-decorator.ts diff --git a/app/app.module.ts b/app/app.module.ts index 275cfa12e7..70b3fe8684 100644 --- a/app/app.module.ts +++ b/app/app.module.ts @@ -17,6 +17,7 @@ import { MainNavigationComponent } from "app/components/shared/main-navigation.c import { BaseModule } from "@batch-flask/ui"; import { AccountModule } from "app/components/account/account.module"; import { ApplicationModule } from "app/components/application/application.module"; +import { CertificateModule } from "app/components/certificate/certificate.module"; import { DataModule } from "app/components/data/data.module"; import { FileModule } from "app/components/file/file.module"; import { JobScheduleModule } from "app/components/job-schedule/job-schedule.module"; @@ -90,7 +91,7 @@ import { } from "./services"; const modules = [ - AccountModule, ApplicationModule, DataModule, + AccountModule, ApplicationModule, CertificateModule, DataModule, FileModule, JobModule, JobScheduleModule, NodeModule, PoolModule, SettingsModule, TaskModule, MarketModule, LayoutModule, MiscModule, diff --git a/app/app.routes.ts b/app/app.routes.ts index e17a44a8cb..6dd413d634 100644 --- a/app/app.routes.ts +++ b/app/app.routes.ts @@ -15,6 +15,8 @@ import { AccountHomeComponent } from "./components/account/home/account-home.com import { AccountMonitoringHomeComponent } from "./components/account/monitoring"; import { ApplicationDefaultComponent, ApplicationDetailsComponent } from "./components/application/details"; import { ApplicationHomeComponent } from "./components/application/home/application-home.component"; +import { CertificateDefaultComponent, CertificateDetailsComponent } from "./components/certificate/details"; +import { CertificateHomeComponent } from "./components/certificate/home/certificate-home.component"; import { DataDefaultComponent, DataDetailsComponent } from "./components/data/details"; import { DataHomeComponent } from "./components/data/home/data-home.component"; import { JobScheduleDefaultComponent, JobScheduleDetailsComponent } from "./components/job-schedule/details"; @@ -73,6 +75,15 @@ export const routes: Routes = [ { path: ":id", component: PoolDetailsComponent }, // pools/{pool.id} ], }, + { + path: "certificates", + canActivate: [NavigationGuard], + component: CertificateHomeComponent, + children: [ + { path: "", component: CertificateDefaultComponent }, // pools/ + { path: ":thumbprint", component: CertificateDetailsComponent }, // pools/{pool.id} + ], + }, { path: "market", canActivate: [NavigationGuard], diff --git a/app/components/certificate/certificate.module.ts b/app/components/certificate/certificate.module.ts new file mode 100644 index 0000000000..24a85b0504 --- /dev/null +++ b/app/components/certificate/certificate.module.ts @@ -0,0 +1,29 @@ +import { NgModule } from "@angular/core"; + +import { commonModules } from "app/common"; +// import { JobScheduleActionModule } from "./action"; +// import { JobScheduleAdvancedFilterComponent } from "./browse/filter/job-schedule-advanced-filter.component"; +// import { JobScheduleListComponent } from "./browse/job-schedule-list.component"; +// import { JobScheduleDetailsModule } from "./details/job-schedule-details.module"; +// import { JobScheduleHomeComponent } from "./home/job-schedule-home.component"; + +const components = [ + // JobScheduleAdvancedFilterComponent, + // JobScheduleHomeComponent, + // JobScheduleListComponent, +]; + +const modules = [ + // JobScheduleActionModule, JobScheduleDetailsModule, + ...commonModules, +]; + +@NgModule({ + declarations: components, + exports: [...modules, ...components], + imports: [...modules], + entryComponents: [ + ], +}) +export class CertificateModule { +} diff --git a/app/components/shared/main-navigation.html b/app/components/shared/main-navigation.html index 4dd99a708d..54f1cbc32f 100644 --- a/app/components/shared/main-navigation.html +++ b/app/components/shared/main-navigation.html @@ -24,6 +24,11 @@
Packages
+
  • + +
    Cert
    +
    +
  • Data
    diff --git a/app/models/decorators/certificate-decorator.ts b/app/models/decorators/certificate-decorator.ts new file mode 100644 index 0000000000..496c72bf6b --- /dev/null +++ b/app/models/decorators/certificate-decorator.ts @@ -0,0 +1,43 @@ +import { Certificate, CertificateState } from "app/models"; +import { DeleteCertificateErrorDecorator } from "app/models/decorators"; +import { DecoratorBase } from "app/utils/decorators"; + +export class CertificateDecorator extends DecoratorBase { + public state: string; + public stateTransitionTime: string; + public previousState: string; + public previousStateTransitionTime: string; + public stateIcon: string; + public thumbprintAlgorithm: string; + public thumbprint: string; + public publicData: string; + + public deleteCertificateError: DeleteCertificateErrorDecorator; + + constructor(certificate: Certificate) { + super(certificate); + + this.state = this.stateField(certificate.state); + this.stateTransitionTime = this.dateField(certificate.stateTransitionTime); + this.stateIcon = this._getStateIcon(certificate.state); + this.previousState = this.stateField(certificate.previousState); + this.previousStateTransitionTime = this.dateField(certificate.previousStateTransitionTime); + this.thumbprintAlgorithm = this.stringField(certificate.thumbprintAlgorithm); + this.thumbprint = this.stringField(certificate.thumbprint); + this.publicData = this.stringField(certificate.publicData); + this.deleteCertificateError = new DeleteCertificateErrorDecorator( + certificate.deleteCertificateError || {} as any); + } + + private _getStateIcon(state: CertificateState): string { + switch (state) { + case CertificateState.active: + return "fa-cog"; + case CertificateState.deleting: + case CertificateState.deletefailed: + return "fa-ban"; + default: + return "fa-question-circle-o"; + } + } +} diff --git a/app/models/decorators/delete-certificate-error-decorator.ts b/app/models/decorators/delete-certificate-error-decorator.ts new file mode 100644 index 0000000000..650ef7a58c --- /dev/null +++ b/app/models/decorators/delete-certificate-error-decorator.ts @@ -0,0 +1,39 @@ +import { DeleteCertificateError, NameValuePair } from "app/models"; +import { DecoratorBase } from "app/utils/decorators"; + +export class DeleteCertificateErrorDecorator extends DecoratorBase { + public code: string; + public message: string; + public details: string; + + /** + * Combination of the error in a small summary to be displayed in grid for example + */ + public summary: string; + + /** + * Flag to know if the error exists + */ + public exists: boolean; + + constructor(error: DeleteCertificateError) { + super(error); + + this.exists = Boolean(error); + this.code = this.stringField(error.code); + this.message = this.stringField(error.message); + this.details = this._getDetails(error.values); + this.summary = this.exists + ? `code: ${error.code}, message: ${error.message}` + : ""; + } + + private _getDetails(values: NameValuePair[]): string { + if (values && values.length > 0) { + return values.map(value => { + return `${value.name}: ${value.value}`; + }).join("
    "); } + + return ""; + } +} diff --git a/app/models/decorators/index.ts b/app/models/decorators/index.ts index 3804c6d1e9..1be4332185 100644 --- a/app/models/decorators/index.ts +++ b/app/models/decorators/index.ts @@ -4,6 +4,8 @@ export * from "./compute-node-info-decorator"; export * from "./container-configuration-decorator"; export * from "./container-decorator"; export * from "./container-lease-decorator"; +export * from "./certificate-decorator"; +export * from "./delete-certificate-error-decorator"; export * from "./failure-info-decorator"; export * from "./image-reference-decorator"; export * from "./job-constraints-decorator"; From 7186adba8b9b2765194df64cbefe5e7754e72400 Mon Sep 17 00:00:00 2001 From: Yunlong Zhang Date: Thu, 15 Mar 2018 10:53:33 -0700 Subject: [PATCH 05/17] First round added certificate home list pages --- app/app.routes.ts | 4 +- .../browse/certificate-list.component.ts | 167 ++++++++++++++++++ .../certificate/browse/certificate-list.html | 34 ++++ .../certificate-advanced-filter.component.ts | 30 ++++ .../filter/certificate-advanced-filter.html | 7 + .../certificate/certificate.module.ts | 19 +- .../details/certificate-default.component.ts | 18 ++ .../details/certificate-details.component.ts | 107 +++++++++++ .../details/certificate-details.html | 18 ++ .../details/certificate-details.module.ts | 26 +++ .../details/certificate-details.scss | 5 + app/components/certificate/details/index.ts | 3 + .../home/certificate-home.component.ts | 49 +++++ .../certificate/home/certificate-home.html | 12 ++ 14 files changed, 488 insertions(+), 11 deletions(-) create mode 100644 app/components/certificate/browse/certificate-list.component.ts create mode 100644 app/components/certificate/browse/certificate-list.html create mode 100644 app/components/certificate/browse/filter/certificate-advanced-filter.component.ts create mode 100644 app/components/certificate/browse/filter/certificate-advanced-filter.html create mode 100644 app/components/certificate/details/certificate-default.component.ts create mode 100644 app/components/certificate/details/certificate-details.component.ts create mode 100644 app/components/certificate/details/certificate-details.html create mode 100644 app/components/certificate/details/certificate-details.module.ts create mode 100644 app/components/certificate/details/certificate-details.scss create mode 100644 app/components/certificate/details/index.ts create mode 100644 app/components/certificate/home/certificate-home.component.ts create mode 100644 app/components/certificate/home/certificate-home.html diff --git a/app/app.routes.ts b/app/app.routes.ts index 6dd413d634..0eee512ae3 100644 --- a/app/app.routes.ts +++ b/app/app.routes.ts @@ -80,8 +80,8 @@ export const routes: Routes = [ canActivate: [NavigationGuard], component: CertificateHomeComponent, children: [ - { path: "", component: CertificateDefaultComponent }, // pools/ - { path: ":thumbprint", component: CertificateDetailsComponent }, // pools/{pool.id} + { path: "", component: CertificateDefaultComponent }, // thumbprint/ + { path: ":thumbprint", component: CertificateDetailsComponent }, // certificate/{certificate.thumbprint} ], }, { diff --git a/app/components/certificate/browse/certificate-list.component.ts b/app/components/certificate/browse/certificate-list.component.ts new file mode 100644 index 0000000000..8900565c78 --- /dev/null +++ b/app/components/certificate/browse/certificate-list.component.ts @@ -0,0 +1,167 @@ +import { + ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, forwardRef, +} from "@angular/core"; +import { FormControl } from "@angular/forms"; +// import { MatDialog } from "@angular/material"; +import { ActivatedRoute, Router } from "@angular/router"; +import { List } from "immutable"; +import { Observable, Subscription } from "rxjs"; + +import { Filter, autobind } from "@batch-flask/core"; +import { ListBaseComponent, ListSelection } from "@batch-flask/core/list"; +// import { BackgroundTaskService } from "@batch-flask/ui/background-task"; +import { ContextMenu, ContextMenuItem } from "@batch-flask/ui/context-menu"; +import { LoadingStatus } from "@batch-flask/ui/loading"; +import { QuickListItemStatus } from "@batch-flask/ui/quick-list"; +import { Certificate, CertificateState } from "app/models"; +import { CertificateListParams, CertificateService, PinnedEntityService } from "app/services"; +import { ListView } from "app/services/core"; +import { ComponentUtils } from "app/utils"; +// import { +// DeleteCertificateAction, +// DeleteCertificateDialogComponent, +// DisableCertificateDialogComponent, +// EnableCertificateDialogComponent, +// TerminateCertificateDialogComponent, +// } from "../action"; + +@Component({ + selector: "bl-certificate-list", + templateUrl: "certificate-list.html", + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [{ + provide: ListBaseComponent, + useExisting: forwardRef(() => CertificateListComponent), + }], +}) +export class CertificateListComponent extends ListBaseComponent implements OnInit, OnDestroy { + public certificates: List; + public LoadingStatus = LoadingStatus; + + public data: ListView; + public searchQuery = new FormControl(); + + private _baseOptions = {}; + private _onCertificateAddedSub: Subscription; + + constructor( + router: Router, + activatedRoute: ActivatedRoute, + changeDetector: ChangeDetectorRef, + // private dialog: MatDialog, + private certificateService: CertificateService, + private pinnedEntityService: PinnedEntityService, + // private taskManager: BackgroundTaskService + ) { + super(changeDetector); + this.data = this.certificateService.listView(); + ComponentUtils.setActiveItem(activatedRoute, this.data); + this.data.items.subscribe((certificates) => { + this.certificates = certificates; + this.changeDetector.markForCheck(); + }); + + this.data.status.subscribe((status) => { + this.status = status; + }); + + this._onCertificateAddedSub = certificateService.onCertificateAdded.subscribe((certificateId) => { + this.data.loadNewItem(certificateService.get(certificateId)); + }); + } + + public ngOnInit() { + this.data.fetchNext(); + } + + public ngOnDestroy() { + if (this._onCertificateAddedSub) { + this._onCertificateAddedSub.unsubscribe(); + } + } + + @autobind() + public refresh(): Observable { + return this.data.refresh(); + } + + public handleFilter(filter: Filter) { + if (filter.isEmpty()) { + this.data.setOptions({ ...this._baseOptions }); + } else { + this.data.setOptions({ ...this._baseOptions, filter: filter.toOData() }); + } + + this.data.fetchNext(); + } + + public certificateStatus(certificate: Certificate): QuickListItemStatus { + switch (certificate.state) { + case CertificateState.active: + return QuickListItemStatus.normal; + case CertificateState.deletefailed: + return QuickListItemStatus.accent; + case CertificateState.deleting: + return QuickListItemStatus.important; + default: + return QuickListItemStatus.normal; + } + } + + public certificateStatusText(certificate: Certificate): string { + switch (certificate.state) { + case CertificateState.active: + return ""; + default: + return `Certficate is ${certificate.state}`; + } + } + + public onScrollToBottom() { + this.data.fetchNext(); + } + + public contextmenu(certificate: Certificate) { + const deletefailed = certificate.state === CertificateState.deletefailed; + return new ContextMenu([ + new ContextMenuItem({ label: "Delete", click: () => {} /*this.deleteCertificate(certificate)*/ }), + new ContextMenuItem({ + label: "Reactivate", + click: () => {}, // this.terminateCertificate(certificate), + enabled: deletefailed, + }), + new ContextMenuItem({ + label: this.pinnedEntityService.isFavorite(certificate) ? "Unpin favorite" : "Pin to favorites", + click: () => this._pinCertificate(certificate), + }), + ]); + } + + public deleteSelection(selection: ListSelection) { + // this.taskManager.startTask("", (backgroundTask) => { + // const task = new DeleteCertificateAction(this.certificateService, [...selection.keys]); + // task.start(backgroundTask); + // return task.waitingDone; + // }); + } + + public deleteCertificate(certificate: Certificate) { + // const dialogRef = this.dialog.open(DeleteCertificateDialogComponent); + // dialogRef.componentInstance.certificateId = certificate.id; + // dialogRef.afterClosed().subscribe((obj) => { + // this.certificateService.get(certificate.id); + // }); + } + + public trackByFn(index: number, certificate: Certificate) { + return certificate.thumbprint; + } + + private _pinCertificate(certificate: Certificate) { + this.pinnedEntityService.pinFavorite(certificate).subscribe((result) => { + if (result) { + this.pinnedEntityService.unPinFavorite(certificate); + } + }); + } +} diff --git a/app/components/certificate/browse/certificate-list.html b/app/components/certificate/browse/certificate-list.html new file mode 100644 index 0000000000..7e216f8ee2 --- /dev/null +++ b/app/components/certificate/browse/certificate-list.html @@ -0,0 +1,34 @@ + + + + + +
    {{certificate.thumbprint}}
    +
    + {{certificate.state}} +
    +
    +
    + + + + Thumbprint algorithm + Thumbprint + State + + + + {{certificate.thumbprintAlgorithm}} + {{certificate.thumbprint}} + {{certificate.state}} + + +
    + + + + + + No certificates + No certificates match this filter + diff --git a/app/components/certificate/browse/filter/certificate-advanced-filter.component.ts b/app/components/certificate/browse/filter/certificate-advanced-filter.component.ts new file mode 100644 index 0000000000..4d7ae37d32 --- /dev/null +++ b/app/components/certificate/browse/filter/certificate-advanced-filter.component.ts @@ -0,0 +1,30 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Output } from "@angular/core"; + +import { Filter } from "@batch-flask/core"; +import { AdvancedFilter, StatePickerControl } from "@batch-flask/ui/advanced-filter"; +import { CertificateState } from "app/models"; + +@Component({ + selector: "bl-certificate-advanced-filter", + templateUrl: "certificate-advanced-filter.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CertificateAdvancedFilterComponent { + @Output() public change = new EventEmitter(); + + public advancedFilter: AdvancedFilter; + + constructor() { + this.advancedFilter = new AdvancedFilter({ + state: new StatePickerControl("State", [ + CertificateState.active, + CertificateState.deletefailed, + CertificateState.deleting, + ]), + }); + + this.advancedFilter.filterChange.subscribe((filter: Filter) => { + this.change.emit(filter); + }); + } +} diff --git a/app/components/certificate/browse/filter/certificate-advanced-filter.html b/app/components/certificate/browse/filter/certificate-advanced-filter.html new file mode 100644 index 0000000000..73e31a7734 --- /dev/null +++ b/app/components/certificate/browse/filter/certificate-advanced-filter.html @@ -0,0 +1,7 @@ + + +

    Certificate advanced filters

    +
    + +
    +
    diff --git a/app/components/certificate/certificate.module.ts b/app/components/certificate/certificate.module.ts index 24a85b0504..459d5a9173 100644 --- a/app/components/certificate/certificate.module.ts +++ b/app/components/certificate/certificate.module.ts @@ -1,20 +1,21 @@ import { NgModule } from "@angular/core"; import { commonModules } from "app/common"; -// import { JobScheduleActionModule } from "./action"; -// import { JobScheduleAdvancedFilterComponent } from "./browse/filter/job-schedule-advanced-filter.component"; -// import { JobScheduleListComponent } from "./browse/job-schedule-list.component"; -// import { JobScheduleDetailsModule } from "./details/job-schedule-details.module"; -// import { JobScheduleHomeComponent } from "./home/job-schedule-home.component"; +import { CertificateListComponent } from "./browse/certificate-list.component"; +// import { CertificateActionModule } from "./action"; +import { CertificateAdvancedFilterComponent } from "./browse/filter/certificate-advanced-filter.component"; +import { CertificateDetailsModule } from "./details/certificate-details.module"; +import { CertificateHomeComponent } from "./home/certificate-home.component"; const components = [ - // JobScheduleAdvancedFilterComponent, - // JobScheduleHomeComponent, - // JobScheduleListComponent, + CertificateAdvancedFilterComponent, + CertificateHomeComponent, + CertificateListComponent, ]; const modules = [ - // JobScheduleActionModule, JobScheduleDetailsModule, + // CertificateActionModule, + CertificateDetailsModule, ...commonModules, ]; diff --git a/app/components/certificate/details/certificate-default.component.ts b/app/components/certificate/details/certificate-default.component.ts new file mode 100644 index 0000000000..a4b9e7bb4f --- /dev/null +++ b/app/components/certificate/details/certificate-default.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +@Component({ + selector: "bl-certificate-details-home", + template: ` +
    + +

    Please select a certificate from the list

    +
    + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) + +export class CertificateDefaultComponent { + public static breadcrumb() { + return { name: "Certificates" }; + } +} diff --git a/app/components/certificate/details/certificate-details.component.ts b/app/components/certificate/details/certificate-details.component.ts new file mode 100644 index 0000000000..4b4710a515 --- /dev/null +++ b/app/components/certificate/details/certificate-details.component.ts @@ -0,0 +1,107 @@ +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from "@angular/core"; +// import { MatDialog, MatDialogConfig } from "@angular/material"; +import { ActivatedRoute, Router } from "@angular/router"; +import { autobind } from "@batch-flask/core"; +import { ElectronRemote } from "@batch-flask/ui"; +import { Observable, Subscription } from "rxjs"; + +// import { SidebarManager } from "@batch-flask/ui/sidebar"; +import { Certificate, CertificateState } from "app/models"; +import { CertificateDecorator } from "app/models/decorators"; +import { CertificateParams, CertificateService, FileSystemService } from "app/services"; +import { EntityView } from "app/services/core"; +// import { +// DeleteCertificateDialogComponent, +// DisableCertificateDialogComponent, +// EnableCertificateDialogComponent, +// CertificateCreateBasicDialogComponent, +// TerminateCertificateDialogComponent, +// } from "../action"; + +import "./certificate-details.scss"; + +@Component({ + selector: "bl-certificate-details", + templateUrl: "certificate-details.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CertificateDetailsComponent implements OnInit, OnDestroy { + public static breadcrumb({ id }, { tab }) { + const label = tab ? `Certificate - ${tab}` : "Certificate"; + return { + name: id, + label, + icon: "certificate", + }; + } + + public certificateThumbprint: string; + public certificate: Certificate; + public decorator: CertificateDecorator; + public data: EntityView; + public CertificateState = CertificateState; + + private _paramsSubscriber: Subscription; + + constructor( + // private dialog: MatDialog, + private activatedRoute: ActivatedRoute, + private fs: FileSystemService, + private remote: ElectronRemote, + // private sidebarManager: SidebarManager, + private certificateService: CertificateService, + private router: Router) { + + this.data = this.certificateService.view(); + this.data.item.subscribe((certificate) => { + this.certificate = certificate; + if (certificate) { + this.decorator = new CertificateDecorator(certificate); + } + }); + this.data.deleted.subscribe((key) => { + if (this.certificateThumbprint === key) { + this.router.navigate(["/certificates"]); + } + }); + } + + public ngOnInit() { + this._paramsSubscriber = this.activatedRoute.params.subscribe((params) => { + this.certificateThumbprint = params["thumbprint"]; + this.data.params = { thumbprint: this.certificateThumbprint }; + this.data.fetch(); + }); + } + + public ngOnDestroy() { + this._paramsSubscriber.unsubscribe(); + this.data.dispose(); + } + + @autobind() + public refresh() { + return this.data.refresh(); + } + + @autobind() + public deleteCertificate() { + // const config = new MatDialogConfig(); + // const dialogRef = this.dialog.open(DeleteCertificateDialogComponent, config); + // dialogRef.componentInstance.certificateId = this.certificate.id; + } + + @autobind() + public exportAsJSON() { + const dialog = this.remote.dialog; + const localPath = dialog.showSaveDialog({ + buttonLabel: "Export", + defaultPath: `${this.certificateThumbprint}.json`, + }); + + if (localPath) { + const content = JSON.stringify(this.certificate._original, null, 2); + return Observable.fromPromise(this.fs.saveFile(localPath, content)); + } + } +} diff --git a/app/components/certificate/details/certificate-details.html b/app/components/certificate/details/certificate-details.html new file mode 100644 index 0000000000..5830f68faf --- /dev/null +++ b/app/components/certificate/details/certificate-details.html @@ -0,0 +1,18 @@ + +
    + +
    {{decorator.thumbprint}}
    +
    + {{decorator.state}} +
    + + + + + +
    + + Hello, world + +
    +
    diff --git a/app/components/certificate/details/certificate-details.module.ts b/app/components/certificate/details/certificate-details.module.ts new file mode 100644 index 0000000000..09784552a9 --- /dev/null +++ b/app/components/certificate/details/certificate-details.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from "@angular/core"; + +import { BaseModule } from "@batch-flask/ui"; +import { commonModules } from "app/common"; +import { CertificateDefaultComponent } from "./certificate-default.component"; +import { CertificateDetailsComponent } from "./certificate-details.component"; + +const components = [ + CertificateDetailsComponent, + CertificateDefaultComponent, +]; + +const modules = [ + BaseModule, +]; + +@NgModule({ + declarations: components, + exports: components, + imports: [ + ...commonModules, + ...modules, + ], +}) +export class CertificateDetailsModule { +} diff --git a/app/components/certificate/details/certificate-details.scss b/app/components/certificate/details/certificate-details.scss new file mode 100644 index 0000000000..5522ae8bba --- /dev/null +++ b/app/components/certificate/details/certificate-details.scss @@ -0,0 +1,5 @@ +@import "app/styles/variables"; + +bl-certificate-details { + +} diff --git a/app/components/certificate/details/index.ts b/app/components/certificate/details/index.ts new file mode 100644 index 0000000000..4916958608 --- /dev/null +++ b/app/components/certificate/details/index.ts @@ -0,0 +1,3 @@ +export * from "./certificate-default.component"; +export * from "./certificate-details.module"; +export * from "./certificate-details.component"; diff --git a/app/components/certificate/home/certificate-home.component.ts b/app/components/certificate/home/certificate-home.component.ts new file mode 100644 index 0000000000..dfea24e417 --- /dev/null +++ b/app/components/certificate/home/certificate-home.component.ts @@ -0,0 +1,49 @@ +import { ChangeDetectionStrategy, Component } from "@angular/core"; +import { FormBuilder, FormControl } from "@angular/forms"; + +import { Filter, FilterBuilder, autobind } from "@batch-flask/core"; +// import { SidebarManager } from "@batch-flask/ui/sidebar"; +// import { CertificateCreateBasicDialogComponent } from "../action"; + +@Component({ + selector: "bl-certificate-home", + templateUrl: "certificate-home.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CertificateHomeComponent { + public quickSearchQuery = new FormControl(); + + public filter: Filter = FilterBuilder.none(); + public quickFilter: Filter = FilterBuilder.none(); + public advancedFilter: Filter = FilterBuilder.none(); + + constructor( + formBuilder: FormBuilder, + // private sidebarManager: SidebarManager + ) { + this.quickSearchQuery.valueChanges.debounceTime(400).distinctUntilChanged().subscribe((query: string) => { + console.log("query", query); + if (query === "") { + this.quickFilter = FilterBuilder.none(); + } else { + this.quickFilter = FilterBuilder.prop("thumbprint").startswith(query); + } + + this._updateFilter(); + }); + } + + @autobind() + public addCertificate() { + // this.sidebarManager.open("add-certificate", CertificateCreateBasicDialogComponent); + } + + public advancedFilterChanged(filter: Filter) { + this.advancedFilter = filter; + this._updateFilter(); + } + + private _updateFilter() { + this.filter = FilterBuilder.and(this.quickFilter, this.advancedFilter); + } +} diff --git a/app/components/certificate/home/certificate-home.html b/app/components/certificate/home/certificate-home.html new file mode 100644 index 0000000000..0a045fd59e --- /dev/null +++ b/app/components/certificate/home/certificate-home.html @@ -0,0 +1,12 @@ + +
    + Certificate +
    +
    + + +
    + + + +
    From 3cb113a1db0da7be01fdf7dbf8bfadcfc945f394 Mon Sep 17 00:00:00 2001 From: Yunlong Zhang Date: Thu, 15 Mar 2018 12:56:31 -0700 Subject: [PATCH 06/17] Updated certificate filter as a client search Some html/component updates --- .../browse/certificate-list.component.ts | 30 ++++++++++++------- .../certificate/browse/certificate-list.html | 6 ++-- .../details/certificate-details.component.ts | 10 +++++-- .../details/certificate-details.html | 1 + .../home/certificate-home.component.ts | 7 ++++- .../certificate/home/certificate-home.html | 2 +- .../ui/browse-layout/browse-layout.html | 2 +- 7 files changed, 38 insertions(+), 20 deletions(-) diff --git a/app/components/certificate/browse/certificate-list.component.ts b/app/components/certificate/browse/certificate-list.component.ts index 8900565c78..5689a0d564 100644 --- a/app/components/certificate/browse/certificate-list.component.ts +++ b/app/components/certificate/browse/certificate-list.component.ts @@ -7,7 +7,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { List } from "immutable"; import { Observable, Subscription } from "rxjs"; -import { Filter, autobind } from "@batch-flask/core"; +import { Filter, FilterMatcher, Operator, autobind } from "@batch-flask/core"; import { ListBaseComponent, ListSelection } from "@batch-flask/core/list"; // import { BackgroundTaskService } from "@batch-flask/ui/background-task"; import { ContextMenu, ContextMenuItem } from "@batch-flask/ui/context-menu"; @@ -36,12 +36,11 @@ import { ComponentUtils } from "app/utils"; }) export class CertificateListComponent extends ListBaseComponent implements OnInit, OnDestroy { public certificates: List; + public displayedCertificates: List = List([]); public LoadingStatus = LoadingStatus; - public data: ListView; public searchQuery = new FormControl(); - private _baseOptions = {}; private _onCertificateAddedSub: Subscription; constructor( @@ -58,7 +57,7 @@ export class CertificateListComponent extends ListBaseComponent implements OnIni ComponentUtils.setActiveItem(activatedRoute, this.data); this.data.items.subscribe((certificates) => { this.certificates = certificates; - this.changeDetector.markForCheck(); + this._updateDisplayedCertificates(); }); this.data.status.subscribe((status) => { @@ -86,13 +85,7 @@ export class CertificateListComponent extends ListBaseComponent implements OnIni } public handleFilter(filter: Filter) { - if (filter.isEmpty()) { - this.data.setOptions({ ...this._baseOptions }); - } else { - this.data.setOptions({ ...this._baseOptions, filter: filter.toOData() }); - } - - this.data.fetchNext(); + this._updateDisplayedCertificates(); } public certificateStatus(certificate: Certificate): QuickListItemStatus { @@ -164,4 +157,19 @@ export class CertificateListComponent extends ListBaseComponent implements OnIni } }); } + + private _updateDisplayedCertificates() { + const matcher = new FilterMatcher({ + thumbprint: (item: Certificate, value: any, operator: Operator) => { + return value === "" || item.thumbprint.toLowerCase().startsWith(value.toLowerCase()); + }, + state: (item: Certificate, value: any, operator: Operator) => { + return value === "" || item.state === value; + }, + }); + this.displayedCertificates = List(this.certificates.filter((x) => { + return matcher.test(this.filter, x); + })); + this.changeDetector.markForCheck(); + } } diff --git a/app/components/certificate/browse/certificate-list.html b/app/components/certificate/browse/certificate-list.html index 7e216f8ee2..ae96efa6b7 100644 --- a/app/components/certificate/browse/certificate-list.html +++ b/app/components/certificate/browse/certificate-list.html @@ -1,6 +1,6 @@ - +
    {{certificate.thumbprint}}
    @@ -17,7 +17,7 @@ State - + {{certificate.thumbprintAlgorithm}} {{certificate.thumbprint}} {{certificate.state}} @@ -27,7 +27,7 @@ - + No certificates No certificates match this filter diff --git a/app/components/certificate/details/certificate-details.component.ts b/app/components/certificate/details/certificate-details.component.ts index 4b4710a515..8c2e9d8ba9 100644 --- a/app/components/certificate/details/certificate-details.component.ts +++ b/app/components/certificate/details/certificate-details.component.ts @@ -26,10 +26,10 @@ import "./certificate-details.scss"; changeDetection: ChangeDetectionStrategy.OnPush, }) export class CertificateDetailsComponent implements OnInit, OnDestroy { - public static breadcrumb({ id }, { tab }) { - const label = tab ? `Certificate - ${tab}` : "Certificate"; + public static breadcrumb({ thumbprint }) { + const label = `Certificate - ${thumbprint}`; return { - name: id, + name: thumbprint, label, icon: "certificate", }; @@ -91,6 +91,10 @@ export class CertificateDetailsComponent implements OnInit, OnDestroy { // dialogRef.componentInstance.certificateId = this.certificate.id; } + @autobind() + public reactivateCertificate() { + } + @autobind() public exportAsJSON() { const dialog = this.remote.dialog; diff --git a/app/components/certificate/details/certificate-details.html b/app/components/certificate/details/certificate-details.html index 5830f68faf..48895cc8ad 100644 --- a/app/components/certificate/details/certificate-details.html +++ b/app/components/certificate/details/certificate-details.html @@ -8,6 +8,7 @@ + diff --git a/app/components/certificate/home/certificate-home.component.ts b/app/components/certificate/home/certificate-home.component.ts index dfea24e417..594304824e 100644 --- a/app/components/certificate/home/certificate-home.component.ts +++ b/app/components/certificate/home/certificate-home.component.ts @@ -17,12 +17,17 @@ export class CertificateHomeComponent { public quickFilter: Filter = FilterBuilder.none(); public advancedFilter: Filter = FilterBuilder.none(); + public keyField = "thumbprint"; + public config = { + quickSearchField: "thumbprint", + keyField: "thumbprint", + }; + constructor( formBuilder: FormBuilder, // private sidebarManager: SidebarManager ) { this.quickSearchQuery.valueChanges.debounceTime(400).distinctUntilChanged().subscribe((query: string) => { - console.log("query", query); if (query === "") { this.quickFilter = FilterBuilder.none(); } else { diff --git a/app/components/certificate/home/certificate-home.html b/app/components/certificate/home/certificate-home.html index 0a045fd59e..7717604e5b 100644 --- a/app/components/certificate/home/certificate-home.html +++ b/app/components/certificate/home/certificate-home.html @@ -1,4 +1,4 @@ - +
    Certificate
    diff --git a/src/@batch-flask/ui/browse-layout/browse-layout.html b/src/@batch-flask/ui/browse-layout/browse-layout.html index de31fd40bc..2c2e1d4c48 100644 --- a/src/@batch-flask/ui/browse-layout/browse-layout.html +++ b/src/@batch-flask/ui/browse-layout/browse-layout.html @@ -38,7 +38,7 @@
    -
    +
    From 2a9270e338a7c3bcef960a85b3db02230ecf9b46 Mon Sep 17 00:00:00 2001 From: Yunlong Zhang Date: Thu, 15 Mar 2018 16:29:34 -0700 Subject: [PATCH 07/17] added certificate configuration --- .../certificate-configuration.component.ts | 31 +++++++++++++++++++ .../details/certificate-configuration.html | 29 +++++++++++++++++ .../details/certificate-details.component.ts | 1 + .../details/certificate-details.html | 7 ++++- .../details/certificate-details.module.ts | 2 ++ app/components/certificate/details/index.ts | 1 + .../pinned-dropdown.component.ts | 4 +++ app/components/shared/main-navigation.html | 2 +- .../decorators/certificate-decorator.ts | 2 ++ .../delete-certificate-error-decorator.ts | 2 ++ .../property-content/property-content.scss | 4 +++ 11 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 app/components/certificate/details/certificate-configuration.component.ts create mode 100644 app/components/certificate/details/certificate-configuration.html diff --git a/app/components/certificate/details/certificate-configuration.component.ts b/app/components/certificate/details/certificate-configuration.component.ts new file mode 100644 index 0000000000..53c0cdf116 --- /dev/null +++ b/app/components/certificate/details/certificate-configuration.component.ts @@ -0,0 +1,31 @@ +import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; + +import { Certificate } from "app/models"; +import { CertificateDecorator } from "app/models/decorators"; + +// tslint:disable:trackBy-function +@Component({ + selector: "bl-certificate-configuration", + templateUrl: "certificate-configuration.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CertificateConfigurationComponent { + @Input() + public set certificate(certificate: Certificate) { + console.log("xxx", certificate); + this._certificate = certificate; + this.refresh(certificate); + } + public get certificate() { return this._certificate; } + + public decorator: CertificateDecorator = { } as any; + + private _certificate: Certificate; + + public refresh(certificate: Certificate) { + if (this.certificate) { + this.decorator = new CertificateDecorator(this.certificate); + console.log(this.decorator); + } + } +} diff --git a/app/components/certificate/details/certificate-configuration.html b/app/components/certificate/details/certificate-configuration.html new file mode 100644 index 0000000000..21b1d64acf --- /dev/null +++ b/app/components/certificate/details/certificate-configuration.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + +
    Name
    +
    Value
    +
    + + + + +
    +
    +
    +
    diff --git a/app/components/certificate/details/certificate-details.component.ts b/app/components/certificate/details/certificate-details.component.ts index 8c2e9d8ba9..c28a6a372c 100644 --- a/app/components/certificate/details/certificate-details.component.ts +++ b/app/components/certificate/details/certificate-details.component.ts @@ -93,6 +93,7 @@ export class CertificateDetailsComponent implements OnInit, OnDestroy { @autobind() public reactivateCertificate() { + // TODO } @autobind() diff --git a/app/components/certificate/details/certificate-details.html b/app/components/certificate/details/certificate-details.html index 48895cc8ad..847c436340 100644 --- a/app/components/certificate/details/certificate-details.html +++ b/app/components/certificate/details/certificate-details.html @@ -13,7 +13,12 @@ - Hello, world + + + Configuration + + +
    diff --git a/app/components/certificate/details/certificate-details.module.ts b/app/components/certificate/details/certificate-details.module.ts index 09784552a9..7f266ac7e9 100644 --- a/app/components/certificate/details/certificate-details.module.ts +++ b/app/components/certificate/details/certificate-details.module.ts @@ -2,12 +2,14 @@ import { NgModule } from "@angular/core"; import { BaseModule } from "@batch-flask/ui"; import { commonModules } from "app/common"; +import { CertificateConfigurationComponent } from "./certificate-configuration.component"; import { CertificateDefaultComponent } from "./certificate-default.component"; import { CertificateDetailsComponent } from "./certificate-details.component"; const components = [ CertificateDetailsComponent, CertificateDefaultComponent, + CertificateConfigurationComponent, ]; const modules = [ diff --git a/app/components/certificate/details/index.ts b/app/components/certificate/details/index.ts index 4916958608..c087cd78c8 100644 --- a/app/components/certificate/details/index.ts +++ b/app/components/certificate/details/index.ts @@ -1,3 +1,4 @@ export * from "./certificate-default.component"; export * from "./certificate-details.module"; export * from "./certificate-details.component"; +export * from "./certificate-configuration.component"; diff --git a/app/components/layout/pinned-entity-dropdown/pinned-dropdown.component.ts b/app/components/layout/pinned-entity-dropdown/pinned-dropdown.component.ts index c678f2ebd9..eb2ab455a7 100644 --- a/app/components/layout/pinned-entity-dropdown/pinned-dropdown.component.ts +++ b/app/components/layout/pinned-entity-dropdown/pinned-dropdown.component.ts @@ -58,6 +58,8 @@ export class PinnedDropDownComponent implements OnInit, OnDestroy { return "Batch job schedule"; case PinnedEntityType.Pool: return "Batch pool"; + case PinnedEntityType.Certificate: + return "Batch certificate"; case PinnedEntityType.FileGroup: return "File group"; default: @@ -75,6 +77,8 @@ export class PinnedDropDownComponent implements OnInit, OnDestroy { return "fa-calendar"; case PinnedEntityType.Pool: return "fa-database"; + case PinnedEntityType.Certificate: + return "fa-certificate"; case PinnedEntityType.FileGroup: return "fa-cloud-upload"; default: diff --git a/app/components/shared/main-navigation.html b/app/components/shared/main-navigation.html index 54f1cbc32f..ca4c8a0753 100644 --- a/app/components/shared/main-navigation.html +++ b/app/components/shared/main-navigation.html @@ -26,7 +26,7 @@
  • -
    Cert
    +
    Certificate
  • diff --git a/app/models/decorators/certificate-decorator.ts b/app/models/decorators/certificate-decorator.ts index 496c72bf6b..2a004dee6d 100644 --- a/app/models/decorators/certificate-decorator.ts +++ b/app/models/decorators/certificate-decorator.ts @@ -11,6 +11,7 @@ export class CertificateDecorator extends DecoratorBase { public thumbprintAlgorithm: string; public thumbprint: string; public publicData: string; + public url: string; public deleteCertificateError: DeleteCertificateErrorDecorator; @@ -22,6 +23,7 @@ export class CertificateDecorator extends DecoratorBase { this.stateIcon = this._getStateIcon(certificate.state); this.previousState = this.stateField(certificate.previousState); this.previousStateTransitionTime = this.dateField(certificate.previousStateTransitionTime); + this.url = this.stringField(certificate.url); this.thumbprintAlgorithm = this.stringField(certificate.thumbprintAlgorithm); this.thumbprint = this.stringField(certificate.thumbprint); this.publicData = this.stringField(certificate.publicData); diff --git a/app/models/decorators/delete-certificate-error-decorator.ts b/app/models/decorators/delete-certificate-error-decorator.ts index 650ef7a58c..1684c110e5 100644 --- a/app/models/decorators/delete-certificate-error-decorator.ts +++ b/app/models/decorators/delete-certificate-error-decorator.ts @@ -4,6 +4,7 @@ import { DecoratorBase } from "app/utils/decorators"; export class DeleteCertificateErrorDecorator extends DecoratorBase { public code: string; public message: string; + public values: NameValuePair[]; public details: string; /** @@ -22,6 +23,7 @@ export class DeleteCertificateErrorDecorator extends DecoratorBase Date: Thu, 15 Mar 2018 16:41:42 -0700 Subject: [PATCH 08/17] Remove console.log --- .../details/certificate-configuration.component.ts | 2 -- app/models/certificate.ts | 4 ++++ app/services/pinned-entity.service.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/components/certificate/details/certificate-configuration.component.ts b/app/components/certificate/details/certificate-configuration.component.ts index 53c0cdf116..46afabdfe3 100644 --- a/app/components/certificate/details/certificate-configuration.component.ts +++ b/app/components/certificate/details/certificate-configuration.component.ts @@ -12,7 +12,6 @@ import { CertificateDecorator } from "app/models/decorators"; export class CertificateConfigurationComponent { @Input() public set certificate(certificate: Certificate) { - console.log("xxx", certificate); this._certificate = certificate; this.refresh(certificate); } @@ -25,7 +24,6 @@ export class CertificateConfigurationComponent { public refresh(certificate: Certificate) { if (this.certificate) { this.decorator = new CertificateDecorator(this.certificate); - console.log(this.decorator); } } } diff --git a/app/models/certificate.ts b/app/models/certificate.ts index 6d9510970d..647262660d 100644 --- a/app/models/certificate.ts +++ b/app/models/certificate.ts @@ -50,6 +50,10 @@ export class Certificate extends Record implements Naviga return this.thumbprint; } + public get name(): string { + return this.thumbprint; + } + public get routerLink(): string[] { return ["/certificates", this.thumbprint]; } diff --git a/app/services/pinned-entity.service.ts b/app/services/pinned-entity.service.ts index 89e9be80b3..8084300f60 100644 --- a/app/services/pinned-entity.service.ts +++ b/app/services/pinned-entity.service.ts @@ -4,7 +4,7 @@ import { AsyncSubject, BehaviorSubject, Observable } from "rxjs"; import { NavigableRecord, PinnableEntity, PinnedEntityType } from "@batch-flask/core"; import { - BatchApplication, BlobContainer, Job, JobSchedule, Pool, + BatchApplication, BlobContainer, Certificate, Job, JobSchedule, Pool, } from "app/models"; import { AccountService } from "./account.service"; import { LocalFileStorage } from "./local-file-storage.service"; @@ -14,6 +14,7 @@ pinnedTypeMap.set(PinnedEntityType.Application, BatchApplication); pinnedTypeMap.set(PinnedEntityType.Pool, Pool); pinnedTypeMap.set(PinnedEntityType.Job, Job); pinnedTypeMap.set(PinnedEntityType.JobSchedule, JobSchedule); +pinnedTypeMap.set(PinnedEntityType.Certificate, Certificate); pinnedTypeMap.set(PinnedEntityType.FileGroup, BlobContainer); @Injectable() From cc1c1a444999d8e10c4943c8a43c893bc6a305e9 Mon Sep 17 00:00:00 2001 From: Yunlong Zhang Date: Thu, 15 Mar 2018 17:10:44 -0700 Subject: [PATCH 09/17] added delete certificate and reactivate certifcate actions --- .../action/certificate-action.module.ts | 20 ++++++++++ .../delete/delete-certificate-action.ts | 40 +++++++++++++++++++ .../delete-certificate-dialog.component.ts | 36 +++++++++++++++++ .../delete/delete-certificate-dialog.html | 9 +++++ .../certificate/action/delete/index.ts | 2 + app/components/certificate/action/index.ts | 4 ++ .../certificate/action/reactivate/index.ts | 1 + ...reactivate-certificate-dialog.component.ts | 26 ++++++++++++ .../reactivate-certificate-dialog.html | 3 ++ .../certificate/certificate.module.ts | 6 +-- 10 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 app/components/certificate/action/certificate-action.module.ts create mode 100644 app/components/certificate/action/delete/delete-certificate-action.ts create mode 100644 app/components/certificate/action/delete/delete-certificate-dialog.component.ts create mode 100644 app/components/certificate/action/delete/delete-certificate-dialog.html create mode 100644 app/components/certificate/action/delete/index.ts create mode 100644 app/components/certificate/action/index.ts create mode 100644 app/components/certificate/action/reactivate/index.ts create mode 100644 app/components/certificate/action/reactivate/reactivate-certificate-dialog.component.ts create mode 100644 app/components/certificate/action/reactivate/reactivate-certificate-dialog.html diff --git a/app/components/certificate/action/certificate-action.module.ts b/app/components/certificate/action/certificate-action.module.ts new file mode 100644 index 0000000000..1c3f923f35 --- /dev/null +++ b/app/components/certificate/action/certificate-action.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from "@angular/core"; + +import { BaseModule } from "@batch-flask/ui"; +import { commonModules } from "app/common"; +import { JobActionModule } from "app/components/job/action"; +import { DeleteCertificateDialogComponent } from "./delete/delete-certificate-dialog.component"; +import { ReactivateCertificateDialogComponent } from "./reactivate/reactivate-certificate-dialog.component"; + +const components = [ + DeleteCertificateDialogComponent, ReactivateCertificateDialogComponent, +]; + +@NgModule({ + declarations: components, + exports: components, + imports: [...commonModules, JobActionModule, BaseModule], + entryComponents: components, +}) +export class CertificateActionModule { +} diff --git a/app/components/certificate/action/delete/delete-certificate-action.ts b/app/components/certificate/action/delete/delete-certificate-action.ts new file mode 100644 index 0000000000..6451eb85f9 --- /dev/null +++ b/app/components/certificate/action/delete/delete-certificate-action.ts @@ -0,0 +1,40 @@ +import { BehaviorSubject } from "rxjs"; + +import { BackgroundTaskService } from "@batch-flask/ui/background-task"; +import { WaitForDeletePoller } from "app/components/core/pollers"; +import { Certificate } from "app/models"; +import { CertificateService } from "app/services"; +import { LongRunningDeleteAction } from "app/services/core"; + +export class DeleteCertificateAction extends LongRunningDeleteAction { + constructor(private certificateService: CertificateService, certificateThumbprints: string[]) { + super("certificate", certificateThumbprints); + } + + public deleteAction(thumbprint: string) { + return this.certificateService.delete(thumbprint); + } + + protected waitForDelete(thumbprint: string, taskManager?: BackgroundTaskService) { + this.certificateService.get(thumbprint).subscribe({ + next: (certificate: Certificate) => { + const task = new WaitForDeletePoller(() => this.certificateService.get(thumbprint)); + if (taskManager) { + taskManager.startTask(`Deleting Certificate '${thumbprint}'`, (bTask) => { + return task.start(bTask.progress); + }); + } else { + task.start(new BehaviorSubject(-1)).subscribe({ + complete: () => { + this.markItemAsDeleted(); + }, + }); + } + }, + error: (error) => { + // No need to watch for Certificate it is already deleted + this.markItemAsDeleted(); + }, + }); + } +} diff --git a/app/components/certificate/action/delete/delete-certificate-dialog.component.ts b/app/components/certificate/action/delete/delete-certificate-dialog.component.ts new file mode 100644 index 0000000000..40f529a062 --- /dev/null +++ b/app/components/certificate/action/delete/delete-certificate-dialog.component.ts @@ -0,0 +1,36 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from "@angular/core"; +import { MatDialogRef } from "@angular/material"; + +import { autobind } from "@batch-flask/core"; +import { BackgroundTaskService } from "@batch-flask/ui/background-task"; +import { DeleteCertificateAction } from "app/components/certificate/action/delete/delete-certificate-action"; +import { CertificateService } from "app/services"; + +@Component({ + selector: "bl-delete-certificate-dialog", + templateUrl: "delete-certificate-dialog.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DeleteCertificateDialogComponent { + public set certificateThumbprint(certificateThumbprint: string) { + this._certificateThumbprint = certificateThumbprint; + this.changeDetector.detectChanges(); + } + public get certificateThumbprint() { return this._certificateThumbprint; } + + private _certificateThumbprint: string; + + constructor( + public dialogRef: MatDialogRef, + private certificateService: CertificateService, + private taskManager: BackgroundTaskService, + private changeDetector: ChangeDetectorRef) { + } + + @autobind() + public destroyCertificate() { + const task = new DeleteCertificateAction(this.certificateService, [this.certificateThumbprint]); + task.startAndWaitAsync(this.taskManager); + return task.actionDone; + } +} diff --git a/app/components/certificate/action/delete/delete-certificate-dialog.html b/app/components/certificate/action/delete/delete-certificate-dialog.html new file mode 100644 index 0000000000..a584757469 --- /dev/null +++ b/app/components/certificate/action/delete/delete-certificate-dialog.html @@ -0,0 +1,9 @@ + +
    +

    Do you want to delete the certificate: '{{certificateThumbprint}}'?

    +

    + Warning! Deleting certificate'{{certificateThumbprint}}' is irreversible. The action you're about to take can't be undone. Clicking + OK will delete this certificate permanently. +

    +
    +
    diff --git a/app/components/certificate/action/delete/index.ts b/app/components/certificate/action/delete/index.ts new file mode 100644 index 0000000000..e9b77fd569 --- /dev/null +++ b/app/components/certificate/action/delete/index.ts @@ -0,0 +1,2 @@ +export * from "./delete-certificate-action"; +export * from "./delete-certificate-dialog.component"; diff --git a/app/components/certificate/action/index.ts b/app/components/certificate/action/index.ts new file mode 100644 index 0000000000..0182406c6d --- /dev/null +++ b/app/components/certificate/action/index.ts @@ -0,0 +1,4 @@ + +export * from "./delete"; +export * from "./reactivate"; +export * from "./certificate-action.module"; diff --git a/app/components/certificate/action/reactivate/index.ts b/app/components/certificate/action/reactivate/index.ts new file mode 100644 index 0000000000..11ad64ff7e --- /dev/null +++ b/app/components/certificate/action/reactivate/index.ts @@ -0,0 +1 @@ +export * from "./reactivate-certificate-dialog.component"; diff --git a/app/components/certificate/action/reactivate/reactivate-certificate-dialog.component.ts b/app/components/certificate/action/reactivate/reactivate-certificate-dialog.component.ts new file mode 100644 index 0000000000..3b1d25d504 --- /dev/null +++ b/app/components/certificate/action/reactivate/reactivate-certificate-dialog.component.ts @@ -0,0 +1,26 @@ +import { ChangeDetectionStrategy, Component } from "@angular/core"; +import { MatDialogRef } from "@angular/material"; + +import { autobind } from "@batch-flask/core"; +import { CertificateService } from "app/services"; + +@Component({ + selector: "bl-reactivate-certificate-dialog", + templateUrl: "reactivate-certificate-dialog.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReactivateCertificateDialogComponent { + public certificateThumbprint: string; + + constructor( + public dialogRef: MatDialogRef, + private certificateService: CertificateService) { + } + + @autobind() + public ok() { + const options: any = {}; + + return this.certificateService.cancelDelete(this.certificateThumbprint, options); + } +} diff --git a/app/components/certificate/action/reactivate/reactivate-certificate-dialog.html b/app/components/certificate/action/reactivate/reactivate-certificate-dialog.html new file mode 100644 index 0000000000..ac8d8cf9c1 --- /dev/null +++ b/app/components/certificate/action/reactivate/reactivate-certificate-dialog.html @@ -0,0 +1,3 @@ + +

    Do you want to reactivate the certificate'{{certificateThumbprint}}'?

    +
    diff --git a/app/components/certificate/certificate.module.ts b/app/components/certificate/certificate.module.ts index 459d5a9173..be3c1fb216 100644 --- a/app/components/certificate/certificate.module.ts +++ b/app/components/certificate/certificate.module.ts @@ -1,8 +1,8 @@ import { NgModule } from "@angular/core"; import { commonModules } from "app/common"; +import { CertificateActionModule } from "./action"; import { CertificateListComponent } from "./browse/certificate-list.component"; -// import { CertificateActionModule } from "./action"; import { CertificateAdvancedFilterComponent } from "./browse/filter/certificate-advanced-filter.component"; import { CertificateDetailsModule } from "./details/certificate-details.module"; import { CertificateHomeComponent } from "./home/certificate-home.component"; @@ -14,9 +14,7 @@ const components = [ ]; const modules = [ - // CertificateActionModule, - CertificateDetailsModule, - ...commonModules, + CertificateActionModule, CertificateDetailsModule, ...commonModules, ]; @NgModule({ From f10512551b518ada9456d545ef902ff1b2f0d2e9 Mon Sep 17 00:00:00 2001 From: Yunlong Zhang Date: Thu, 15 Mar 2018 17:31:14 -0700 Subject: [PATCH 10/17] Link delete/reactivate actions to cert list and detail page button and context menu --- .../browse/certificate-list.component.ts | 50 ++++++++++--------- .../details/certificate-details.component.ts | 26 ++++------ .../details/certificate-details.html | 4 +- .../details/certificate-details.scss | 1 - 4 files changed, 41 insertions(+), 40 deletions(-) diff --git a/app/components/certificate/browse/certificate-list.component.ts b/app/components/certificate/browse/certificate-list.component.ts index 5689a0d564..5ce63804de 100644 --- a/app/components/certificate/browse/certificate-list.component.ts +++ b/app/components/certificate/browse/certificate-list.component.ts @@ -2,14 +2,14 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, forwardRef, } from "@angular/core"; import { FormControl } from "@angular/forms"; -// import { MatDialog } from "@angular/material"; +import { MatDialog } from "@angular/material"; import { ActivatedRoute, Router } from "@angular/router"; import { List } from "immutable"; import { Observable, Subscription } from "rxjs"; import { Filter, FilterMatcher, Operator, autobind } from "@batch-flask/core"; import { ListBaseComponent, ListSelection } from "@batch-flask/core/list"; -// import { BackgroundTaskService } from "@batch-flask/ui/background-task"; +import { BackgroundTaskService } from "@batch-flask/ui/background-task"; import { ContextMenu, ContextMenuItem } from "@batch-flask/ui/context-menu"; import { LoadingStatus } from "@batch-flask/ui/loading"; import { QuickListItemStatus } from "@batch-flask/ui/quick-list"; @@ -17,13 +17,9 @@ import { Certificate, CertificateState } from "app/models"; import { CertificateListParams, CertificateService, PinnedEntityService } from "app/services"; import { ListView } from "app/services/core"; import { ComponentUtils } from "app/utils"; -// import { -// DeleteCertificateAction, -// DeleteCertificateDialogComponent, -// DisableCertificateDialogComponent, -// EnableCertificateDialogComponent, -// TerminateCertificateDialogComponent, -// } from "../action"; +import { + DeleteCertificateAction, DeleteCertificateDialogComponent, ReactivateCertificateDialogComponent, +} from "../action"; @Component({ selector: "bl-certificate-list", @@ -47,10 +43,10 @@ export class CertificateListComponent extends ListBaseComponent implements OnIni router: Router, activatedRoute: ActivatedRoute, changeDetector: ChangeDetectorRef, - // private dialog: MatDialog, + private dialog: MatDialog, private certificateService: CertificateService, private pinnedEntityService: PinnedEntityService, - // private taskManager: BackgroundTaskService + private taskManager: BackgroundTaskService, ) { super(changeDetector); this.data = this.certificateService.listView(); @@ -117,10 +113,10 @@ export class CertificateListComponent extends ListBaseComponent implements OnIni public contextmenu(certificate: Certificate) { const deletefailed = certificate.state === CertificateState.deletefailed; return new ContextMenu([ - new ContextMenuItem({ label: "Delete", click: () => {} /*this.deleteCertificate(certificate)*/ }), + new ContextMenuItem({ label: "Delete", click: () => this.deleteCertificate(certificate) }), new ContextMenuItem({ label: "Reactivate", - click: () => {}, // this.terminateCertificate(certificate), + click: () => this.reactivateCertificate(certificate), enabled: deletefailed, }), new ContextMenuItem({ @@ -131,19 +127,27 @@ export class CertificateListComponent extends ListBaseComponent implements OnIni } public deleteSelection(selection: ListSelection) { - // this.taskManager.startTask("", (backgroundTask) => { - // const task = new DeleteCertificateAction(this.certificateService, [...selection.keys]); - // task.start(backgroundTask); - // return task.waitingDone; - // }); + this.taskManager.startTask("", (backgroundTask) => { + const task = new DeleteCertificateAction(this.certificateService, [...selection.keys]); + task.start(backgroundTask); + return task.waitingDone; + }); } public deleteCertificate(certificate: Certificate) { - // const dialogRef = this.dialog.open(DeleteCertificateDialogComponent); - // dialogRef.componentInstance.certificateId = certificate.id; - // dialogRef.afterClosed().subscribe((obj) => { - // this.certificateService.get(certificate.id); - // }); + const dialogRef = this.dialog.open(DeleteCertificateDialogComponent); + dialogRef.componentInstance.certificateThumbprint = certificate.thumbprint; + dialogRef.afterClosed().subscribe((obj) => { + this.certificateService.get(certificate.thumbprint); + }); + } + + public reactivateCertificate(certificate: Certificate) { + const dialogRef = this.dialog.open(ReactivateCertificateDialogComponent); + dialogRef.componentInstance.certificateThumbprint = certificate.thumbprint; + dialogRef.afterClosed().subscribe((obj) => { + this.certificateService.get(certificate.thumbprint); + }); } public trackByFn(index: number, certificate: Certificate) { diff --git a/app/components/certificate/details/certificate-details.component.ts b/app/components/certificate/details/certificate-details.component.ts index c28a6a372c..cff6881a95 100644 --- a/app/components/certificate/details/certificate-details.component.ts +++ b/app/components/certificate/details/certificate-details.component.ts @@ -1,22 +1,17 @@ import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from "@angular/core"; -// import { MatDialog, MatDialogConfig } from "@angular/material"; +import { MatDialog, MatDialogConfig } from "@angular/material"; import { ActivatedRoute, Router } from "@angular/router"; import { autobind } from "@batch-flask/core"; import { ElectronRemote } from "@batch-flask/ui"; import { Observable, Subscription } from "rxjs"; -// import { SidebarManager } from "@batch-flask/ui/sidebar"; import { Certificate, CertificateState } from "app/models"; import { CertificateDecorator } from "app/models/decorators"; import { CertificateParams, CertificateService, FileSystemService } from "app/services"; import { EntityView } from "app/services/core"; -// import { -// DeleteCertificateDialogComponent, -// DisableCertificateDialogComponent, -// EnableCertificateDialogComponent, -// CertificateCreateBasicDialogComponent, -// TerminateCertificateDialogComponent, -// } from "../action"; +import { + DeleteCertificateDialogComponent, ReactivateCertificateDialogComponent, +} from "../action"; import "./certificate-details.scss"; @@ -44,11 +39,10 @@ export class CertificateDetailsComponent implements OnInit, OnDestroy { private _paramsSubscriber: Subscription; constructor( - // private dialog: MatDialog, + private dialog: MatDialog, private activatedRoute: ActivatedRoute, private fs: FileSystemService, private remote: ElectronRemote, - // private sidebarManager: SidebarManager, private certificateService: CertificateService, private router: Router) { @@ -86,14 +80,16 @@ export class CertificateDetailsComponent implements OnInit, OnDestroy { @autobind() public deleteCertificate() { - // const config = new MatDialogConfig(); - // const dialogRef = this.dialog.open(DeleteCertificateDialogComponent, config); - // dialogRef.componentInstance.certificateId = this.certificate.id; + const config = new MatDialogConfig(); + const dialogRef = this.dialog.open(DeleteCertificateDialogComponent, config); + dialogRef.componentInstance.certificateThumbprint = this.certificate.thumbprint; } @autobind() public reactivateCertificate() { - // TODO + const config = new MatDialogConfig(); + const dialogRef = this.dialog.open(ReactivateCertificateDialogComponent, config); + dialogRef.componentInstance.certificateThumbprint = this.certificate.thumbprint; } @autobind() diff --git a/app/components/certificate/details/certificate-details.html b/app/components/certificate/details/certificate-details.html index 847c436340..657de985ae 100644 --- a/app/components/certificate/details/certificate-details.html +++ b/app/components/certificate/details/certificate-details.html @@ -8,7 +8,9 @@ - + + + diff --git a/app/components/certificate/details/certificate-details.scss b/app/components/certificate/details/certificate-details.scss index 5522ae8bba..cd067ef75b 100644 --- a/app/components/certificate/details/certificate-details.scss +++ b/app/components/certificate/details/certificate-details.scss @@ -1,5 +1,4 @@ @import "app/styles/variables"; bl-certificate-details { - } From 678a5d3d112d198351d02f9f4988cb588d5ca8e1 Mon Sep 17 00:00:00 2001 From: Yunlong Zhang Date: Fri, 16 Mar 2018 10:13:12 -0700 Subject: [PATCH 11/17] Basic certificate create form --- ...rtificate-create-basic-dialog.component.ts | 74 +++++++++++++++++++ .../add/certificate-create-basic-dialog.html | 12 +++ .../add/certificate-create-basic-dialog.scss | 0 .../certificate/action/add/index.ts | 1 + .../action/certificate-action.module.ts | 3 +- .../home/certificate-home.component.ts | 11 +-- app/models/forms/create-certificate-model.ts | 32 ++++++++ app/models/forms/index.ts | 1 + 8 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 app/components/certificate/action/add/certificate-create-basic-dialog.component.ts create mode 100644 app/components/certificate/action/add/certificate-create-basic-dialog.html create mode 100644 app/components/certificate/action/add/certificate-create-basic-dialog.scss create mode 100644 app/components/certificate/action/add/index.ts create mode 100644 app/models/forms/create-certificate-model.ts diff --git a/app/components/certificate/action/add/certificate-create-basic-dialog.component.ts b/app/components/certificate/action/add/certificate-create-basic-dialog.component.ts new file mode 100644 index 0000000000..b15496c29d --- /dev/null +++ b/app/components/certificate/action/add/certificate-create-basic-dialog.component.ts @@ -0,0 +1,74 @@ +import { Component } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; +import { Observable } from "rxjs"; + +import { DynamicForm, autobind } from "@batch-flask/core"; +import { ComplexFormConfig } from "@batch-flask/ui/form"; +import { NotificationService } from "@batch-flask/ui/notifications"; +import { SidebarRef } from "@batch-flask/ui/sidebar"; +import { Certificate } from "app/models"; +import { CertificateCreateDto } from "app/models/dtos"; +import { certificateToFormModel, createCertificateFormToJsonData } from "app/models/forms"; +import { CertificateService } from "app/services"; + +import "./certificate-create-basic-dialog.scss"; + +@Component({ + selector: "bl-certificate-create-basic-dialog", + templateUrl: "certificate-create-basic-dialog.html", +}) +export class CertificateCreateBasicDialogComponent extends DynamicForm { + public complexFormConfig: ComplexFormConfig; + public fileUri = "create.certificate.batch.json"; + + constructor( + private formBuilder: FormBuilder, + public sidebarRef: SidebarRef, + private certificateService: CertificateService, + private notificationService: NotificationService) { + super(CertificateCreateDto); + this._setComplexFormConfig(); + + this.form = this.formBuilder.group({ + certificateFormat: [null], + data: [null], + password: [null], + thumbprint: [null], + thumbprintAlgorithm: [null], + }); + } + + public dtoToForm(certificate: CertificateCreateDto) { + return certificateToFormModel(certificate); + } + + public formToDto(data: any): CertificateCreateDto { + return createCertificateFormToJsonData(data); + } + + @autobind() + public submit(data: CertificateCreateDto): Observable { + const thumbprint = data.thumbprint; + const obs = this.certificateService.add(data); + obs.subscribe({ + next: () => { + this.certificateService.onCertificateAdded.next(thumbprint); + this.notificationService.success("Certificate added!", + `Certificate '${thumbprint}' was created successfully!`); + }, + error: () => null, + }); + + return obs; + } + + private _setComplexFormConfig() { + this.complexFormConfig = { + jsonEditor: { + dtoType: CertificateCreateDto, + toDto: (value) => this.formToDto(value), + fromDto: (value) => this.dtoToForm(value), + }, + }; + } +} diff --git a/app/components/certificate/action/add/certificate-create-basic-dialog.html b/app/components/certificate/action/add/certificate-create-basic-dialog.html new file mode 100644 index 0000000000..97e124e25a --- /dev/null +++ b/app/components/certificate/action/add/certificate-create-basic-dialog.html @@ -0,0 +1,12 @@ + + + +
    + + + + +
    +
    +
    +
    diff --git a/app/components/certificate/action/add/certificate-create-basic-dialog.scss b/app/components/certificate/action/add/certificate-create-basic-dialog.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/components/certificate/action/add/index.ts b/app/components/certificate/action/add/index.ts new file mode 100644 index 0000000000..fea3fbeb7b --- /dev/null +++ b/app/components/certificate/action/add/index.ts @@ -0,0 +1 @@ +export * from "./certificate-create-basic-dialog.component"; diff --git a/app/components/certificate/action/certificate-action.module.ts b/app/components/certificate/action/certificate-action.module.ts index 1c3f923f35..44c6eccddb 100644 --- a/app/components/certificate/action/certificate-action.module.ts +++ b/app/components/certificate/action/certificate-action.module.ts @@ -3,11 +3,12 @@ import { NgModule } from "@angular/core"; import { BaseModule } from "@batch-flask/ui"; import { commonModules } from "app/common"; import { JobActionModule } from "app/components/job/action"; +import { CertificateCreateBasicDialogComponent } from "./add/certificate-create-basic-dialog.component"; import { DeleteCertificateDialogComponent } from "./delete/delete-certificate-dialog.component"; import { ReactivateCertificateDialogComponent } from "./reactivate/reactivate-certificate-dialog.component"; const components = [ - DeleteCertificateDialogComponent, ReactivateCertificateDialogComponent, + DeleteCertificateDialogComponent, ReactivateCertificateDialogComponent, CertificateCreateBasicDialogComponent, ]; @NgModule({ diff --git a/app/components/certificate/home/certificate-home.component.ts b/app/components/certificate/home/certificate-home.component.ts index 594304824e..6c28d38661 100644 --- a/app/components/certificate/home/certificate-home.component.ts +++ b/app/components/certificate/home/certificate-home.component.ts @@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, Component } from "@angular/core"; import { FormBuilder, FormControl } from "@angular/forms"; import { Filter, FilterBuilder, autobind } from "@batch-flask/core"; -// import { SidebarManager } from "@batch-flask/ui/sidebar"; -// import { CertificateCreateBasicDialogComponent } from "../action"; +import { SidebarManager } from "@batch-flask/ui/sidebar"; +import { CertificateCreateBasicDialogComponent } from "../action/add"; @Component({ selector: "bl-certificate-home", @@ -23,10 +23,7 @@ export class CertificateHomeComponent { keyField: "thumbprint", }; - constructor( - formBuilder: FormBuilder, - // private sidebarManager: SidebarManager - ) { + constructor(formBuilder: FormBuilder, private sidebarManager: SidebarManager) { this.quickSearchQuery.valueChanges.debounceTime(400).distinctUntilChanged().subscribe((query: string) => { if (query === "") { this.quickFilter = FilterBuilder.none(); @@ -40,7 +37,7 @@ export class CertificateHomeComponent { @autobind() public addCertificate() { - // this.sidebarManager.open("add-certificate", CertificateCreateBasicDialogComponent); + this.sidebarManager.open("add-certificate", CertificateCreateBasicDialogComponent); } public advancedFilterChanged(filter: Filter) { diff --git a/app/models/forms/create-certificate-model.ts b/app/models/forms/create-certificate-model.ts new file mode 100644 index 0000000000..378e046a3e --- /dev/null +++ b/app/models/forms/create-certificate-model.ts @@ -0,0 +1,32 @@ +import { CertificateCreateDto } from "app/models/dtos"; + +export type CertificateFormat = "pfx" | "cer"; + +export interface CreateCertificateModel { + certificateFormat: CertificateFormat; + data: string; + password?: string; + thumbprint: string; + thumbprintAlgorithm: string; +} + +export function createCertificateFormToJsonData(formData: CreateCertificateModel): CertificateCreateDto { + const data: any = { + certificateFormat: formData.certificateFormat, + data: formData.data, + password: formData.password, + thumbprint: formData.thumbprint, + thumbprintAlgorithm: formData.thumbprintAlgorithm, + }; + return new CertificateCreateDto(data); +} + +export function certificateToFormModel(certificate: CertificateCreateDto): CreateCertificateModel { + return { + certificateFormat: certificate.certificateFormat, + data: certificate.data, + password: certificate.password, + thumbprint: certificate.thumbprint, + thumbprintAlgorithm: certificate.thumbprintAlgorithm, + }; +} diff --git a/app/models/forms/index.ts b/app/models/forms/index.ts index 8bff78438b..372f89b65c 100644 --- a/app/models/forms/index.ts +++ b/app/models/forms/index.ts @@ -1,4 +1,5 @@ export * from "./create-application-model"; +export * from "./create-certificate-model"; export * from "./create-form-group-model"; export * from "./create-job-model"; export * from "./create-job-schedule-model"; From 238a27836fcc386dad3ce474311e9459184da1c9 Mon Sep 17 00:00:00 2001 From: Yunlong Zhang Date: Fri, 16 Mar 2018 10:46:44 -0700 Subject: [PATCH 12/17] added file picker --- ...rtificate-create-basic-dialog.component.ts | 22 ++++++++++++++++++- .../add/certificate-create-basic-dialog.html | 16 ++++++++++++-- src/common/constants/constants.ts | 1 + 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/components/certificate/action/add/certificate-create-basic-dialog.component.ts b/app/components/certificate/action/add/certificate-create-basic-dialog.component.ts index b15496c29d..c7199a45db 100644 --- a/app/components/certificate/action/add/certificate-create-basic-dialog.component.ts +++ b/app/components/certificate/action/add/certificate-create-basic-dialog.component.ts @@ -1,5 +1,5 @@ import { Component } from "@angular/core"; -import { FormBuilder } from "@angular/forms"; +import { FormBuilder, Validators } from "@angular/forms"; import { Observable } from "rxjs"; import { DynamicForm, autobind } from "@batch-flask/core"; @@ -10,6 +10,7 @@ import { Certificate } from "app/models"; import { CertificateCreateDto } from "app/models/dtos"; import { certificateToFormModel, createCertificateFormToJsonData } from "app/models/forms"; import { CertificateService } from "app/services"; +import { Constants } from "app/utils"; import "./certificate-create-basic-dialog.scss"; @@ -19,6 +20,7 @@ import "./certificate-create-basic-dialog.scss"; }) export class CertificateCreateBasicDialogComponent extends DynamicForm { public complexFormConfig: ComplexFormConfig; + public file: File; public fileUri = "create.certificate.batch.json"; constructor( @@ -28,6 +30,7 @@ export class CertificateCreateBasicDialogComponent extends DynamicForm 0) { + this.file = element.files[0]; + this.form.controls["certificate"].setValue(this.file.name); + } else { + this.file = null; + this.form.controls["certificate"].setValue(null); + } + } + private _setComplexFormConfig() { this.complexFormConfig = { jsonEditor: { diff --git a/app/components/certificate/action/add/certificate-create-basic-dialog.html b/app/components/certificate/action/add/certificate-create-basic-dialog.html index 97e124e25a..1989bf08bc 100644 --- a/app/components/certificate/action/add/certificate-create-basic-dialog.html +++ b/app/components/certificate/action/add/certificate-create-basic-dialog.html @@ -1,11 +1,23 @@ - + +
    + + Please select a valid certificate file + Certificate need to be valid CER or PFX files only + +
    +
    +
    - +
    diff --git a/src/common/constants/constants.ts b/src/common/constants/constants.ts index 2ae3c8a6d6..747a80f699 100644 --- a/src/common/constants/constants.ts +++ b/src/common/constants/constants.ts @@ -39,6 +39,7 @@ export const forms = { id: /^[\w\_-]+$/i, appVersion: /^[a-zA-Z0-9_-][a-zA-Z0-9_.-]*$/i, appFilename: /\.zip$/i, + certificateFileName: /(\.pfx|\.cer)$/i, fileGroup: /^[a-z0-9]([a-z0-9]|-(?!-|\z))*$/, batchAccount: /^[0-9a-z]*$/, }, From 1fdf525e9c9294b3c76025781183b78f9f843271 Mon Sep 17 00:00:00 2001 From: Yunlong Zhang Date: Fri, 16 Mar 2018 15:58:31 -0700 Subject: [PATCH 13/17] added function that get cert thumprint based uploaded cert added node-forge --- ...rtificate-create-basic-dialog.component.ts | 94 ------------------ .../certificate-create-dialog.component.ts | 82 ++++++++++++++++ ...og.html => certificate-create-dialog.html} | 10 +- ...og.scss => certificate-create-dialog.scss} | 0 .../certificate/action/add/index.ts | 2 +- .../action/certificate-action.module.ts | 4 +- .../home/certificate-home.component.ts | 4 +- app/models/dtos/certificate-create.dto.ts | 11 +-- app/models/forms/create-certificate-model.ts | 32 ------- app/models/forms/index.ts | 1 - app/services/certificate.service.ts | 96 ++++++++++++++++++- package.json | 1 + yarn.lock | 4 + 13 files changed, 187 insertions(+), 154 deletions(-) delete mode 100644 app/components/certificate/action/add/certificate-create-basic-dialog.component.ts create mode 100644 app/components/certificate/action/add/certificate-create-dialog.component.ts rename app/components/certificate/action/add/{certificate-create-basic-dialog.html => certificate-create-dialog.html} (68%) rename app/components/certificate/action/add/{certificate-create-basic-dialog.scss => certificate-create-dialog.scss} (100%) delete mode 100644 app/models/forms/create-certificate-model.ts diff --git a/app/components/certificate/action/add/certificate-create-basic-dialog.component.ts b/app/components/certificate/action/add/certificate-create-basic-dialog.component.ts deleted file mode 100644 index c7199a45db..0000000000 --- a/app/components/certificate/action/add/certificate-create-basic-dialog.component.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Component } from "@angular/core"; -import { FormBuilder, Validators } from "@angular/forms"; -import { Observable } from "rxjs"; - -import { DynamicForm, autobind } from "@batch-flask/core"; -import { ComplexFormConfig } from "@batch-flask/ui/form"; -import { NotificationService } from "@batch-flask/ui/notifications"; -import { SidebarRef } from "@batch-flask/ui/sidebar"; -import { Certificate } from "app/models"; -import { CertificateCreateDto } from "app/models/dtos"; -import { certificateToFormModel, createCertificateFormToJsonData } from "app/models/forms"; -import { CertificateService } from "app/services"; -import { Constants } from "app/utils"; - -import "./certificate-create-basic-dialog.scss"; - -@Component({ - selector: "bl-certificate-create-basic-dialog", - templateUrl: "certificate-create-basic-dialog.html", -}) -export class CertificateCreateBasicDialogComponent extends DynamicForm { - public complexFormConfig: ComplexFormConfig; - public file: File; - public fileUri = "create.certificate.batch.json"; - - constructor( - private formBuilder: FormBuilder, - public sidebarRef: SidebarRef, - private certificateService: CertificateService, - private notificationService: NotificationService) { - super(CertificateCreateDto); - this._setComplexFormConfig(); - const validation = Constants.forms.validation; - - this.form = this.formBuilder.group({ - certificateFormat: [null], - data: [null], - password: [null], - thumbprint: [null], - thumbprintAlgorithm: [null], - certificate: ["", [ - Validators.required, - Validators.pattern(validation.regex.certificateFileName), - ]], - }); - } - - public dtoToForm(certificate: CertificateCreateDto) { - return certificateToFormModel(certificate); - } - - public formToDto(data: any): CertificateCreateDto { - return createCertificateFormToJsonData(data); - } - - @autobind() - public submit(data: CertificateCreateDto): Observable { - const thumbprint = data.thumbprint; - const obs = this.certificateService.add(data); - obs.subscribe({ - next: () => { - this.certificateService.onCertificateAdded.next(thumbprint); - this.notificationService.success("Certificate added!", - `Certificate '${thumbprint}' was created successfully!`); - }, - error: () => null, - }); - - return obs; - } - - public fileSelected(changeEvent: Event) { - const element = changeEvent.srcElement as any; - this.form.controls["certificate"].markAsTouched(); - - if (element.files.length > 0) { - this.file = element.files[0]; - this.form.controls["certificate"].setValue(this.file.name); - } else { - this.file = null; - this.form.controls["certificate"].setValue(null); - } - } - - private _setComplexFormConfig() { - this.complexFormConfig = { - jsonEditor: { - dtoType: CertificateCreateDto, - toDto: (value) => this.formToDto(value), - fromDto: (value) => this.dtoToForm(value), - }, - }; - } -} diff --git a/app/components/certificate/action/add/certificate-create-dialog.component.ts b/app/components/certificate/action/add/certificate-create-dialog.component.ts new file mode 100644 index 0000000000..41bf7a7acb --- /dev/null +++ b/app/components/certificate/action/add/certificate-create-dialog.component.ts @@ -0,0 +1,82 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormGroup, Validators } from "@angular/forms"; +import { Observable } from "rxjs"; + +import { autobind } from "@batch-flask/core"; +import { NotificationService } from "@batch-flask/ui/notifications"; +import { SidebarRef } from "@batch-flask/ui/sidebar"; +import { CertificateCreateDto } from "app/models/dtos"; +import { CertificateFormat, CertificateService } from "app/services"; +import { Constants } from "app/utils"; + +import "./certificate-create-dialog.scss"; + +@Component({ + selector: "bl-certificate-create-dialog", + templateUrl: "certificate-create-dialog.html", +}) +export class CertificateCreateDialogComponent { + public file: File; + public form: FormGroup; + + constructor( + private formBuilder: FormBuilder, + public sidebarRef: SidebarRef, + private certificateService: CertificateService, + private notificationService: NotificationService) { + const validation = Constants.forms.validation; + + this.form = this.formBuilder.group({ + certificate: ["", [ + Validators.required, + Validators.pattern(validation.regex.certificateFileName), + ]], + password: [null], + }); + } + + @autobind() + public submit(data: CertificateCreateDto): Observable { + const obs = this.certificateService.parseCertificate(this.file, data.password); + + obs.subscribe({ + next: (certificate: any) => { + const obs = this.certificateService.add(certificate); + obs.subscribe({ + next: () => { + this.certificateService.onCertificateAdded.next(certificate.thumbprint); + this.notificationService.success("Certificate added!", + `Certificate '${certificate.thumbprint}' was created successfully!`); + }, + error: () => null, + }); + }, + error: (response: Response) => { + this.notificationService.error( + "Certificate creation failed", + response.toString(), + ); + }, + }); + return obs; + } + + public fileSelected(changeEvent: Event) { + const element = changeEvent.srcElement as any; + this.form.controls["certificate"].markAsTouched(); + + if (element.files.length > 0) { + this.file = element.files[0]; + this.form.controls["certificate"].setValue(this.file.name); + } else { + this.file = null; + this.form.controls["certificate"].setValue(null); + } + + this.form.controls["password"].setValue(null); + } + + public showPassword() { + return this.file && this.certificateService.getCertificateExtension(this.file) === CertificateFormat.pfx; + } +} diff --git a/app/components/certificate/action/add/certificate-create-basic-dialog.html b/app/components/certificate/action/add/certificate-create-dialog.html similarity index 68% rename from app/components/certificate/action/add/certificate-create-basic-dialog.html rename to app/components/certificate/action/add/certificate-create-dialog.html index 1989bf08bc..dbd523684c 100644 --- a/app/components/certificate/action/add/certificate-create-basic-dialog.html +++ b/app/components/certificate/action/add/certificate-create-dialog.html @@ -1,18 +1,14 @@ - +
    Please select a valid certificate file Certificate need to be valid CER or PFX files only -
    + {{ showPassword }}
    - +
    diff --git a/app/components/certificate/action/add/certificate-create-basic-dialog.scss b/app/components/certificate/action/add/certificate-create-dialog.scss similarity index 100% rename from app/components/certificate/action/add/certificate-create-basic-dialog.scss rename to app/components/certificate/action/add/certificate-create-dialog.scss diff --git a/app/components/certificate/action/add/index.ts b/app/components/certificate/action/add/index.ts index fea3fbeb7b..58c8a7b0a7 100644 --- a/app/components/certificate/action/add/index.ts +++ b/app/components/certificate/action/add/index.ts @@ -1 +1 @@ -export * from "./certificate-create-basic-dialog.component"; +export * from "./certificate-create-dialog.component"; diff --git a/app/components/certificate/action/certificate-action.module.ts b/app/components/certificate/action/certificate-action.module.ts index 44c6eccddb..c7d85081c6 100644 --- a/app/components/certificate/action/certificate-action.module.ts +++ b/app/components/certificate/action/certificate-action.module.ts @@ -3,12 +3,12 @@ import { NgModule } from "@angular/core"; import { BaseModule } from "@batch-flask/ui"; import { commonModules } from "app/common"; import { JobActionModule } from "app/components/job/action"; -import { CertificateCreateBasicDialogComponent } from "./add/certificate-create-basic-dialog.component"; +import { CertificateCreateDialogComponent } from "./add/certificate-create-dialog.component"; import { DeleteCertificateDialogComponent } from "./delete/delete-certificate-dialog.component"; import { ReactivateCertificateDialogComponent } from "./reactivate/reactivate-certificate-dialog.component"; const components = [ - DeleteCertificateDialogComponent, ReactivateCertificateDialogComponent, CertificateCreateBasicDialogComponent, + DeleteCertificateDialogComponent, ReactivateCertificateDialogComponent, CertificateCreateDialogComponent, ]; @NgModule({ diff --git a/app/components/certificate/home/certificate-home.component.ts b/app/components/certificate/home/certificate-home.component.ts index 6c28d38661..980d72f02d 100644 --- a/app/components/certificate/home/certificate-home.component.ts +++ b/app/components/certificate/home/certificate-home.component.ts @@ -3,7 +3,7 @@ import { FormBuilder, FormControl } from "@angular/forms"; import { Filter, FilterBuilder, autobind } from "@batch-flask/core"; import { SidebarManager } from "@batch-flask/ui/sidebar"; -import { CertificateCreateBasicDialogComponent } from "../action/add"; +import { CertificateCreateDialogComponent } from "../action/add"; @Component({ selector: "bl-certificate-home", @@ -37,7 +37,7 @@ export class CertificateHomeComponent { @autobind() public addCertificate() { - this.sidebarManager.open("add-certificate", CertificateCreateBasicDialogComponent); + this.sidebarManager.open("add-certificate", CertificateCreateDialogComponent); } public advancedFilterChanged(filter: Filter) { diff --git a/app/models/dtos/certificate-create.dto.ts b/app/models/dtos/certificate-create.dto.ts index f2d2810519..ebd0b3644f 100644 --- a/app/models/dtos/certificate-create.dto.ts +++ b/app/models/dtos/certificate-create.dto.ts @@ -1,15 +1,6 @@ import { Dto, DtoAttr } from "@batch-flask/core"; -export type CertificateFormat = "cer" | "pfx"; - export class CertificateCreateDto extends Dto { - @DtoAttr() public thumbprintAlgorithm?: string; - - @DtoAttr() public thumbprint?: string; - + @DtoAttr() public certificate?: string; @DtoAttr() public password?: string; - - @DtoAttr() public data?: string; - - @DtoAttr() public certificateFormat?: CertificateFormat; } diff --git a/app/models/forms/create-certificate-model.ts b/app/models/forms/create-certificate-model.ts deleted file mode 100644 index 378e046a3e..0000000000 --- a/app/models/forms/create-certificate-model.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { CertificateCreateDto } from "app/models/dtos"; - -export type CertificateFormat = "pfx" | "cer"; - -export interface CreateCertificateModel { - certificateFormat: CertificateFormat; - data: string; - password?: string; - thumbprint: string; - thumbprintAlgorithm: string; -} - -export function createCertificateFormToJsonData(formData: CreateCertificateModel): CertificateCreateDto { - const data: any = { - certificateFormat: formData.certificateFormat, - data: formData.data, - password: formData.password, - thumbprint: formData.thumbprint, - thumbprintAlgorithm: formData.thumbprintAlgorithm, - }; - return new CertificateCreateDto(data); -} - -export function certificateToFormModel(certificate: CertificateCreateDto): CreateCertificateModel { - return { - certificateFormat: certificate.certificateFormat, - data: certificate.data, - password: certificate.password, - thumbprint: certificate.thumbprint, - thumbprintAlgorithm: certificate.thumbprintAlgorithm, - }; -} diff --git a/app/models/forms/index.ts b/app/models/forms/index.ts index 372f89b65c..8bff78438b 100644 --- a/app/models/forms/index.ts +++ b/app/models/forms/index.ts @@ -1,5 +1,4 @@ export * from "./create-application-model"; -export * from "./create-certificate-model"; export * from "./create-form-group-model"; export * from "./create-job-model"; export * from "./create-job-schedule-model"; diff --git a/app/services/certificate.service.ts b/app/services/certificate.service.ts index 5deada3222..0af7b8bdef 100644 --- a/app/services/certificate.service.ts +++ b/app/services/certificate.service.ts @@ -1,9 +1,10 @@ import { Injectable } from "@angular/core"; import { List } from "immutable"; -import { Observable, Subject } from "rxjs"; +import { AsyncSubject, Observable, Subject } from "rxjs"; +// tslint:disable-next-line:no-var-requires +const forge = require("node-forge"); import { Certificate } from "app/models"; -import { CertificateCreateDto } from "app/models/dtos"; import { Constants, log } from "app/utils"; import { BatchClientService } from "./batch-client.service"; import { @@ -23,7 +24,12 @@ export interface CertificateParams { export interface CertificateListOptions extends ListOptionsAttributes { } -const defaultThumbprintAlgorithm = "sha1"; +export const defaultThumbprintAlgorithm = "sha1"; + +export enum CertificateFormat { + pfx = "pfx", + cer = "cer", +} @Injectable() export class CertificateService extends ServiceBase { @@ -110,8 +116,8 @@ export class CertificateService extends ServiceBase { }); } - public add(certificate: CertificateCreateDto, options: any = {}): Observable<{}> { - return this.callBatchClient((client) => client.certificate.add(certificate.toJS(), options)); + public add(certificate: any, options: any = {}): Observable<{}> { + return this.callBatchClient((client) => client.certificate.add(certificate, options)); } public cancelDelete(thumbprint: string, options: any = {}, thumbprintAlgorithm?: string) { @@ -122,4 +128,84 @@ export class CertificateService extends ServiceBase { log.error(`Error cancel delete certificate: ${thumbprint}`, error); }); } + + /** + * parseCertificate helps to parse uploaded certificate and return parameters which can be + * used to create batch certificate + * @param file uploaded certificate + * @param password password for pfx certifcate + */ + public parseCertificate(file: File, password: string) { + const subject = new AsyncSubject(); + const reader = new FileReader(); + reader.readAsBinaryString(file); + reader.onload = () => { + // try catch potential error when get thumbprint + try { + const certificateFormat = this.getCertificateExtension(file); + const binaryEncodedData = reader.result; + const base64EncodedData = btoa(binaryEncodedData); + const isCer = certificateFormat === CertificateFormat.cer; + const data = isCer ? binaryEncodedData : base64EncodedData; + const thumbprint = this._generateThumbprint(data, + CertificateFormat[certificateFormat], password); + subject.next({ + thumbprintAlgorithm: defaultThumbprintAlgorithm, + thumbprint: thumbprint, + data: base64EncodedData, + certificateFormat: certificateFormat, + password: !isCer ? password : null, + }); + subject.complete(); + } catch (err) { + subject.error(err); + } + }; + reader.onerror = () => { + subject.error("Error encountered reading certificate file as binary string."); + }; + return subject.asObservable(); + } + + /** + * Helper function to get certificate extension string + * @param file + */ + public getCertificateExtension(file: File) { + return file.name.substr(file.name.length - 3, 3).toLowerCase(); + } + + /** + * This function is a helper function for generating certificate thumbprint based on input + * data, certificate format and password. Now only support pfx and cer certificate thumbprint generation + * @param data binary string of uploaded file + * @param format certificate type. Ex. cer or pfx + * @param password only specify password when foramt is pfx + */ + private _generateThumbprint(data: string, format: CertificateFormat, password?: string): string { + let certDer: string = null; + switch (format) { + case CertificateFormat.pfx: + const p12Der = forge.util.decode64(data); + const p12Asn1 = forge.asn1.fromDer(p12Der); + const outCert = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, password); + const keyBags = outCert.getBags({ bagType: forge.pki.oids.certBag }); + const bag = keyBags[forge.pki.oids.certBag][0]; + const certAsn1 = forge.pki.certificateToAsn1(bag.cert); + certDer = forge.asn1.toDer(certAsn1).getBytes(); + break; + case CertificateFormat.cer: + const outAsn1 = forge.asn1.fromDer(data); + certDer = forge.asn1.toDer(outAsn1).getBytes(); + break; + default: + throw new Error(`Supported certificate type are CER and PFX, + current certificate type is not supported.`); + } + const md = forge.md.sha1.create(); + md.start(); + md.update(certDer); + const digest = md.digest(); + return digest.toHex(); + } } diff --git a/package.json b/package.json index 40119080a5..43b9df4cce 100644 --- a/package.json +++ b/package.json @@ -155,6 +155,7 @@ "mousetrap": "^1.6.0", "ms-rest-js": "~0.2.3", "node-fetch": "~1.7.3", + "node-forge": "^0.7.4", "reflect-metadata": "^0.1.12", "rxjs": "~5.5.6", "strip-json-comments": "~2.0.1", diff --git a/yarn.lock b/yarn.lock index 99b59af914..200010aacd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6196,6 +6196,10 @@ node-forge@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300" +node-forge@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.4.tgz#8e6e9f563a1e32213aa7508cded22aa791dbf986" + node-gyp@^3.3.1: version "3.6.2" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60" From 379fdb111ee30f17015c8a2fbcbacd43fe976c85 Mon Sep 17 00:00:00 2001 From: Yunlong Zhang Date: Fri, 16 Mar 2018 16:24:06 -0700 Subject: [PATCH 14/17] added password validation --- .../certificate-create-dialog.component.ts | 23 +++++++++++++++---- .../action/add/certificate-create-dialog.html | 5 ++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/components/certificate/action/add/certificate-create-dialog.component.ts b/app/components/certificate/action/add/certificate-create-dialog.component.ts index 41bf7a7acb..26e40beae8 100644 --- a/app/components/certificate/action/add/certificate-create-dialog.component.ts +++ b/app/components/certificate/action/add/certificate-create-dialog.component.ts @@ -1,5 +1,5 @@ import { Component } from "@angular/core"; -import { FormBuilder, FormGroup, Validators } from "@angular/forms"; +import { FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms"; import { Observable } from "rxjs"; import { autobind } from "@batch-flask/core"; @@ -31,7 +31,7 @@ export class CertificateCreateDialogComponent { Validators.required, Validators.pattern(validation.regex.certificateFileName), ]], - password: [null], + password: [null, this._passwordValidator()], }); } @@ -64,7 +64,6 @@ export class CertificateCreateDialogComponent { public fileSelected(changeEvent: Event) { const element = changeEvent.srcElement as any; this.form.controls["certificate"].markAsTouched(); - if (element.files.length > 0) { this.file = element.files[0]; this.form.controls["certificate"].setValue(this.file.name); @@ -72,11 +71,25 @@ export class CertificateCreateDialogComponent { this.file = null; this.form.controls["certificate"].setValue(null); } - this.form.controls["password"].setValue(null); } - public showPassword() { + public get showPassword() { return this.file && this.certificateService.getCertificateExtension(this.file) === CertificateFormat.pfx; } + + private _passwordValidator() { + return (control: FormControl): {[key: string]: any} => { + if (this.showPassword) { + if (!control.value) { + return { + pfxPasswordRequired: { + value: true, + }, + }; + } + } + return null; + }; + } } diff --git a/app/components/certificate/action/add/certificate-create-dialog.html b/app/components/certificate/action/add/certificate-create-dialog.html index dbd523684c..4aca449768 100644 --- a/app/components/certificate/action/add/certificate-create-dialog.html +++ b/app/components/certificate/action/add/certificate-create-dialog.html @@ -6,14 +6,13 @@ Please select a valid certificate file Certificate need to be valid CER or PFX files only
    - {{ showPassword }}
    - +
    - + Password is required if the certificate format is pfx
    From b41c951fdfd9dfb7380b86560555b1484c02271a Mon Sep 17 00:00:00 2001 From: Yunlong Zhang Date: Fri, 16 Mar 2018 16:57:05 -0700 Subject: [PATCH 15/17] Fixed action errors --- .../action/add/certificate-create-dialog.component.ts | 6 +++--- app/models/dtos/certificate-create.dto.ts | 6 ------ app/models/dtos/index.ts | 1 - app/services/certificate.service.ts | 5 ++--- 4 files changed, 5 insertions(+), 13 deletions(-) delete mode 100644 app/models/dtos/certificate-create.dto.ts diff --git a/app/components/certificate/action/add/certificate-create-dialog.component.ts b/app/components/certificate/action/add/certificate-create-dialog.component.ts index 26e40beae8..ec3008e771 100644 --- a/app/components/certificate/action/add/certificate-create-dialog.component.ts +++ b/app/components/certificate/action/add/certificate-create-dialog.component.ts @@ -5,7 +5,6 @@ import { Observable } from "rxjs"; import { autobind } from "@batch-flask/core"; import { NotificationService } from "@batch-flask/ui/notifications"; import { SidebarRef } from "@batch-flask/ui/sidebar"; -import { CertificateCreateDto } from "app/models/dtos"; import { CertificateFormat, CertificateService } from "app/services"; import { Constants } from "app/utils"; @@ -36,8 +35,9 @@ export class CertificateCreateDialogComponent { } @autobind() - public submit(data: CertificateCreateDto): Observable { - const obs = this.certificateService.parseCertificate(this.file, data.password); + public submit(): Observable { + const formData = this.form.value; + const obs = this.certificateService.parseCertificate(this.file, formData.password); obs.subscribe({ next: (certificate: any) => { diff --git a/app/models/dtos/certificate-create.dto.ts b/app/models/dtos/certificate-create.dto.ts deleted file mode 100644 index ebd0b3644f..0000000000 --- a/app/models/dtos/certificate-create.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Dto, DtoAttr } from "@batch-flask/core"; - -export class CertificateCreateDto extends Dto { - @DtoAttr() public certificate?: string; - @DtoAttr() public password?: string; -} diff --git a/app/models/dtos/index.ts b/app/models/dtos/index.ts index eb3bc1718e..9f639d381c 100644 --- a/app/models/dtos/index.ts +++ b/app/models/dtos/index.ts @@ -5,7 +5,6 @@ export * from "./account-create.dto"; export * from "./account-patch.dto"; export * from "./application-package-reference.dto"; -export * from "./certificate-create.dto"; export * from "./container-setup.dto"; export * from "./file-group-create.dto"; export * from "./file-group-options.dto"; diff --git a/app/services/certificate.service.ts b/app/services/certificate.service.ts index 0af7b8bdef..2e1857274b 100644 --- a/app/services/certificate.service.ts +++ b/app/services/certificate.service.ts @@ -106,8 +106,7 @@ export class CertificateService extends ServiceBase { }); } - public delete(thumbprint: string, options: any = {}, thumbprintAlgorithm?: string) - : Observable<{}> { + public delete(thumbprint: string, options: any = {}, thumbprintAlgorithm?: string): Observable<{}> { return this.callBatchClient((client) => { const algorithm = thumbprintAlgorithm || defaultThumbprintAlgorithm; return client.certificate.delete(algorithm, thumbprint, options); @@ -123,7 +122,7 @@ export class CertificateService extends ServiceBase { public cancelDelete(thumbprint: string, options: any = {}, thumbprintAlgorithm?: string) { return this.callBatchClient((client) => { const algorithm = thumbprintAlgorithm || defaultThumbprintAlgorithm; - return client.certificate.cancelDelete(algorithm, thumbprint, options); + return client.certificate.cancelDeletion(algorithm, thumbprint, options); }, (error) => { log.error(`Error cancel delete certificate: ${thumbprint}`, error); }); From 15449515691fc30c70c86359476e97ea7501b4a8 Mon Sep 17 00:00:00 2001 From: Yunlong Zhang Date: Mon, 19 Mar 2018 10:00:29 -0700 Subject: [PATCH 16/17] Code review update -Fixed typo, spell, and misc issues. --- app/app.routes.ts | 2 +- .../add/certificate-create-dialog.component.ts | 4 ++-- .../reactivate-certificate-dialog.html | 2 +- .../browse/certificate-list.component.ts | 2 +- app/services/certificate.service.ts | 17 ++++------------- 5 files changed, 9 insertions(+), 18 deletions(-) diff --git a/app/app.routes.ts b/app/app.routes.ts index 0eee512ae3..f1cac6a104 100644 --- a/app/app.routes.ts +++ b/app/app.routes.ts @@ -80,7 +80,7 @@ export const routes: Routes = [ canActivate: [NavigationGuard], component: CertificateHomeComponent, children: [ - { path: "", component: CertificateDefaultComponent }, // thumbprint/ + { path: "", component: CertificateDefaultComponent }, // certificates/ { path: ":thumbprint", component: CertificateDetailsComponent }, // certificate/{certificate.thumbprint} ], }, diff --git a/app/components/certificate/action/add/certificate-create-dialog.component.ts b/app/components/certificate/action/add/certificate-create-dialog.component.ts index ec3008e771..2750fac66c 100644 --- a/app/components/certificate/action/add/certificate-create-dialog.component.ts +++ b/app/components/certificate/action/add/certificate-create-dialog.component.ts @@ -6,7 +6,7 @@ import { autobind } from "@batch-flask/core"; import { NotificationService } from "@batch-flask/ui/notifications"; import { SidebarRef } from "@batch-flask/ui/sidebar"; import { CertificateFormat, CertificateService } from "app/services"; -import { Constants } from "app/utils"; +import { Constants, FileUrlUtils } from "app/utils"; import "./certificate-create-dialog.scss"; @@ -75,7 +75,7 @@ export class CertificateCreateDialogComponent { } public get showPassword() { - return this.file && this.certificateService.getCertificateExtension(this.file) === CertificateFormat.pfx; + return this.file && FileUrlUtils.getFileExtension(this.file.name) === CertificateFormat.pfx; } private _passwordValidator() { diff --git a/app/components/certificate/action/reactivate/reactivate-certificate-dialog.html b/app/components/certificate/action/reactivate/reactivate-certificate-dialog.html index ac8d8cf9c1..73415c91d1 100644 --- a/app/components/certificate/action/reactivate/reactivate-certificate-dialog.html +++ b/app/components/certificate/action/reactivate/reactivate-certificate-dialog.html @@ -1,3 +1,3 @@ -

    Do you want to reactivate the certificate'{{certificateThumbprint}}'?

    +

    Do you want to reactivate the certificate '{{certificateThumbprint}}'?

    diff --git a/app/components/certificate/browse/certificate-list.component.ts b/app/components/certificate/browse/certificate-list.component.ts index 5ce63804de..ae703c7ae5 100644 --- a/app/components/certificate/browse/certificate-list.component.ts +++ b/app/components/certificate/browse/certificate-list.component.ts @@ -102,7 +102,7 @@ export class CertificateListComponent extends ListBaseComponent implements OnIni case CertificateState.active: return ""; default: - return `Certficate is ${certificate.state}`; + return `Certificate is ${certificate.state}`; } } diff --git a/app/services/certificate.service.ts b/app/services/certificate.service.ts index 2e1857274b..356b386ec2 100644 --- a/app/services/certificate.service.ts +++ b/app/services/certificate.service.ts @@ -1,11 +1,10 @@ import { Injectable } from "@angular/core"; import { List } from "immutable"; +import * as forge from "node-forge"; import { AsyncSubject, Observable, Subject } from "rxjs"; -// tslint:disable-next-line:no-var-requires -const forge = require("node-forge"); import { Certificate } from "app/models"; -import { Constants, log } from "app/utils"; +import { Constants, FileUrlUtils, log } from "app/utils"; import { BatchClientService } from "./batch-client.service"; import { BatchEntityGetter, BatchListGetter, ContinuationToken, @@ -141,7 +140,7 @@ export class CertificateService extends ServiceBase { reader.onload = () => { // try catch potential error when get thumbprint try { - const certificateFormat = this.getCertificateExtension(file); + const certificateFormat = FileUrlUtils.getFileExtension(file.name); const binaryEncodedData = reader.result; const base64EncodedData = btoa(binaryEncodedData); const isCer = certificateFormat === CertificateFormat.cer; @@ -166,20 +165,12 @@ export class CertificateService extends ServiceBase { return subject.asObservable(); } - /** - * Helper function to get certificate extension string - * @param file - */ - public getCertificateExtension(file: File) { - return file.name.substr(file.name.length - 3, 3).toLowerCase(); - } - /** * This function is a helper function for generating certificate thumbprint based on input * data, certificate format and password. Now only support pfx and cer certificate thumbprint generation * @param data binary string of uploaded file * @param format certificate type. Ex. cer or pfx - * @param password only specify password when foramt is pfx + * @param password only specify password when format is pfx */ private _generateThumbprint(data: string, format: CertificateFormat, password?: string): string { let certDer: string = null; From 05e877e183cd5b2625685939d079af9e6dad17a9 Mon Sep 17 00:00:00 2001 From: Yunlong Zhang Date: Thu, 22 Mar 2018 13:40:42 -0700 Subject: [PATCH 17/17] update prefix of node-forge version --- package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 148c829205..33c8c01bf7 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "mousetrap": "^1.6.0", "ms-rest-js": "~0.2.3", "node-fetch": "~1.7.3", - "node-forge": "^0.7.4", + "node-forge": "~0.7.4", "reflect-metadata": "^0.1.12", "rxjs": "~5.5.6", "strip-json-comments": "~2.0.1", diff --git a/yarn.lock b/yarn.lock index 04e1d65dfb..74f033b71e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6196,7 +6196,7 @@ node-forge@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300" -node-forge@^0.7.4: +node-forge@~0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.4.tgz#8e6e9f563a1e32213aa7508cded22aa791dbf986"