From af231b39146f867e0b15ba4bd245451512b4f4aa Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 2 Oct 2024 10:53:40 -0500 Subject: [PATCH 01/15] convert anchor link to button on trash page (#11351) --- .../trash-list-items-container.component.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html index 2ccfeaf3459..69322b08c8a 100644 --- a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html +++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html @@ -7,14 +7,15 @@ - {{ cipher.name }} - + @@ -119,26 +114,6 @@ {{ "clone" | i18n }} - - - - - - + + + + + + + + + +
+ +
+
+ diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts new file mode 100644 index 00000000000..4969ea2b16a --- /dev/null +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -0,0 +1,436 @@ +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { Router } from "@angular/router"; +import { firstValueFrom, Subject } from "rxjs"; +import { map } from "rxjs/operators"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; +import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { + AsyncActionsModule, + ButtonModule, + DialogModule, + DialogService, + ItemModule, + ToastService, +} from "@bitwarden/components"; +import { + CipherAttachmentsComponent, + CipherFormConfig, + CipherFormGenerationService, + CipherFormModule, + CipherViewComponent, +} from "@bitwarden/vault"; + +import { SharedModule } from "../../../shared/shared.module"; +import { + AttachmentDialogCloseResult, + AttachmentDialogResult, + AttachmentsV2Component, +} from "../../individual-vault/attachments-v2.component"; +import { WebCipherFormGenerationService } from "../../services/web-cipher-form-generation.service"; +import { WebVaultPremiumUpgradePromptService } from "../../services/web-premium-upgrade-prompt.service"; +import { WebViewPasswordHistoryService } from "../../services/web-view-password-history.service"; + +export type VaultItemDialogMode = "view" | "form"; + +export interface VaultItemDialogParams { + /** + * The mode of the dialog. + * - `view` is for viewing an existing cipher. + * - `form` is for editing or creating a new cipher. + */ + mode: VaultItemDialogMode; + + /** + * The configuration object for the dialog and form. + */ + formConfig: CipherFormConfig; + + /** + * If true, the "edit" button will be disabled in the dialog. + */ + disableForm?: boolean; +} + +export enum VaultItemDialogResult { + /** + * A cipher was saved (created or updated). + */ + Saved = "saved", + + /** + * A cipher was deleted. + */ + Deleted = "deleted", + + /** + * The dialog was closed to navigate the user the premium upgrade page. + */ + PremiumUpgrade = "premiumUpgrade", +} + +@Component({ + selector: "app-vault-item-dialog", + templateUrl: "vault-item-dialog.component.html", + standalone: true, + imports: [ + ButtonModule, + CipherViewComponent, + DialogModule, + CommonModule, + SharedModule, + CipherFormModule, + CipherAttachmentsComponent, + AsyncActionsModule, + ItemModule, + ], + providers: [ + { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService }, + { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, + { provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService }, + ], +}) +export class VaultItemDialogComponent implements OnInit, OnDestroy { + /** + * Reference to the dialog content element. Used to scroll to the top of the dialog when switching modes. + * @protected + */ + @ViewChild("dialogContent") + protected dialogContent: ElementRef; + + /** + * Tracks if the cipher was ever modified while the dialog was open. Used to ensure the dialog emits the correct result + * in case of closing with the X button or ESC key. + * @private + */ + private _cipherModified: boolean = false; + + /** + * The original mode of the form when the dialog is first opened. + * Used to determine if the form should switch to edit mode after successfully creating a new cipher. + * @private + */ + private _originalFormMode = this.params.formConfig.mode; + + /** + * Subject to emit when the form is ready to be displayed. + * @private + */ + private _formReadySubject = new Subject(); + + /** + * Tracks if the dialog is performing the initial load. Used to display a spinner while loading. + * @private + */ + protected performingInitialLoad: boolean = true; + + /** + * The title of the dialog. Updates based on the dialog mode and cipher type. + * @protected + */ + protected title: string; + + /** + * The current cipher being viewed. Undefined if creating a new cipher. + * @protected + */ + protected cipher?: CipherView; + + /** + * The organization the current cipher belongs to. Undefined if creating a new cipher. + * @protected + */ + protected organization?: Organization; + + /** + * The collections the current cipher is assigned to. Undefined if creating a new cipher. + * @protected + */ + protected collections?: CollectionView[]; + + /** + * Flag to indicate if the user has access to attachments via a premium subscription. + * @protected + */ + protected canAccessAttachments$ = this.billingAccountProfileStateService.hasPremiumFromAnySource$; + + protected get loadingForm() { + return this.loadForm && !this.formReady; + } + + protected get disableEdit() { + return this.params.disableForm; + } + + protected get canDelete() { + return this.cipher?.edit ?? false; + } + + protected get showCipherView() { + return this.cipher != undefined && (this.params.mode === "view" || this.loadingForm); + } + + /** + * Flag to initialize/attach the form component. + */ + protected loadForm = this.params.mode === "form"; + + /** + * Flag to indicate the form is ready to be displayed. + */ + protected formReady = false; + + protected formConfig: CipherFormConfig = this.params.formConfig; + + constructor( + @Inject(DIALOG_DATA) protected params: VaultItemDialogParams, + private dialogRef: DialogRef, + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + private messagingService: MessagingService, + private logService: LogService, + private cipherService: CipherService, + private accountService: AccountService, + private router: Router, + private billingAccountProfileStateService: BillingAccountProfileStateService, + ) { + this.updateTitle(); + } + + async ngOnInit() { + this.cipher = await this.getDecryptedCipherView(this.formConfig); + + if (this.cipher) { + this.collections = this.formConfig.collections.filter((c) => + this.cipher.collectionIds?.includes(c.id), + ); + this.organization = this.formConfig.organizations.find( + (o) => o.id === this.cipher.organizationId, + ); + } + + this.performingInitialLoad = false; + } + + ngOnDestroy() { + // If the cipher was modified, be sure we emit the saved result in case the dialog was closed with the X button or ESC key. + if (this._cipherModified) { + this.dialogRef.close(VaultItemDialogResult.Saved); + } + } + + /** + * Called by the CipherFormComponent when the cipher is saved successfully. + * @param cipherView - The newly saved cipher. + */ + protected async onCipherSaved(cipherView: CipherView) { + // We successfully saved the cipher, update the dialog state and switch to view mode. + this.cipher = cipherView; + this.collections = this.formConfig.collections.filter((c) => + cipherView.collectionIds?.includes(c.id), + ); + + // If the cipher was newly created (via add/clone), switch the form to edit for subsequent edits. + if (this._originalFormMode === "add" || this._originalFormMode === "clone") { + this.formConfig.mode = "edit"; + } + this.formConfig.originalCipher = await this.cipherService.get(cipherView.id); + this._cipherModified = true; + await this.changeMode("view"); + } + + /** + * Called by the CipherFormComponent when the form is ready to be displayed. + */ + protected onFormReady() { + this.formReady = true; + this._formReadySubject.next(); + } + + delete = async () => { + if (!this.cipher) { + return; + } + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { + key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation", + }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.deleteCipher(); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("success"), + message: this.i18nService.t( + this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem", + ), + }); + this.messagingService.send( + this.cipher.isDeleted ? "permanentlyDeletedCipher" : "deletedCipher", + ); + } catch (e) { + this.logService.error(e); + } + this._cipherModified = false; + this.dialogRef.close(VaultItemDialogResult.Deleted); + }; + + openAttachmentsDialog = async () => { + const dialogRef = this.dialogService.open( + AttachmentsV2Component, + { + data: { + cipherId: this.formConfig.originalCipher?.id as CipherId, + }, + }, + ); + + const result = await firstValueFrom(dialogRef.closed); + + if ( + result.action === AttachmentDialogResult.Removed || + result.action === AttachmentDialogResult.Uploaded + ) { + this._cipherModified = true; + } + }; + + switchToEdit = async () => { + if (!this.cipher) { + return; + } + await this.changeMode("form"); + }; + + cancel = async () => { + // We're in View mode, or we don't have a cipher, close the dialog. + if (this.params.mode === "view" || this.cipher == null) { + this.dialogRef.close(this._cipherModified ? VaultItemDialogResult.Saved : undefined); + return; + } + + // We're in Form mode, and we have a cipher, switch back to View mode. + await this.changeMode("view"); + }; + + private async getDecryptedCipherView(config: CipherFormConfig) { + if (config.originalCipher == null) { + return; + } + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + return await config.originalCipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(config.originalCipher, activeUserId), + ); + } + + private updateTitle() { + let partOne: string; + + if (this.params.mode === "view") { + partOne = "viewItemType"; + } else if (this.formConfig.mode === "edit" || this.formConfig.mode === "partial-edit") { + partOne = "editItemHeader"; + } else { + partOne = "newItemHeader"; + } + + const type = this.cipher?.type ?? this.formConfig.cipherType ?? CipherType.Login; + + switch (type) { + case CipherType.Login: + this.title = this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLowerCase()); + break; + case CipherType.Card: + this.title = this.i18nService.t(partOne, this.i18nService.t("typeCard").toLowerCase()); + break; + case CipherType.Identity: + this.title = this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLowerCase()); + break; + case CipherType.SecureNote: + this.title = this.i18nService.t(partOne, this.i18nService.t("note").toLowerCase()); + break; + } + } + + /** + * Changes the mode of the dialog. When switching to Form mode, the form is initialized first then displayed once ready. + * @param mode + * @private + */ + private async changeMode(mode: VaultItemDialogMode) { + this.formReady = false; + + if (mode == "form") { + this.loadForm = true; + // Wait for the formReadySubject to emit before continuing. + // This helps prevent flashing an empty dialog while the form is initializing. + await firstValueFrom(this._formReadySubject); + } else { + this.loadForm = false; + } + + this.params.mode = mode; + this.updateTitle(); + // Scroll to the top of the dialog content when switching modes. + this.dialogContent.nativeElement.parentElement.scrollTop = 0; + + // Update the URL query params to reflect the new mode. + await this.router.navigate([], { + queryParams: { + action: mode === "form" ? "edit" : "view", + itemId: this.cipher?.id, + }, + queryParamsHandling: "merge", + replaceUrl: true, + }); + } + + /** + * Helper method to delete cipher. + */ + private async deleteCipher(): Promise { + const asAdmin = this.organization?.canEditAllCiphers; + if (this.cipher.isDeleted) { + await this.cipherService.deleteWithServer(this.cipher.id, asAdmin); + } else { + await this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin); + } + } + + /** + * Opens the VaultItemDialog. + * @param dialogService + * @param params + */ + static open(dialogService: DialogService, params: VaultItemDialogParams) { + return dialogService.open( + VaultItemDialogComponent, + { + data: params, + }, + ); + } +} diff --git a/libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.html b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.html similarity index 100% rename from libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.html rename to apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.html diff --git a/libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.spec.ts b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.spec.ts similarity index 91% rename from libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.spec.ts rename to apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.spec.ts index 844f15a47af..653f553313a 100644 --- a/libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.spec.ts +++ b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.spec.ts @@ -1,4 +1,4 @@ -import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { mock, MockProxy } from "jest-mock-extended"; @@ -6,15 +6,16 @@ import { BehaviorSubject } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; - -import { UsernameGenerationServiceAbstraction } from "../../../../../../libs/tools/generator/extensions/legacy/src/username-generation.service.abstraction"; -import { CipherFormGeneratorComponent } from "../cipher-generator/cipher-form-generator.component"; +import { + PasswordGenerationServiceAbstraction, + UsernameGenerationServiceAbstraction, +} from "@bitwarden/generator-legacy"; +import { CipherFormGeneratorComponent } from "@bitwarden/vault"; import { + WebVaultGeneratorDialogAction, WebVaultGeneratorDialogComponent, WebVaultGeneratorDialogParams, - WebVaultGeneratorDialogAction, } from "./web-generator-dialog.component"; describe("WebVaultGeneratorDialogComponent", () => { diff --git a/libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.ts b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.ts similarity index 94% rename from libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.ts rename to apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.ts index 03a41990c86..91ab7ba7cc3 100644 --- a/libs/vault/src/cipher-form/components/web-generator-dialog/web-generator-dialog.component.ts +++ b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.ts @@ -3,11 +3,9 @@ import { CommonModule } from "@angular/common"; import { Component, Inject } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ButtonModule, DialogService } from "@bitwarden/components"; +import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; import { CipherFormGeneratorComponent } from "@bitwarden/vault"; -import { DialogModule } from "../../../../../../libs/components/src/dialog"; - export interface WebVaultGeneratorDialogParams { type: "password" | "username"; } diff --git a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts index 64935c8af38..85faac0c08c 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts @@ -17,9 +17,8 @@ import { CipherFormModule, } from "@bitwarden/vault"; -import { WebCipherFormGenerationService } from "../../../../../../libs/vault/src/cipher-form/services/web-cipher-form-generation.service"; -import { CipherViewComponent } from "../../../../../../libs/vault/src/cipher-view/cipher-view.component"; import { SharedModule } from "../../shared/shared.module"; +import { WebCipherFormGenerationService } from "../services/web-cipher-form-generation.service"; import { AttachmentsV2Component } from "./attachments-v2.component"; @@ -48,13 +47,13 @@ export interface AddEditCipherDialogCloseResult { /** * Component for viewing a cipher, presented in a dialog. + * @deprecated Use the VaultItemDialogComponent instead. */ @Component({ selector: "app-vault-add-edit-v2", templateUrl: "add-edit-v2.component.html", standalone: true, imports: [ - CipherViewComponent, CommonModule, AsyncActionsModule, DialogModule, diff --git a/apps/web/src/app/vault/individual-vault/attachments-v2.component.html b/apps/web/src/app/vault/individual-vault/attachments-v2.component.html index 532a0224be4..256082c2987 100644 --- a/apps/web/src/app/vault/individual-vault/attachments-v2.component.html +++ b/apps/web/src/app/vault/individual-vault/attachments-v2.component.html @@ -1,4 +1,4 @@ - + {{ "attachments" | i18n }} diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 4c4919cb058..734ab5acf1f 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -1,3 +1,4 @@ +import { DialogRef } from "@angular/cdk/dialog"; import { ChangeDetectorRef, Component, @@ -63,6 +64,7 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { DialogService, Icons, ToastService } from "@bitwarden/components"; import { + CipherFormConfig, CollectionAssignmentResult, DefaultCipherFormConfigService, PasswordRepromptService, @@ -75,16 +77,16 @@ import { CollectionDialogTabType, openCollectionDialog, } from "../components/collection-dialog"; +import { + VaultItemDialogComponent, + VaultItemDialogMode, + VaultItemDialogResult, +} from "../components/vault-item-dialog/vault-item-dialog.component"; import { VaultItem } from "../components/vault-items/vault-item"; import { VaultItemEvent } from "../components/vault-items/vault-item-event"; import { VaultItemsModule } from "../components/vault-items/vault-items.module"; import { getNestedCollectionTree } from "../utils/collection-utils"; -import { - AddEditCipherDialogCloseResult, - AddEditCipherDialogResult, - openAddEditCipherDialog, -} from "./add-edit-v2.component"; import { AddEditComponent } from "./add-edit.component"; import { AttachmentDialogCloseResult, @@ -116,11 +118,6 @@ import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/v import { VaultFilterModule } from "./vault-filter/vault-filter.module"; import { VaultHeaderComponent } from "./vault-header/vault-header.component"; import { VaultOnboardingComponent } from "./vault-onboarding/vault-onboarding.component"; -import { - openViewCipherDialog, - ViewCipherDialogCloseResult, - ViewCipherDialogResult, -} from "./view.component"; const BroadcasterSubscriptionId = "VaultComponent"; const SearchTextDebounceInterval = 200; @@ -179,6 +176,8 @@ export class VaultComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); private extensionRefreshEnabled: boolean; + private vaultItemDialogRef?: DialogRef | undefined; + constructor( private syncService: SyncService, private route: ActivatedRoute, @@ -352,12 +351,20 @@ export class VaultComponent implements OnInit, OnDestroy { firstSetup$ .pipe( switchMap(() => this.route.queryParams), + // Only process the queryParams if the dialog is not open (only when extension refresh is enabled) + filter(() => this.vaultItemDialogRef == undefined || !this.extensionRefreshEnabled), switchMap(async (params) => { const cipherId = getCipherIdFromParams(params); if (cipherId) { if (await this.cipherService.get(cipherId)) { - if (params.action === "view") { + let action = params.action; + // Default to "view" if extension refresh is enabled + if (action == null && this.extensionRefreshEnabled) { + action = "view"; + } + + if (action === "view") { await this.viewCipherById(cipherId); } else { await this.editCipherId(cipherId); @@ -526,7 +533,7 @@ export class VaultComponent implements OnInit, OnDestroy { */ async editCipherAttachments(cipher: CipherView) { if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) { - this.go({ cipherId: null, itemId: null }); + await this.go({ cipherId: null, itemId: null }); return; } @@ -590,6 +597,29 @@ export class VaultComponent implements OnInit, OnDestroy { }); } + /** + * Open the combined view / edit dialog for a cipher. + * @param mode - Starting mode of the dialog. + * @param formConfig - Configuration for the form when editing/adding a cipher. + */ + async openVaultItemDialog(mode: VaultItemDialogMode, formConfig: CipherFormConfig) { + this.vaultItemDialogRef = VaultItemDialogComponent.open(this.dialogService, { + mode, + formConfig, + }); + + const result = await lastValueFrom(this.vaultItemDialogRef.closed); + this.vaultItemDialogRef = undefined; + + // If the dialog was closed by deleting the cipher, refresh the vault. + if (result === VaultItemDialogResult.Deleted || result === VaultItemDialogResult.Saved) { + this.refresh(); + } + + // Clear the query params when the dialog closes + await this.go({ cipherId: null, itemId: null, action: null }); + } + async addCipher(cipherType?: CipherType) { if (this.extensionRefreshEnabled) { return this.addCipherV2(cipherType); @@ -643,23 +673,7 @@ export class VaultComponent implements OnInit, OnDestroy { folderId: this.activeFilter.folderId, }; - // Open the dialog. - const dialogRef = openAddEditCipherDialog(this.dialogService, { - data: cipherFormConfig, - }); - - // Wait for the dialog to close. - const result: AddEditCipherDialogCloseResult = await lastValueFrom(dialogRef.closed); - - // Refresh the vault to show the new cipher. - if (result?.action === AddEditCipherDialogResult.Added) { - this.refresh(); - this.go({ itemId: result.id, action: "view" }); - return; - } - - // If the dialog was closed by any other action navigate back to the vault. - this.go({ cipherId: null, itemId: null, action: null }); + await this.openVaultItemDialog("form", cipherFormConfig); } async editCipher(cipher: CipherView, cloneMode?: boolean) { @@ -675,7 +689,7 @@ export class VaultComponent implements OnInit, OnDestroy { !(await this.passwordRepromptService.showPasswordPrompt()) ) { // didn't pass password prompt, so don't open add / edit modal - this.go({ cipherId: null, itemId: null, action: null }); + await this.go({ cipherId: null, itemId: null, action: null }); return; } @@ -707,14 +721,14 @@ export class VaultComponent implements OnInit, OnDestroy { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises modal.onClosedPromise().then(() => { - this.go({ cipherId: null, itemId: null, action: null }); + void this.go({ cipherId: null, itemId: null, action: null }); }); return childComponent; } /** - * Edit a cipher using the new AddEditCipherDialogV2 component. + * Edit a cipher using the new VaultItemDialog. * * @param cipher * @param cloneMode @@ -726,31 +740,7 @@ export class VaultComponent implements OnInit, OnDestroy { cipher.type, ); - const dialogRef = openAddEditCipherDialog(this.dialogService, { - data: cipherFormConfig, - }); - - const result: AddEditCipherDialogCloseResult = await firstValueFrom(dialogRef.closed); - - /** - * Refresh the vault if the dialog was closed by adding, editing, or deleting a cipher. - */ - if (result?.action === AddEditCipherDialogResult.Edited) { - this.refresh(); - } - - /** - * View the cipher if the dialog was closed by editing the cipher. - */ - if (result?.action === AddEditCipherDialogResult.Edited) { - this.go({ itemId: cipher.id, action: "view" }); - return; - } - - /** - * Navigate to the vault if the dialog was closed by any other action. - */ - this.go({ cipherId: null, itemId: null, action: null }); + await this.openVaultItemDialog("form", cipherFormConfig); } /** @@ -777,39 +767,17 @@ export class VaultComponent implements OnInit, OnDestroy { !(await this.passwordRepromptService.showPasswordPrompt()) ) { // Didn't pass password prompt, so don't open add / edit modal. - this.go({ cipherId: null, itemId: null }); + await this.go({ cipherId: null, itemId: null, action: null }); return; } - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - // Decrypt the cipher. - const cipherView = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + const cipherFormConfig = await this.cipherFormConfigService.buildConfig( + cipher.edit ? "edit" : "partial-edit", + cipher.id as CipherId, + cipher.type, ); - // Open the dialog. - const dialogRef = openViewCipherDialog(this.dialogService, { - data: { cipher: cipherView }, - }); - - // Wait for the dialog to close. - const result: ViewCipherDialogCloseResult = await lastValueFrom(dialogRef.closed); - - // If the dialog was closed by clicking the edit button, navigate to open the edit dialog. - if (result?.action === ViewCipherDialogResult.Edited) { - this.go({ itemId: cipherView.id, action: "edit" }); - return; - } - - // If the dialog was closed by deleting the cipher, refresh the vault. - if (result?.action === ViewCipherDialogResult.Deleted) { - this.refresh(); - } - - // Clear the query params when the view dialog closes - this.go({ cipherId: null, itemId: null, action: null }); + await this.openVaultItemDialog("view", cipherFormConfig); } async addCollection() { @@ -958,7 +926,10 @@ export class VaultComponent implements OnInit, OnDestroy { } const component = await this.editCipher(cipher, true); - component.cloneMode = true; + + if (component != null) { + component.cloneMode = true; + } } async restore(c: CipherView): Promise { @@ -1220,7 +1191,7 @@ export class VaultComponent implements OnInit, OnDestroy { return organization.canEditAllCiphers; } - private go(queryParams: any = null) { + private async go(queryParams: any = null) { if (queryParams == null) { queryParams = { favorites: this.activeFilter.isFavorites || null, @@ -1231,7 +1202,7 @@ export class VaultComponent implements OnInit, OnDestroy { }; } - void this.router.navigate([], { + await this.router.navigate([], { relativeTo: this.route, queryParams: queryParams, queryParamsHandling: "merge", diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts index ead52d805a8..4841a186130 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.ts @@ -1,6 +1,6 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; -import { Component, Inject, OnInit, EventEmitter } from "@angular/core"; +import { Component, EventEmitter, Inject, OnInit } from "@angular/core"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -52,6 +52,7 @@ export interface ViewCipherDialogCloseResult { /** * Component for viewing a cipher, presented in a dialog. + * @deprecated Use the VaultItemDialogComponent instead. */ @Component({ selector: "app-vault-view", diff --git a/libs/vault/src/cipher-form/services/web-cipher-form-generation.service.spec.ts b/apps/web/src/app/vault/services/web-cipher-form-generation.service.spec.ts similarity index 100% rename from libs/vault/src/cipher-form/services/web-cipher-form-generation.service.spec.ts rename to apps/web/src/app/vault/services/web-cipher-form-generation.service.spec.ts diff --git a/libs/vault/src/cipher-form/services/web-cipher-form-generation.service.ts b/apps/web/src/app/vault/services/web-cipher-form-generation.service.ts similarity index 100% rename from libs/vault/src/cipher-form/services/web-cipher-form-generation.service.ts rename to apps/web/src/app/vault/services/web-cipher-form-generation.service.ts diff --git a/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.spec.ts b/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.spec.ts index 6c68dae7077..57d08595e1b 100644 --- a/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.spec.ts +++ b/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.spec.ts @@ -1,15 +1,12 @@ import { DialogRef } from "@angular/cdk/dialog"; import { TestBed } from "@angular/core/testing"; import { Router } from "@angular/router"; -import { of, lastValueFrom } from "rxjs"; +import { lastValueFrom, of } from "rxjs"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { DialogService } from "@bitwarden/components"; -import { - ViewCipherDialogCloseResult, - ViewCipherDialogResult, -} from "../individual-vault/view.component"; +import { VaultItemDialogResult } from "../components/vault-item-dialog/vault-item-dialog.component"; import { WebVaultPremiumUpgradePromptService } from "./web-premium-upgrade-prompt.service"; @@ -17,7 +14,7 @@ describe("WebVaultPremiumUpgradePromptService", () => { let service: WebVaultPremiumUpgradePromptService; let dialogServiceMock: jest.Mocked; let routerMock: jest.Mocked; - let dialogRefMock: jest.Mocked>; + let dialogRefMock: jest.Mocked>; beforeEach(() => { dialogServiceMock = { @@ -30,7 +27,7 @@ describe("WebVaultPremiumUpgradePromptService", () => { dialogRefMock = { close: jest.fn(), - } as unknown as jest.Mocked>; + } as unknown as jest.Mocked>; TestBed.configureTestingModule({ providers: [ @@ -62,9 +59,7 @@ describe("WebVaultPremiumUpgradePromptService", () => { "billing", "subscription", ]); - expect(dialogRefMock.close).toHaveBeenCalledWith({ - action: ViewCipherDialogResult.PremiumUpgrade, - }); + expect(dialogRefMock.close).toHaveBeenCalledWith(VaultItemDialogResult.PremiumUpgrade); }); it("prompts for premium upgrade and navigates to premium subscription if organizationId is not provided", async () => { @@ -79,9 +74,7 @@ describe("WebVaultPremiumUpgradePromptService", () => { type: "success", }); expect(routerMock.navigate).toHaveBeenCalledWith(["settings/subscription/premium"]); - expect(dialogRefMock.close).toHaveBeenCalledWith({ - action: ViewCipherDialogResult.PremiumUpgrade, - }); + expect(dialogRefMock.close).toHaveBeenCalledWith(VaultItemDialogResult.PremiumUpgrade); }); it("does not navigate or close dialog if upgrade is no action is taken", async () => { diff --git a/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.ts b/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.ts index 8f9c8c0bd72..ec15937b05f 100644 --- a/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.ts +++ b/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.ts @@ -6,10 +6,7 @@ import { OrganizationId } from "@bitwarden/common/types/guid"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { DialogService } from "@bitwarden/components"; -import { - ViewCipherDialogCloseResult, - ViewCipherDialogResult, -} from "../individual-vault/view.component"; +import { VaultItemDialogResult } from "../components/vault-item-dialog/vault-item-dialog.component"; /** * This service is used to prompt the user to upgrade to premium. @@ -19,7 +16,7 @@ export class WebVaultPremiumUpgradePromptService implements PremiumUpgradePrompt constructor( private dialogService: DialogService, private router: Router, - private dialog: DialogRef, + private dialog: DialogRef, ) {} /** @@ -51,7 +48,7 @@ export class WebVaultPremiumUpgradePromptService implements PremiumUpgradePrompt } if (upgradeConfirmed) { - this.dialog.close({ action: ViewCipherDialogResult.PremiumUpgrade }); + this.dialog.close(VaultItemDialogResult.PremiumUpgrade); } } } diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.ts b/libs/vault/src/cipher-form/components/cipher-form.component.ts index 2a65218e82d..4df6aa67ea6 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.ts @@ -1,6 +1,7 @@ import { NgIf } from "@angular/common"; import { AfterViewInit, + ChangeDetectorRef, Component, DestroyRef, EventEmitter, @@ -14,6 +15,7 @@ import { } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, FormGroup, ReactiveFormsModule } from "@angular/forms"; +import { Subject } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; @@ -101,6 +103,10 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci */ @Output() cipherSaved = new EventEmitter(); + private formReadySubject = new Subject(); + + @Output() formReady = this.formReadySubject.asObservable(); + /** * The original cipher being edited or cloned. Null for add mode. */ @@ -173,9 +179,13 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci async init() { this.loading = true; + + // Force change detection so that all child components are destroyed and re-created + this.changeDetectorRef.detectChanges(); + this.updatedCipherView = new CipherView(); this.originalCipherView = null; - this.cipherForm.reset(); + this.cipherForm = this.formBuilder.group({}); if (this.config == null) { return; @@ -207,6 +217,7 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci } this.loading = false; + this.formReadySubject.next(); } constructor( @@ -214,6 +225,7 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci private addEditFormService: CipherFormService, private toastService: ToastService, private i18nService: I18nService, + private changeDetectorRef: ChangeDetectorRef, ) {} /** diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index e28f7f2a2bb..c73ad149400 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, Input, OnDestroy, OnInit } from "@angular/core"; +import { Component, Input, OnChanges, OnDestroy } from "@angular/core"; import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -44,7 +44,7 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide AutofillOptionsViewComponent, ], }) -export class CipherViewComponent implements OnInit, OnDestroy { +export class CipherViewComponent implements OnChanges, OnDestroy { @Input({ required: true }) cipher: CipherView; /** @@ -63,7 +63,11 @@ export class CipherViewComponent implements OnInit, OnDestroy { private folderService: FolderService, ) {} - async ngOnInit() { + async ngOnChanges() { + if (this.cipher == null) { + return; + } + await this.loadCipherData(); this.cardIsExpired = isCardExpired(this.cipher.card); From c8d4b819bcdd710886ed13dc147a18da80635a95 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 3 Oct 2024 08:06:41 +1000 Subject: [PATCH 10/15] [PM-11869] Adjust CollectionService to be reactive to keys being available (#11144) --- .../vault/abstractions/collection.service.ts | 11 +- .../vault/services/collection.service.spec.ts | 135 ++++++++++++++++++ .../src/vault/services/collection.service.ts | 53 ++++--- 3 files changed, 176 insertions(+), 23 deletions(-) create mode 100644 libs/common/src/vault/services/collection.service.spec.ts diff --git a/libs/common/src/vault/abstractions/collection.service.ts b/libs/common/src/vault/abstractions/collection.service.ts index 084aa3a8084..81ae76729a2 100644 --- a/libs/common/src/vault/abstractions/collection.service.ts +++ b/libs/common/src/vault/abstractions/collection.service.ts @@ -1,6 +1,7 @@ import { Observable } from "rxjs"; -import { CollectionId, UserId } from "../../types/guid"; +import { CollectionId, OrganizationId, UserId } from "../../types/guid"; +import { OrgKey } from "../../types/key"; import { CollectionData } from "../models/data/collection.data"; import { Collection } from "../models/domain/collection"; import { TreeNode } from "../models/domain/tree-node"; @@ -13,9 +14,13 @@ export abstract class CollectionService { encrypt: (model: CollectionView) => Promise; decryptedCollectionViews$: (ids: CollectionId[]) => Observable; /** - * @deprecated This method will soon be made private, use `decryptedCollectionViews$` instead. + * @deprecated This method will soon be made private + * See PM-12375 */ - decryptMany: (collections: Collection[]) => Promise; + decryptMany: ( + collections: Collection[], + orgKeys?: Record, + ) => Promise; get: (id: string) => Promise; getAll: () => Promise; getAllDecrypted: () => Promise; diff --git a/libs/common/src/vault/services/collection.service.spec.ts b/libs/common/src/vault/services/collection.service.spec.ts new file mode 100644 index 00000000000..e18b53dafcb --- /dev/null +++ b/libs/common/src/vault/services/collection.service.spec.ts @@ -0,0 +1,135 @@ +import { mock } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { + FakeStateProvider, + makeEncString, + makeSymmetricCryptoKey, + mockAccountServiceWith, +} from "../../../spec"; +import { CryptoService } from "../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { I18nService } from "../../platform/abstractions/i18n.service"; +import { Utils } from "../../platform/misc/utils"; +import { EncString } from "../../platform/models/domain/enc-string"; +import { ContainerService } from "../../platform/services/container.service"; +import { CollectionId, OrganizationId, UserId } from "../../types/guid"; +import { OrgKey } from "../../types/key"; +import { CollectionData } from "../models/data/collection.data"; + +import { CollectionService, ENCRYPTED_COLLECTION_DATA_KEY } from "./collection.service"; + +describe("CollectionService", () => { + afterEach(() => { + delete (window as any).bitwardenContainerService; + }); + + describe("decryptedCollections$", () => { + it("emits decrypted collections from state", async () => { + // Arrange test collections + const org1 = Utils.newGuid() as OrganizationId; + const org2 = Utils.newGuid() as OrganizationId; + + const collection1 = collectionDataFactory(org1); + const collection2 = collectionDataFactory(org2); + + // Arrange state provider + const fakeStateProvider = mockStateProvider(); + await fakeStateProvider.setUserState(ENCRYPTED_COLLECTION_DATA_KEY, { + [collection1.id]: collection1, + [collection2.id]: collection2, + }); + + // Arrange cryptoService - orgKeys and mock decryption + const cryptoService = mockCryptoService(); + cryptoService.orgKeys$.mockReturnValue( + of({ + [org1]: makeSymmetricCryptoKey(), + [org2]: makeSymmetricCryptoKey(), + }), + ); + + const collectionService = new CollectionService( + cryptoService, + mock(), + mockI18nService(), + fakeStateProvider, + ); + + const result = await firstValueFrom(collectionService.decryptedCollections$); + expect(result.length).toBe(2); + expect(result[0]).toMatchObject({ + id: collection1.id, + name: "DECRYPTED_STRING", + }); + expect(result[1]).toMatchObject({ + id: collection2.id, + name: "DECRYPTED_STRING", + }); + }); + + it("handles null collection state", async () => { + // Arrange test collections + const org1 = Utils.newGuid() as OrganizationId; + const org2 = Utils.newGuid() as OrganizationId; + + // Arrange state provider + const fakeStateProvider = mockStateProvider(); + await fakeStateProvider.setUserState(ENCRYPTED_COLLECTION_DATA_KEY, null); + + // Arrange cryptoService - orgKeys and mock decryption + const cryptoService = mockCryptoService(); + cryptoService.orgKeys$.mockReturnValue( + of({ + [org1]: makeSymmetricCryptoKey(), + [org2]: makeSymmetricCryptoKey(), + }), + ); + + const collectionService = new CollectionService( + cryptoService, + mock(), + mockI18nService(), + fakeStateProvider, + ); + + const decryptedCollections = await firstValueFrom(collectionService.decryptedCollections$); + expect(decryptedCollections.length).toBe(0); + + const encryptedCollections = await firstValueFrom(collectionService.encryptedCollections$); + expect(encryptedCollections.length).toBe(0); + }); + }); +}); + +const mockI18nService = () => { + const i18nService = mock(); + i18nService.collator = null; // this is a mock only, avoid use of this object + return i18nService; +}; + +const mockStateProvider = () => { + const userId = Utils.newGuid() as UserId; + return new FakeStateProvider(mockAccountServiceWith(userId)); +}; + +const mockCryptoService = () => { + const cryptoService = mock(); + const encryptService = mock(); + encryptService.decryptToUtf8 + .calledWith(expect.any(EncString), expect.anything()) + .mockResolvedValue("DECRYPTED_STRING"); + + (window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService); + + return cryptoService; +}; + +const collectionDataFactory = (orgId: OrganizationId) => { + const collection = new CollectionData({} as any); + collection.id = Utils.newGuid() as CollectionId; + collection.organizationId = orgId; + collection.name = makeEncString("ENC_STRING").encryptedString; + + return collection; +}; diff --git a/libs/common/src/vault/services/collection.service.ts b/libs/common/src/vault/services/collection.service.ts index 09d21390aea..dfa40105c3a 100644 --- a/libs/common/src/vault/services/collection.service.ts +++ b/libs/common/src/vault/services/collection.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom, map, Observable } from "rxjs"; +import { combineLatest, firstValueFrom, map, Observable, of, switchMap } from "rxjs"; import { Jsonify } from "type-fest"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -15,6 +15,7 @@ import { UserKeyDefinition, } from "../../platform/state"; import { CollectionId, OrganizationId, UserId } from "../../types/guid"; +import { OrgKey } from "../../types/key"; import { CollectionService as CollectionServiceAbstraction } from "../../vault/abstractions/collection.service"; import { CollectionData } from "../models/data/collection.data"; import { Collection } from "../models/domain/collection"; @@ -22,7 +23,7 @@ import { TreeNode } from "../models/domain/tree-node"; import { CollectionView } from "../models/view/collection.view"; import { ServiceUtils } from "../service-utils"; -const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record( +export const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record( COLLECTION_DATA, "collections", { @@ -31,19 +32,19 @@ const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record, +const DECRYPTED_COLLECTION_DATA_KEY = new DeriveDefinition< + [Record, Record], CollectionView[], { collectionService: CollectionService } ->(ENCRYPTED_COLLECTION_DATA_KEY, { +>(COLLECTION_DATA, "decryptedCollections", { deserializer: (obj) => obj.map((collection) => CollectionView.fromJSON(collection)), - derive: async (collections: Record, { collectionService }) => { - const data: Collection[] = []; - for (const id in collections ?? {}) { - const collectionId = id as CollectionId; - data.push(new Collection(collections[collectionId])); + derive: async ([collections, orgKeys], { collectionService }) => { + if (collections == null) { + return []; } - return await collectionService.decryptMany(data); + + const data = Object.values(collections).map((c) => new Collection(c)); + return await collectionService.decryptMany(data, orgKeys); }, }); @@ -68,18 +69,25 @@ export class CollectionService implements CollectionServiceAbstraction { protected stateProvider: StateProvider, ) { this.encryptedCollectionDataState = this.stateProvider.getActive(ENCRYPTED_COLLECTION_DATA_KEY); + this.encryptedCollections$ = this.encryptedCollectionDataState.state$.pipe( map((collections) => { - const response: Collection[] = []; - for (const id in collections ?? {}) { - response.push(new Collection(collections[id as CollectionId])); + if (collections == null) { + return []; } - return response; + + return Object.values(collections).map((c) => new Collection(c)); }), ); + const encryptedCollectionsWithKeys = this.encryptedCollectionDataState.combinedState$.pipe( + switchMap(([userId, collectionData]) => + combineLatest([of(collectionData), this.cryptoService.orgKeys$(userId)]), + ), + ); + this.decryptedCollectionDataState = this.stateProvider.getDerived( - this.encryptedCollectionDataState.state$, + encryptedCollectionsWithKeys, DECRYPTED_COLLECTION_DATA_KEY, { collectionService: this }, ); @@ -108,19 +116,24 @@ export class CollectionService implements CollectionServiceAbstraction { return collection; } - async decryptMany(collections: Collection[]): Promise { - if (collections == null) { + // TODO: this should be private and orgKeys should be required. + // See https://bitwarden.atlassian.net/browse/PM-12375 + async decryptMany( + collections: Collection[], + orgKeys?: Record, + ): Promise { + if (collections == null || collections.length === 0) { return []; } const decCollections: CollectionView[] = []; - const organizationKeys = await firstValueFrom(this.cryptoService.activeUserOrgKeys$); + orgKeys ??= await firstValueFrom(this.cryptoService.activeUserOrgKeys$); const promises: Promise[] = []; collections.forEach((collection) => { promises.push( collection - .decrypt(organizationKeys[collection.organizationId as OrganizationId]) + .decrypt(orgKeys[collection.organizationId as OrganizationId]) .then((c) => decCollections.push(c)), ); }); From 891733f4a9e4071bef35e062ebbf02322c4b60ef Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 2 Oct 2024 21:26:07 -0500 Subject: [PATCH 11/15] [PM-12727/12737] Adding copy for Add/Edit (#11369) * add copy for attachments view in the web * add delete attachment copy on the web --- apps/web/src/locales/en/messages.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index c2bda63c277..bad4b54805e 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9265,5 +9265,14 @@ }, "editAccess": { "message": "Edit access" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" } } From d3d1a559728a18323c337ea16edb53a7d7d03ff8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 13:10:48 +0200 Subject: [PATCH 12/15] [deps] Platform: Update big-integer to v1.6.52 (#10563) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 10 +++++----- package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index ac0e171b94f..759fb14ba5a 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -59,7 +59,7 @@ "@koa/multer": "3.0.2", "@koa/router": "12.0.1", "argon2": "0.40.1", - "big-integer": "1.6.51", + "big-integer": "1.6.52", "browser-hrtime": "1.1.8", "chalk": "4.1.2", "commander": "11.1.0", diff --git a/package-lock.json b/package-lock.json index 3a4c9e91842..983d4c57fc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "@ng-select/ng-select": "11.2.0", "argon2": "0.40.1", "argon2-browser": "1.18.0", - "big-integer": "1.6.51", + "big-integer": "1.6.52", "bootstrap": "4.6.0", "braintree-web-drop-in": "1.43.0", "buffer": "6.0.3", @@ -203,7 +203,7 @@ "@koa/multer": "3.0.2", "@koa/router": "12.0.1", "argon2": "0.40.1", - "big-integer": "1.6.51", + "big-integer": "1.6.52", "browser-hrtime": "1.1.8", "chalk": "4.1.2", "commander": "11.1.0", @@ -12852,9 +12852,9 @@ } }, "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", "license": "Unlicense", "engines": { "node": ">=0.6" diff --git a/package.json b/package.json index 115b02eebe2..d16b7d70365 100644 --- a/package.json +++ b/package.json @@ -165,7 +165,7 @@ "@ng-select/ng-select": "11.2.0", "argon2": "0.40.1", "argon2-browser": "1.18.0", - "big-integer": "1.6.51", + "big-integer": "1.6.52", "bootstrap": "4.6.0", "braintree-web-drop-in": "1.43.0", "buffer": "6.0.3", From 29db451808188ad47931cf2947c5b62205d1b965 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 3 Oct 2024 13:11:50 +0200 Subject: [PATCH 13/15] fix: only load vault timeout service in background (#11344) --- .../browser/src/background/main.background.ts | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 62f9bf05dad..fd513f8792d 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -283,7 +283,7 @@ export default class MainBackground { folderService: InternalFolderServiceAbstraction; userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction; collectionService: CollectionServiceAbstraction; - vaultTimeoutService: VaultTimeoutService; + vaultTimeoutService?: VaultTimeoutService; vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction; passwordGenerationService: PasswordGenerationServiceAbstraction; syncService: SyncService; @@ -842,24 +842,26 @@ export default class MainBackground { this.vaultSettingsService = new VaultSettingsService(this.stateProvider); - this.vaultTimeoutService = new VaultTimeoutService( - this.accountService, - this.masterPasswordService, - this.cipherService, - this.folderService, - this.collectionService, - this.platformUtilsService, - this.messagingService, - this.searchService, - this.stateService, - this.authService, - this.vaultTimeoutSettingsService, - this.stateEventRunnerService, - this.taskSchedulerService, - this.logService, - lockedCallback, - logoutCallback, - ); + if (!this.popupOnlyContext) { + this.vaultTimeoutService = new VaultTimeoutService( + this.accountService, + this.masterPasswordService, + this.cipherService, + this.folderService, + this.collectionService, + this.platformUtilsService, + this.messagingService, + this.searchService, + this.stateService, + this.authService, + this.vaultTimeoutSettingsService, + this.stateEventRunnerService, + this.taskSchedulerService, + this.logService, + lockedCallback, + logoutCallback, + ); + } this.containerService = new ContainerService(this.cryptoService, this.encryptService); this.sendStateProvider = new SendStateProvider(this.stateProvider); From 4b200acc3e97e1a63b291b1faad7abaf2007ace6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 13:13:42 +0200 Subject: [PATCH 14/15] [deps]: Update @napi-rs/cli to v2.18.4 (#10584) * [deps]: Update @napi-rs/cli to v2.18.4 * fix: update desktop/src/package-lock.json to reflect sub-package dependencies * feat: apply changes after running napi-rs/cli --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Andreas Coroiu --- apps/desktop/desktop_native/napi/index.d.ts | 10 +++++----- apps/desktop/desktop_native/napi/package.json | 2 +- apps/desktop/src/package-lock.json | 7 +++++++ package-lock.json | 8 ++++---- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index fe4ab59fd8e..903f6ae94af 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -3,7 +3,7 @@ /* auto-generated by NAPI-RS */ -export namespace passwords { +export declare namespace passwords { /** Fetch the stored password from the keychain. */ export function getPassword(service: string, account: string): Promise /** Fetch the stored password from the keychain that was stored with Keytar. */ @@ -14,7 +14,7 @@ export namespace passwords { export function deletePassword(service: string, account: string): Promise export function isAvailable(): Promise } -export namespace biometrics { +export declare namespace biometrics { export function prompt(hwnd: Buffer, message: string): Promise export function available(): Promise export function setBiometricSecret(service: string, account: string, secret: string, keyMaterial: KeyMaterial | undefined | null, ivB64: string): Promise @@ -38,16 +38,16 @@ export namespace biometrics { ivB64: string } } -export namespace clipboards { +export declare namespace clipboards { export function read(): Promise export function write(text: string, password: boolean): Promise } -export namespace processisolations { +export declare namespace processisolations { export function disableCoredumps(): Promise export function isCoreDumpingDisabled(): Promise export function disableMemoryAccess(): Promise } -export namespace powermonitors { +export declare namespace powermonitors { export function onLock(callback: (err: Error | null, ) => any): Promise export function isLockMonitorAvailable(): Promise } diff --git a/apps/desktop/desktop_native/napi/package.json b/apps/desktop/desktop_native/napi/package.json index 9f098c4965d..d557ccfd259 100644 --- a/apps/desktop/desktop_native/napi/package.json +++ b/apps/desktop/desktop_native/napi/package.json @@ -9,7 +9,7 @@ "author": "", "license": "GPL-3.0", "devDependencies": { - "@napi-rs/cli": "2.16.2" + "@napi-rs/cli": "2.18.4" }, "napi": { "name": "desktop_napi", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 3bd771c66af..acd5f97a3f4 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -21,6 +21,13 @@ "@napi-rs/cli": "2.16.2" } }, + "../desktop_native/napi": { + "version": "0.1.0", + "license": "GPL-3.0", + "devDependencies": { + "@napi-rs/cli": "2.18.4" + } + }, "node_modules/@bitwarden/desktop-napi": { "resolved": "../desktop_native/napi", "link": true diff --git a/package-lock.json b/package-lock.json index 983d4c57fc9..371a05dbabe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -242,7 +242,7 @@ "version": "0.1.0", "license": "GPL-3.0", "devDependencies": { - "@napi-rs/cli": "2.16.2" + "@napi-rs/cli": "2.18.4" } }, "apps/web": { @@ -7051,9 +7051,9 @@ } }, "node_modules/@napi-rs/cli": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.2.tgz", - "integrity": "sha512-U2aZfnr0s9KkXpZlYC0l5WxWCXL7vJUNpCnWMwq3T9GG9rhYAAUM9CTZsi1Z+0iR2LcHbfq9EfMgoqnuTyUjfg==", + "version": "2.18.4", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.4.tgz", + "integrity": "sha512-SgJeA4df9DE2iAEpr3M2H0OKl/yjtg1BnRI5/JyowS71tUWhrfSu2LT0V3vlHET+g1hBVlrO60PmEXwUEKp8Mg==", "dev": true, "license": "MIT", "bin": { From 229b712c05999e40b039bca2e4beb86349e80615 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:05:50 +0000 Subject: [PATCH 15/15] Bumped client version(s) (#11380) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- apps/web/package.json | 2 +- package-lock.json | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index fff378c72b9..9d878694594 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.9.2", + "version": "2024.10.0", "scripts": { "build": "cross-env MANIFEST_VERSION=3 webpack", "build:mv2": "webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 35692dd5674..598c6ff0286 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.9.2", + "version": "2024.10.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 7bd40691768..5413ee5b63f 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.9.2", + "version": "2024.10.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/web/package.json b/apps/web/package.json index 520c096a50c..bc7a96e239f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.9.2", + "version": "2024.10.0", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index 371a05dbabe..6498bb54c37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -193,7 +193,7 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.9.2" + "version": "2024.10.0" }, "apps/cli": { "name": "@bitwarden/cli", @@ -247,7 +247,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.9.2" + "version": "2024.10.0" }, "libs/admin-console": { "name": "@bitwarden/admin-console",