1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-05 10:03:21 +00:00
Files
browser/libs/vault/src/services/copy-cipher-field.service.ts
Nick Krantz b4120e0e3f [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
2025-07-17 14:55:32 -05:00

176 lines
5.3 KiB
TypeScript

import { Injectable } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherRepromptType } from "@bitwarden/common/vault/enums";
import {
CipherViewLike,
CipherViewLikeUtils,
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { ToastService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
/**
* The types of fields that can be copied from a cipher.
*/
export type CopyAction =
| "username"
| "password"
| "totp"
| "cardNumber"
| "securityCode"
| "email"
| "phone"
| "address"
| "secureNote"
| "hiddenField"
| "privateKey"
| "publicKey"
| "keyFingerprint";
type CopyActionInfo = {
/**
* The i18n key for the type of field being copied. Will be used to display a toast message.
*/
typeI18nKey: string;
/**
* Whether the field is protected and requires password re-prompting before being copied.
*/
protected: boolean;
/**
* Optional event to collect when the field is copied.
*/
event?: EventType;
};
const CopyActions: Record<CopyAction, CopyActionInfo> = {
username: { typeI18nKey: "username", protected: false },
password: {
typeI18nKey: "password",
protected: true,
event: EventType.Cipher_ClientCopiedPassword,
},
totp: { typeI18nKey: "verificationCodeTotp", protected: true },
cardNumber: { typeI18nKey: "number", protected: true },
securityCode: {
typeI18nKey: "securityCode",
protected: true,
event: EventType.Cipher_ClientCopiedCardCode,
},
email: { typeI18nKey: "email", protected: true },
phone: { typeI18nKey: "phone", protected: true },
address: { typeI18nKey: "address", protected: true },
secureNote: { typeI18nKey: "note", protected: true },
privateKey: { typeI18nKey: "sshPrivateKey", protected: true },
publicKey: { typeI18nKey: "sshPublicKey", protected: true },
keyFingerprint: { typeI18nKey: "sshFingerprint", protected: true },
hiddenField: {
typeI18nKey: "value",
protected: true,
event: EventType.Cipher_ClientCopiedHiddenField,
},
};
@Injectable({
providedIn: "root",
})
export class CopyCipherFieldService {
constructor(
private platformUtilsService: PlatformUtilsService,
private toastService: ToastService,
private eventCollectionService: EventCollectionService,
private passwordRepromptService: PasswordRepromptService,
private totpService: TotpService,
private i18nService: I18nService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private accountService: AccountService,
) {}
/**
* Copy a field value from a cipher to the clipboard.
* @param valueToCopy The value to copy.
* @param actionType The type of field being copied.
* @param cipher The cipher containing the field to copy.
* @param skipReprompt Whether to skip password re-prompting.
*
* @returns Whether the field was copied successfully.
*/
async copy(
valueToCopy: string,
actionType: CopyAction,
cipher: CipherViewLike,
skipReprompt: boolean = false,
): Promise<boolean> {
const action = CopyActions[actionType];
if (
!skipReprompt &&
cipher.reprompt !== CipherRepromptType.None &&
action.protected &&
!(await this.passwordRepromptService.showPasswordPrompt())
) {
return false;
}
if (valueToCopy == null) {
return false;
}
if (actionType === "totp") {
if (!(await this.totpAllowed(cipher))) {
return false;
}
const totpResponse = await firstValueFrom(this.totpService.getCode$(valueToCopy));
if (!totpResponse?.code) {
return false;
}
valueToCopy = totpResponse.code;
}
this.platformUtilsService.copyToClipboard(valueToCopy);
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("valueCopied", this.i18nService.t(action.typeI18nKey)),
title: "",
});
if (action.event !== undefined) {
await this.eventCollectionService.collect(
action.event,
cipher.id,
false,
cipher.organizationId,
);
}
return true;
}
/**
* Determines if TOTP generation is allowed for a cipher and user.
*/
async totpAllowed(cipher: CipherViewLike): Promise<boolean> {
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
if (!activeAccount?.id) {
return false;
}
const login = CipherViewLikeUtils.getLogin(cipher);
return (
!!login?.totp &&
(cipher.organizationUseTotp ||
(await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeAccount.id),
)))
);
}
}