diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 59f80b6973b..b74bebe1617 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5734,6 +5734,12 @@ "deleteSecrets":{ "message": "Delete secrets" }, + "hardDeleteSecret":{ + "message": "Permanently delete secret" + }, + "hardDeleteSecrets":{ + "message": "Permanently delete secrets" + }, "secretProjectAssociationDescription" :{ "message": "Select projects that the secret will be associated with. Only organization users with access to these projects will be able to see the secret." }, @@ -5788,6 +5794,9 @@ "secretsNoItemsMessage":{ "message": "To get started, add a new secret or import secrets." }, + "secretsTrashNoItemsMessage":{ + "message": "There are no secrets in the trash." + }, "serviceAccountsNoItemsTitle":{ "message":"Nothing to show yet" }, @@ -5833,6 +5842,15 @@ "softDeletesSuccessToast":{ "message":"Secrets sent to trash" }, + "hardDeleteSecretConfirmation": { + "message": "Are you sure you want to permanently delete this secret?" + }, + "hardDeleteSecretsConfirmation": { + "message": "Are you sure you want to permanently delete these secrets?" + }, + "hardDeletesSuccessToast":{ + "message":"Secrets permanently deleted" + }, "serviceAccountCreated":{ "message":"Service account created" }, @@ -5911,6 +5929,9 @@ "softDeleteSuccessToast":{ "message":"Secret sent to trash" }, + "hardDeleteSuccessToast":{ + "message":"Secret permanently deleted" + }, "searchProjects":{ "message":"Search projects" }, @@ -6399,6 +6420,24 @@ "errorReadingImportFile": { "message": "An error occurred when trying to read the import file" }, + "restoreSecret": { + "message": "Restore secret" + }, + "restoreSecrets": { + "message": "Restore secrets" + }, + "restoreSecretPrompt": { + "message": "Are you sure you want to restore this secret?" + }, + "restoreSecretsPrompt": { + "message": "Are you sure you want to restore these secrets?" + }, + "secretRestoredSuccessToast": { + "message": "Secret restored" + }, + "secretsRestoredSuccessToast": { + "message": "Secrets restored" + }, "selectionIsRequired": { "message": "Selection is required." } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-delete.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-delete.component.html index 301c36a901f..aba920d04fd 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-delete.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-delete.component.html @@ -1,6 +1,6 @@ {{ title | i18n }} - +
{{ "softDeleteSecretWarning" | i18n }}
@@ -8,7 +8,7 @@
- - {{ "secretsNoItemsTitle" | i18n }} - {{ "secretsNoItemsMessage" | i18n }} - - + + + {{ "secretsNoItemsTitle" | i18n }} + {{ "secretsTrashNoItemsMessage" | i18n }} + + + {{ "secretsNoItemsTitle" | i18n }} + {{ "secretsNoItemsMessage" | i18n }} + + + @@ -86,7 +92,7 @@ - @@ -98,9 +104,14 @@ {{ "copySecretValue" | i18n }} - + + +
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-hard-delete.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-hard-delete.component.ts new file mode 100644 index 00000000000..2705f2c1e0b --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-hard-delete.component.ts @@ -0,0 +1,42 @@ +import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; + +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; + +import { SecretService } from "../../secrets/secret.service"; + +export interface SecretHardDeleteOperation { + secretIds: string[]; + organizationId: string; +} + +@Component({ + selector: "sm-secret-hard-delete-dialog", + templateUrl: "./secret-hard-delete.component.html", +}) +export class SecretHardDeleteDialogComponent { + constructor( + public dialogRef: DialogRef, + private secretService: SecretService, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + @Inject(DIALOG_DATA) public data: SecretHardDeleteOperation + ) {} + + get title() { + return this.data.secretIds.length === 1 ? "hardDeleteSecret" : "hardDeleteSecrets"; + } + + get submitButtonText() { + return this.data.secretIds.length === 1 ? "deleteSecret" : "deleteSecrets"; + } + + delete = async () => { + await this.secretService.deleteTrashed(this.data.organizationId, this.data.secretIds); + const message = + this.data.secretIds.length === 1 ? "hardDeleteSuccessToast" : "hardDeletesSuccessToast"; + this.dialogRef.close(this.data.secretIds); + this.platformUtilsService.showToast("success", null, this.i18nService.t(message)); + }; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-restore.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-restore.component.html new file mode 100644 index 00000000000..8cfc29aa9c0 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-restore.component.html @@ -0,0 +1,16 @@ + + {{ title | i18n }} + + {{ + data.secretIds.length === 1 ? ("restoreSecretPrompt" | i18n) : ("restoreSecretsPrompt" | i18n) + }} + +
+ + +
+
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-restore.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-restore.component.ts new file mode 100644 index 00000000000..7348c598aeb --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-restore.component.ts @@ -0,0 +1,41 @@ +import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; + +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; + +import { SecretService } from "../../secrets/secret.service"; + +export interface SecretRestoreOperation { + secretIds: string[]; + organizationId: string; +} + +@Component({ + selector: "sm-secret-restore-dialog", + templateUrl: "./secret-restore.component.html", +}) +export class SecretRestoreDialogComponent { + constructor( + public dialogRef: DialogRef, + private secretService: SecretService, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + @Inject(DIALOG_DATA) public data: SecretRestoreOperation + ) {} + + get title() { + return this.data.secretIds.length === 1 ? "restoreSecret" : "restoreSecrets"; + } + + restore = async () => { + let message = ""; + await this.secretService.restoreTrashed(this.data.organizationId, this.data.secretIds); + message = + this.data.secretIds.length === 1 + ? "secretRestoredSuccessToast" + : "secretsRestoredSuccessToast"; + this.dialogRef.close(this.data.secretIds); + this.platformUtilsService.showToast("success", null, this.i18nService.t(message)); + }; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash-routing.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash-routing.module.ts new file mode 100644 index 00000000000..e7132779177 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; + +import { TrashComponent } from "./trash.component"; + +const routes: Routes = [ + { + path: "", + component: TrashComponent, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class TrashRoutingModule {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.html new file mode 100644 index 00000000000..3936e63790a --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.html @@ -0,0 +1,9 @@ + + + + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts new file mode 100644 index 00000000000..ee34a60a27a --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts @@ -0,0 +1,66 @@ +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { combineLatestWith, Observable, startWith, switchMap } from "rxjs"; + +import { DialogService } from "@bitwarden/components"; + +import { SecretListView } from "../models/view/secret-list.view"; +import { SecretService } from "../secrets/secret.service"; + +import { + SecretHardDeleteDialogComponent, + SecretHardDeleteOperation, +} from "./dialog/secret-hard-delete.component"; +import { + SecretRestoreDialogComponent, + SecretRestoreOperation, +} from "./dialog/secret-restore.component"; + +@Component({ + selector: "sm-trash", + templateUrl: "./trash.component.html", +}) +export class TrashComponent implements OnInit { + secrets$: Observable; + + private organizationId: string; + + constructor( + private route: ActivatedRoute, + private secretService: SecretService, + private dialogService: DialogService + ) {} + + ngOnInit() { + this.secrets$ = this.secretService.secret$.pipe( + startWith(null), + combineLatestWith(this.route.params), + switchMap(async ([_, params]) => { + this.organizationId = params.organizationId; + return await this.getSecrets(); + }) + ); + } + + private async getSecrets(): Promise { + return await this.secretService.getTrashedSecrets(this.organizationId); + } + + openDeleteSecret(secretIds: string[]) { + this.dialogService.open(SecretHardDeleteDialogComponent, { + data: { + secretIds: secretIds, + organizationId: this.organizationId, + }, + }); + } + + openRestoreSecret(secretIds: string[]) { + this.dialogService.open(SecretRestoreDialogComponent, { + data: { + secretIds: secretIds, + organizationId: this.organizationId, + }, + }); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.module.ts new file mode 100644 index 00000000000..a6a45b73872 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from "@angular/core"; + +import { SecretsManagerSharedModule } from "../shared/sm-shared.module"; + +import { SecretHardDeleteDialogComponent } from "./dialog/secret-hard-delete.component"; +import { SecretRestoreDialogComponent } from "./dialog/secret-restore.component"; +import { TrashRoutingModule } from "./trash-routing.module"; +import { TrashComponent } from "./trash.component"; + +@NgModule({ + imports: [SecretsManagerSharedModule, TrashRoutingModule], + declarations: [SecretHardDeleteDialogComponent, SecretRestoreDialogComponent, TrashComponent], + providers: [], +}) +export class TrashModule {}