1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 21:50:15 +00:00
Files
browser/libs/vault/src/components/copy-cipher-field.directive.ts
2025-10-27 11:13:11 -04:00

148 lines
4.9 KiB
TypeScript

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
* <button appCopyField="username" [cipher]="cipher">Copy Username</button>
* ```
*/
@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<CopyAction, "hiddenField">;
// 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;
}
}
}