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 { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.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"; /** * Directive to copy a specific field from a cipher on click. Uses the `CopyCipherFieldService` to * handle the copying of the field and any necessary password re-prompting or totp generation. * * Automatically disables the host element if the field to copy is not available or null. * * If the host element is a menu item, it will be hidden when disabled. * * @example * ```html * * ``` */ @Directive({ selector: "[appCopyField]", }) export class CopyCipherFieldDirective implements OnChanges { // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ alias: "appCopyField", required: true, }) action!: Exclude; // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) cipher!: CipherViewLike; constructor( private copyCipherFieldService: CopyCipherFieldService, private accountService: AccountService, private cipherService: CipherService, @Optional() private menuItemDirective?: MenuItemDirective, @Optional() private iconButtonComponent?: BitIconButtonComponent, ) {} @HostBinding("attr.disabled") protected disabled: boolean | null = null; /** * Hide the element if it is disabled and is a menu item. * @private */ @HostBinding("class.tw-hidden") private get hidden() { return this.disabled && this.menuItemDirective; } @HostListener("click") async copy() { const value = await this.getValueToCopy(); await this.copyCipherFieldService.copy(value ?? "", this.action, this.cipher); } async ngOnChanges() { await this.updateDisabledState(); } private async updateDisabledState() { this.disabled = !this.cipher || !this.hasValueToCopy() || (this.action === "totp" && !(await this.copyCipherFieldService.totpAllowed(this.cipher))) ? true : null; // When used on an icon button, update the disabled state of the button component if (this.iconButtonComponent) { this.iconButtonComponent.disabled.set(this.disabled ?? false); } // If the directive is used on a menu item, update the menu item to prevent keyboard navigation if (this.menuItemDirective) { this.menuItemDirective.disabled = this.disabled ?? false; } } /** 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( uuidAsString(this.cipher.id!), activeAccountId, ); _cipher = await this.cipherService.decrypt(encryptedCipher, activeAccountId); } else { _cipher = this.cipher; } switch (this.action) { case "username": return _cipher.login?.username || _cipher.identity?.username; case "password": return _cipher.login?.password; case "totp": return _cipher.login?.totp; case "cardNumber": return _cipher.card?.number; case "securityCode": return _cipher.card?.code; case "email": return _cipher.identity?.email; case "phone": return _cipher.identity?.phone; case "address": return _cipher.identity?.fullAddressForCopy; case "secureNote": return _cipher.notes; case "privateKey": return _cipher.sshKey?.privateKey; case "publicKey": return _cipher.sshKey?.publicKey; case "keyFingerprint": return _cipher.sshKey?.keyFingerprint; default: return null; } } }