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 @@
+
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 }}
-