1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-11249] Sync attachment updates across platforms (#11758)

* update extension refresh form when an attachment is added or removed

- This is needed because the revision date was updated on the server and the locally stored cipher needs to match.

* receive updated cipher from delete attachment endpoint

- deleting an attachment will now alter the revision timestamp on a cipher.

* patch the cipher when an attachment is added or deleted

* migrate vault component to use the `cipherViews$` observable

* reference `cipherViews$` on desktop for vault-items

- This avoid race conditions where ciphers are cleared out in the background. `cipherViews` should always emit the latest views

* return CipherData from cipher service so that consumers have the updated cipher right away

* use the updated cipher from attachment endpoints to refresh the details within the add/edit components on desktop
This commit is contained in:
Nick Krantz
2025-01-28 10:01:23 -06:00
committed by GitHub
parent 70ea75d8f7
commit 7c2bf504a3
10 changed files with 95 additions and 26 deletions

View File

@@ -16,6 +16,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -26,7 +27,7 @@ import { KeyService } from "@bitwarden/key-management";
export class AttachmentsComponent implements OnInit {
@Input() cipherId: string;
@Input() viewOnly: boolean;
@Output() onUploadedAttachment = new EventEmitter();
@Output() onUploadedAttachment = new EventEmitter<CipherView>();
@Output() onDeletedAttachment = new EventEmitter();
@Output() onReuploadedAttachment = new EventEmitter();
@@ -34,7 +35,7 @@ export class AttachmentsComponent implements OnInit {
cipherDomain: Cipher;
canAccessAttachments: boolean;
formPromise: Promise<any>;
deletePromises: { [id: string]: Promise<any> } = {};
deletePromises: { [id: string]: Promise<CipherData> } = {};
reuploadPromises: { [id: string]: Promise<any> } = {};
emergencyAccessId?: string = null;
protected componentName = "";
@@ -96,7 +97,7 @@ export class AttachmentsComponent implements OnInit {
title: null,
message: this.i18nService.t("attachmentSaved"),
});
this.onUploadedAttachment.emit();
this.onUploadedAttachment.emit(this.cipher);
} catch (e) {
this.logService.error(e);
}
@@ -125,7 +126,16 @@ export class AttachmentsComponent implements OnInit {
try {
this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id);
await this.deletePromises[attachment.id];
const updatedCipher = await this.deletePromises[attachment.id];
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const cipher = new Cipher(updatedCipher);
this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
this.toastService.showToast({
variant: "success",
title: null,
@@ -140,7 +150,7 @@ export class AttachmentsComponent implements OnInit {
}
this.deletePromises[attachment.id] = null;
this.onDeletedAttachment.emit();
this.onDeletedAttachment.emit(this.cipher);
}
async download(attachment: AttachmentView) {

View File

@@ -1,7 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { BehaviorSubject, firstValueFrom, from, Subject, switchMap, takeUntil } from "rxjs";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { BehaviorSubject, Subject, firstValueFrom, from, switchMap, takeUntil } from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
@@ -40,7 +41,12 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
constructor(
protected searchService: SearchService,
protected cipherService: CipherService,
) {}
) {
this.cipherService.cipherViews$.pipe(takeUntilDestroyed()).subscribe((ciphers) => {
void this.doSearch(ciphers);
this.loaded = true;
});
}
ngOnInit(): void {
this._searchText$
@@ -117,7 +123,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted;
protected async doSearch(indexedCiphers?: CipherView[]) {
indexedCiphers = indexedCiphers ?? (await this.cipherService.getAllDecrypted());
indexedCiphers = indexedCiphers ?? (await firstValueFrom(this.cipherService.cipherViews$));
const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$);
if (failedCiphers != null && failedCiphers.length > 0) {

View File

@@ -11,7 +11,7 @@ import {
OnInit,
Output,
} from "@angular/core";
import { firstValueFrom, map, Observable } from "rxjs";
import { filter, firstValueFrom, map, Observable } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
@@ -144,11 +144,15 @@ export class ViewComponent implements OnDestroy, OnInit {
async load() {
this.cleanUp();
const cipher = await this.cipherService.get(this.cipherId);
const activeUserId = await firstValueFrom(this.activeUserId$);
this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
// Grab individual cipher from `cipherViews$` for the most up-to-date information
this.cipher = await firstValueFrom(
this.cipherService.cipherViews$.pipe(
map((ciphers) => ciphers.find((c) => c.id === this.cipherId)),
filter((cipher) => !!cipher),
),
);
this.canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
);