From fbd0d41b518445502b7d09d15ad27c9daa44acee Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 6 Mar 2023 20:25:06 +0100 Subject: [PATCH] [SM-568] Delete service accounts (#4881) --- apps/web/src/locales/en/messages.json | 27 ++++ ...rvice-account-delete-dialog.component.html | 35 +++++ ...service-account-delete-dialog.component.ts | 122 ++++++++++++++++++ .../service-account.service.ts | 16 +++ .../service-accounts-list.component.html | 2 +- .../service-accounts-list.component.ts | 10 +- .../service-accounts.component.html | 1 + .../service-accounts.component.ts | 15 +++ .../service-accounts.module.ts | 2 + 9 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.html create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 786ebea9f4..14077394f6 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5845,6 +5845,33 @@ "message": "View service account", "description": "Action to view the details of a service account." }, + "deleteServiceAccountDialogMessage": { + "message": "Deleting service account $SERVICE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "service_account": { + "content": "$1", + "example": "Service account name" + } + } + }, + "deleteServiceAccountsDialogMessage":{ + "message": "Deleting service accounts is permanent and irreversible." + }, + "deleteServiceAccountsConfirmMessage":{ + "message": "Delete $COUNT$ service accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteServiceAccountToast":{ + "message": "The service account have been deleted" + }, + "deleteServiceAccountsToast":{ + "message": "Service accounts deleted" + }, "searchServiceAccounts": { "message": "Search service accounts", "description": "Placeholder text for searching service accounts." diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.html new file mode 100644 index 0000000000..6be10fd5b8 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.html @@ -0,0 +1,35 @@ +
+ + + {{ title }} + + + {{ data.serviceAccounts[0].name }} + + + {{ data.serviceAccounts.length }} + {{ "serviceAccounts" | i18n }} + + + + +
+ + {{ dialogContent }} + + + {{ dialogConfirmationLabel }} + + +
+ +
+ + +
+
+
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts new file mode 100644 index 0000000000..3c1696d918 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts @@ -0,0 +1,122 @@ +import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; +import { + FormControl, + FormGroup, + ValidationErrors, + ValidatorFn, + AbstractControl, +} from "@angular/forms"; + +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; +import { DialogService } from "@bitwarden/components"; + +import { ServiceAccountView } from "../../models/view/service-account.view"; +import { + BulkOperationStatus, + BulkStatusDetails, + BulkStatusDialogComponent, +} from "../../shared/dialogs/bulk-status-dialog.component"; +import { ServiceAccountService } from "../service-account.service"; + +export interface ServiceAccountDeleteOperation { + serviceAccounts: ServiceAccountView[]; +} + +@Component({ + selector: "sm-service-account-delete-dialog", + templateUrl: "./service-account-delete-dialog.component.html", +}) +export class ServiceAccountDeleteDialogComponent { + formGroup = new FormGroup({ + confirmDelete: new FormControl("", [this.matchConfirmationMessageValidator()]), + }); + + constructor( + public dialogRef: DialogRef, + @Inject(DIALOG_DATA) public data: ServiceAccountDeleteOperation, + private serviceAccountService: ServiceAccountService, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private dialogService: DialogService + ) {} + + get title() { + return this.data.serviceAccounts.length === 1 + ? this.i18nService.t("deleteServiceAccount") + : this.i18nService.t("deleteServiceAccounts"); + } + + get dialogContent() { + return this.data.serviceAccounts.length === 1 + ? this.i18nService.t("deleteServiceAccountDialogMessage", this.data.serviceAccounts[0].name) + : this.i18nService.t("deleteServiceAccountsDialogMessage"); + } + + get dialogConfirmationLabel() { + return this.i18nService.t("deleteProjectInputLabel", this.dialogConfirmationMessage); + } + + submit = async () => { + this.formGroup.markAllAsTouched(); + + if (this.formGroup.invalid) { + return; + } + + await this.delete(); + this.dialogRef.close(); + }; + + async delete() { + const bulkResponses = await this.serviceAccountService.delete(this.data.serviceAccounts); + + const errors = bulkResponses.filter((response) => response.errorMessage); + if (errors.length > 0) { + this.openBulkStatusDialog(errors); + return; + } + + const message = + this.data.serviceAccounts.length === 1 + ? "deleteServiceAccountToast" + : "deleteServiceAccountsToast"; + this.platformUtilsService.showToast("success", null, this.i18nService.t(message)); + } + + openBulkStatusDialog(bulkStatusResults: BulkOperationStatus[]) { + this.dialogService.open(BulkStatusDialogComponent, { + data: { + title: "deleteServiceAccounts", + subTitle: "serviceAccounts", + columnTitle: "serviceAccountName", + message: "bulkDeleteProjectsErrorMessage", + details: bulkStatusResults, + }, + }); + } + + private get dialogConfirmationMessage() { + return this.data.serviceAccounts?.length === 1 + ? this.i18nService.t("deleteProjectConfirmMessage", this.data.serviceAccounts[0].name) + : this.i18nService.t( + "deleteServiceAccountsConfirmMessage", + this.data.serviceAccounts?.length.toString() + ); + } + + private matchConfirmationMessageValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (this.dialogConfirmationMessage.toLowerCase() == control.value.toLowerCase()) { + return null; + } else { + return { + confirmationDoesntMatchError: { + message: this.i18nService.t("smConfirmationRequired"), + }, + }; + } + }; + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.service.ts index 4870ac12f2..d9abfbf6d2 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.service.ts @@ -9,6 +9,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-cr import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { ServiceAccountView } from "../models/view/service-account.view"; +import { BulkOperationStatus } from "../shared/dialogs/bulk-status-dialog.component"; import { ServiceAccountRequest } from "./models/requests/service-account.request"; import { ServiceAccountResponse } from "./models/responses/service-account.response"; @@ -54,6 +55,21 @@ export class ServiceAccountService { ); } + async delete(serviceAccounts: ServiceAccountView[]): Promise { + const ids = serviceAccounts.map((serviceAccount) => serviceAccount.id); + const r = await this.apiService.send("POST", "/service-accounts/delete", ids, true, true); + + this._serviceAccount.next(null); + + return r.data.map((element: { id: string; error: string }) => { + const bulkOperationStatus = new BulkOperationStatus(); + bulkOperationStatus.id = element.id; + bulkOperationStatus.name = serviceAccounts.find((sa) => sa.id == element.id).name; + bulkOperationStatus.errorMessage = element.error; + return bulkOperationStatus; + }); + } + private async getOrganizationKey(organizationId: string): Promise { return await this.cryptoService.getOrgKey(organizationId); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html index e706a1f8bd..0a8237e8dd 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html @@ -83,7 +83,7 @@ {{ "viewServiceAccount" | i18n }} -