mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 09:43:23 +00:00
[PM-22134] Migrate list views to CipherListView from the SDK (#15174)
* add `CipherViewLike` and utilities to handle `CipherView` and `CipherViewLike` * migrate libs needed for web vault to support `CipherViewLike` * migrate web vault components to support * add for CipherView. will have to be later * fetch full CipherView for copying a password * have only the cipher service utilize SDK migration flag - This keeps feature flag logic away from the component - Also cuts down on what is needed for other platforms * strongly type CipherView for AC vault - Probably temporary before migration of the AC vault to `CipherListView` SDK * fix build icon tests by being more gracious with the uri structure * migrate desktop components to CipherListViews$ * consume card from sdk * add browser implementation for `CipherListView` * update copy message for single copiable items * refactor `getCipherViewLikeLogin` to `getLogin` * refactor `getCipherViewLikeCard` to `getCard` * add `hasFido2Credentials` helper * add decryption failure to cipher like utils * add todo with ticket * fix decryption failure typing * fix copy card messages * fix addition of organizations and collections for `PopupCipherViewLike` - accessors were being lost * refactor to getters to fix re-rendering bug * fix decryption failure helper * fix sorting functions for `CipherViewLike` * formatting * add `CipherViewLikeUtils` tests * refactor "copiable" to "copyable" to match SDK * use `hasOldAttachments` from cipherlistview * fix typing * update SDK version * add feature flag for cipher list view work * use `CipherViewLikeUtils` for copyable values rather than referring to the cipher directly * update restricted item type to support CipherViewLike * add cipher support to `CipherViewLikeUtils` * update `isCipherListView` check * refactor CipherLike to a separate type * refactor `getFullCipherView` into the cipher service * add optional chaining for `uriChecksum` * set empty array for decrypted CipherListView * migrate nudge service to use `cipherListViews` * update web vault to not depend on `cipherViews$` * update popup list filters to use `CipherListView` * fix storybook * fix tests * accept undefined as a MY VAULT filter value for cipher list views * use `LoginUriView` for uri logic (#15530) * filter out null ciphers from the `_allDecryptedCiphers$` (#15539) * use `launchUri` to avoid any unexpected behavior in URIs - this appends `http://` when missing
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { BitIconButtonComponent, MenuItemDirective } from "@bitwarden/components";
|
||||
import { CopyCipherFieldService } from "@bitwarden/vault";
|
||||
@@ -9,23 +15,31 @@ describe("CopyCipherFieldDirective", () => {
|
||||
copy: jest.fn().mockResolvedValue(null),
|
||||
totpAllowed: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
let mockAccountService: AccountService;
|
||||
let mockCipherService: CipherService;
|
||||
|
||||
let copyCipherFieldDirective: CopyCipherFieldDirective;
|
||||
|
||||
beforeEach(() => {
|
||||
copyFieldService.copy.mockClear();
|
||||
copyFieldService.totpAllowed.mockClear();
|
||||
mockAccountService = mock<AccountService>();
|
||||
mockAccountService.activeAccount$ = of({ id: "test-account-id" } as Account);
|
||||
mockCipherService = mock<CipherService>();
|
||||
|
||||
copyCipherFieldDirective = new CopyCipherFieldDirective(
|
||||
copyFieldService as unknown as CopyCipherFieldService,
|
||||
mockAccountService,
|
||||
mockCipherService,
|
||||
);
|
||||
copyCipherFieldDirective.cipher = new CipherView();
|
||||
copyCipherFieldDirective.cipher.type = CipherType.Login;
|
||||
});
|
||||
|
||||
describe("disabled state", () => {
|
||||
it("should be enabled when the field is available", async () => {
|
||||
copyCipherFieldDirective.action = "username";
|
||||
copyCipherFieldDirective.cipher.login.username = "test-username";
|
||||
(copyCipherFieldDirective.cipher as CipherView).login.username = "test-username";
|
||||
|
||||
await copyCipherFieldDirective.ngOnChanges();
|
||||
|
||||
@@ -35,6 +49,7 @@ describe("CopyCipherFieldDirective", () => {
|
||||
it("should be disabled when the field is not available", async () => {
|
||||
// create empty cipher
|
||||
copyCipherFieldDirective.cipher = new CipherView();
|
||||
copyCipherFieldDirective.cipher.type = CipherType.Login;
|
||||
|
||||
copyCipherFieldDirective.action = "username";
|
||||
|
||||
@@ -52,11 +67,15 @@ describe("CopyCipherFieldDirective", () => {
|
||||
|
||||
copyCipherFieldDirective = new CopyCipherFieldDirective(
|
||||
copyFieldService as unknown as CopyCipherFieldService,
|
||||
mockAccountService,
|
||||
mockCipherService,
|
||||
undefined,
|
||||
iconButton as unknown as BitIconButtonComponent,
|
||||
);
|
||||
|
||||
copyCipherFieldDirective.action = "password";
|
||||
copyCipherFieldDirective.cipher = new CipherView();
|
||||
copyCipherFieldDirective.cipher.type = CipherType.Login;
|
||||
|
||||
await copyCipherFieldDirective.ngOnChanges();
|
||||
|
||||
@@ -70,6 +89,8 @@ describe("CopyCipherFieldDirective", () => {
|
||||
|
||||
copyCipherFieldDirective = new CopyCipherFieldDirective(
|
||||
copyFieldService as unknown as CopyCipherFieldService,
|
||||
mockAccountService,
|
||||
mockCipherService,
|
||||
menuItemDirective as unknown as MenuItemDirective,
|
||||
);
|
||||
|
||||
@@ -83,9 +104,11 @@ describe("CopyCipherFieldDirective", () => {
|
||||
|
||||
describe("login", () => {
|
||||
beforeEach(() => {
|
||||
copyCipherFieldDirective.cipher.login.username = "test-username";
|
||||
copyCipherFieldDirective.cipher.login.password = "test-password";
|
||||
copyCipherFieldDirective.cipher.login.totp = "test-totp";
|
||||
const cipher = copyCipherFieldDirective.cipher as CipherView;
|
||||
cipher.type = CipherType.Login;
|
||||
cipher.login.username = "test-username";
|
||||
cipher.login.password = "test-password";
|
||||
cipher.login.totp = "test-totp";
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -107,10 +130,12 @@ describe("CopyCipherFieldDirective", () => {
|
||||
|
||||
describe("identity", () => {
|
||||
beforeEach(() => {
|
||||
copyCipherFieldDirective.cipher.identity.username = "test-username";
|
||||
copyCipherFieldDirective.cipher.identity.email = "test-email";
|
||||
copyCipherFieldDirective.cipher.identity.phone = "test-phone";
|
||||
copyCipherFieldDirective.cipher.identity.address1 = "test-address-1";
|
||||
const cipher = copyCipherFieldDirective.cipher as CipherView;
|
||||
cipher.type = CipherType.Identity;
|
||||
cipher.identity.username = "test-username";
|
||||
cipher.identity.email = "test-email";
|
||||
cipher.identity.phone = "test-phone";
|
||||
cipher.identity.address1 = "test-address-1";
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -133,8 +158,10 @@ describe("CopyCipherFieldDirective", () => {
|
||||
|
||||
describe("card", () => {
|
||||
beforeEach(() => {
|
||||
copyCipherFieldDirective.cipher.card.number = "test-card-number";
|
||||
copyCipherFieldDirective.cipher.card.code = "test-card-code";
|
||||
const cipher = copyCipherFieldDirective.cipher as CipherView;
|
||||
cipher.type = CipherType.Card;
|
||||
cipher.card.number = "test-card-number";
|
||||
cipher.card.code = "test-card-code";
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -155,7 +182,9 @@ describe("CopyCipherFieldDirective", () => {
|
||||
|
||||
describe("secure note", () => {
|
||||
beforeEach(() => {
|
||||
copyCipherFieldDirective.cipher.notes = "test-secure-note";
|
||||
const cipher = copyCipherFieldDirective.cipher as CipherView;
|
||||
cipher.type = CipherType.SecureNote;
|
||||
cipher.notes = "test-secure-note";
|
||||
});
|
||||
|
||||
it("copies secure note field to clipboard", async () => {
|
||||
@@ -173,9 +202,11 @@ describe("CopyCipherFieldDirective", () => {
|
||||
|
||||
describe("ssh key", () => {
|
||||
beforeEach(() => {
|
||||
copyCipherFieldDirective.cipher.sshKey.privateKey = "test-private-key";
|
||||
copyCipherFieldDirective.cipher.sshKey.publicKey = "test-public-key";
|
||||
copyCipherFieldDirective.cipher.sshKey.keyFingerprint = "test-key-fingerprint";
|
||||
const cipher = copyCipherFieldDirective.cipher as CipherView;
|
||||
cipher.type = CipherType.SshKey;
|
||||
cipher.sshKey.privateKey = "test-private-key";
|
||||
cipher.sshKey.publicKey = "test-public-key";
|
||||
cipher.sshKey.keyFingerprint = "test-key-fingerprint";
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { Directive, HostBinding, HostListener, Input, OnChanges, Optional } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { MenuItemDirective, BitIconButtonComponent } from "@bitwarden/components";
|
||||
import { CopyAction, CopyCipherFieldService } from "@bitwarden/vault";
|
||||
|
||||
@@ -27,10 +35,12 @@ export class CopyCipherFieldDirective implements OnChanges {
|
||||
})
|
||||
action!: Exclude<CopyAction, "hiddenField">;
|
||||
|
||||
@Input({ required: true }) cipher!: CipherView;
|
||||
@Input({ required: true }) cipher!: CipherViewLike;
|
||||
|
||||
constructor(
|
||||
private copyCipherFieldService: CopyCipherFieldService,
|
||||
private accountService: AccountService,
|
||||
private cipherService: CipherService,
|
||||
@Optional() private menuItemDirective?: MenuItemDirective,
|
||||
@Optional() private iconButtonComponent?: BitIconButtonComponent,
|
||||
) {}
|
||||
@@ -49,7 +59,7 @@ export class CopyCipherFieldDirective implements OnChanges {
|
||||
|
||||
@HostListener("click")
|
||||
async copy() {
|
||||
const value = this.getValueToCopy();
|
||||
const value = await this.getValueToCopy();
|
||||
await this.copyCipherFieldService.copy(value ?? "", this.action, this.cipher);
|
||||
}
|
||||
|
||||
@@ -60,7 +70,7 @@ export class CopyCipherFieldDirective implements OnChanges {
|
||||
private async updateDisabledState() {
|
||||
this.disabled =
|
||||
!this.cipher ||
|
||||
!this.getValueToCopy() ||
|
||||
!this.hasValueToCopy() ||
|
||||
(this.action === "totp" && !(await this.copyCipherFieldService.totpAllowed(this.cipher)))
|
||||
? true
|
||||
: null;
|
||||
@@ -76,32 +86,51 @@ export class CopyCipherFieldDirective implements OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
private getValueToCopy() {
|
||||
/** Returns `true` when the cipher has the associated value as populated. */
|
||||
private hasValueToCopy() {
|
||||
return CipherViewLikeUtils.hasCopyableValue(this.cipher, this.action);
|
||||
}
|
||||
|
||||
/** Returns the value of the cipher to be copied. */
|
||||
private async getValueToCopy() {
|
||||
let _cipher: CipherView;
|
||||
|
||||
if (CipherViewLikeUtils.isCipherListView(this.cipher)) {
|
||||
// When the cipher is of type `CipherListView`, the full cipher needs to be decrypted
|
||||
const activeAccountId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
);
|
||||
const encryptedCipher = await this.cipherService.get(this.cipher.id!, activeAccountId);
|
||||
_cipher = await this.cipherService.decrypt(encryptedCipher, activeAccountId);
|
||||
} else {
|
||||
_cipher = this.cipher;
|
||||
}
|
||||
|
||||
switch (this.action) {
|
||||
case "username":
|
||||
return this.cipher.login?.username || this.cipher.identity?.username;
|
||||
return _cipher.login?.username || _cipher.identity?.username;
|
||||
case "password":
|
||||
return this.cipher.login?.password;
|
||||
return _cipher.login?.password;
|
||||
case "totp":
|
||||
return this.cipher.login?.totp;
|
||||
return _cipher.login?.totp;
|
||||
case "cardNumber":
|
||||
return this.cipher.card?.number;
|
||||
return _cipher.card?.number;
|
||||
case "securityCode":
|
||||
return this.cipher.card?.code;
|
||||
return _cipher.card?.code;
|
||||
case "email":
|
||||
return this.cipher.identity?.email;
|
||||
return _cipher.identity?.email;
|
||||
case "phone":
|
||||
return this.cipher.identity?.phone;
|
||||
return _cipher.identity?.phone;
|
||||
case "address":
|
||||
return this.cipher.identity?.fullAddressForCopy;
|
||||
return _cipher.identity?.fullAddressForCopy;
|
||||
case "secureNote":
|
||||
return this.cipher.notes;
|
||||
return _cipher.notes;
|
||||
case "privateKey":
|
||||
return this.cipher.sshKey?.privateKey;
|
||||
return _cipher.sshKey?.privateKey;
|
||||
case "publicKey":
|
||||
return this.cipher.sshKey?.publicKey;
|
||||
return _cipher.sshKey?.publicKey;
|
||||
case "keyFingerprint":
|
||||
return this.cipher.sshKey?.keyFingerprint;
|
||||
return _cipher.sshKey?.keyFingerprint;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user