diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html index 2a2a4ded05..68c80a7bd5 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.html +++ b/libs/vault/src/cipher-view/cipher-view.component.html @@ -9,11 +9,7 @@ - + diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html index 9ca983a5c9..3cd8431519 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html @@ -3,7 +3,7 @@ {{ "loginCredentials" | i18n }} - + {{ "username" | i18n }} @@ -11,7 +11,7 @@ readonly bitInput type="text" - [value]="login.username" + [value]="cipher.login.username" aria-readonly="true" data-testid="login-username" /> @@ -19,20 +19,20 @@ bitIconButton="bwi-clone" bitSuffix type="button" - [appCopyClick]="login.username" + [appCopyClick]="cipher.login.username" [valueLabel]="'username' | i18n" showToast [appA11yTitle]="'copyValue' | i18n" data-testid="toggle-username" > - + {{ "password" | i18n }} @@ -45,7 +45,7 @@ (toggledChange)="pwToggleValue($event)" > - + - + {{ "verificationCodeTotp" | i18n }} + = this.billingAccountProfileStateService.hasPremiumFromAnySource$.pipe( shareReplay({ refCount: true, bufferSize: 1 }), ); showPasswordCount: boolean = false; passwordRevealed: boolean = false; + totpCopyCode: string; constructor( private billingAccountProfileStateService: BillingAccountProfileStateService, @@ -60,4 +64,8 @@ export class LoginCredentialsViewComponent { togglePasswordCount() { this.showPasswordCount = !this.showPasswordCount; } + + setTotpCopyCode(e: any) { + this.totpCopyCode = e; + } } diff --git a/libs/vault/src/components/totp-countdown/totp-countdown.component.html b/libs/vault/src/components/totp-countdown/totp-countdown.component.html new file mode 100644 index 0000000000..ba4ff04a09 --- /dev/null +++ b/libs/vault/src/components/totp-countdown/totp-countdown.component.html @@ -0,0 +1,34 @@ + + + {{ totpSec }} + + + + + + + + diff --git a/libs/vault/src/components/totp-countdown/totp-countdown.component.ts b/libs/vault/src/components/totp-countdown/totp-countdown.component.ts new file mode 100644 index 0000000000..aeb7d30654 --- /dev/null +++ b/libs/vault/src/components/totp-countdown/totp-countdown.component.ts @@ -0,0 +1,80 @@ +import { CommonModule } from "@angular/common"; +import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; + +import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { TypographyModule } from "@bitwarden/components"; + +@Component({ + selector: "button[bitTotpCountdown]:not(button[bitButton])", + templateUrl: "totp-countdown.component.html", + standalone: true, + imports: [CommonModule, TypographyModule], +}) +export class BitTotpCountdownComponent implements OnInit { + @Input() cipher: CipherView; + @Output() sendCopyCode = new EventEmitter(); + + totpCode: string; + totpCodeFormatted: string; + totpDash: number; + totpSec: number; + totpLow: boolean; + private totpInterval: any; + + constructor(protected totpService: TotpService) {} + + async ngOnInit() { + await this.totpUpdateCode(); + const interval = this.totpService.getTimeInterval(this.cipher.login.totp); + await this.totpTick(interval); + + this.totpInterval = setInterval(async () => { + await this.totpTick(interval); + }, 1000); + } + + private async totpUpdateCode() { + if (this.cipher.login.totp == null) { + this.clearTotp(); + return; + } + + this.totpCode = await this.totpService.getCode(this.cipher.login.totp); + if (this.totpCode != null) { + if (this.totpCode.length > 4) { + this.totpCodeFormatted = this.formatTotpCode(); + this.sendCopyCode.emit(this.totpCodeFormatted); + } else { + this.totpCodeFormatted = this.totpCode; + } + } else { + this.totpCodeFormatted = null; + this.sendCopyCode.emit(this.totpCodeFormatted); + this.clearTotp(); + } + } + + private async totpTick(intervalSeconds: number) { + const epoch = Math.round(new Date().getTime() / 1000.0); + const mod = epoch % intervalSeconds; + + this.totpSec = intervalSeconds - mod; + this.totpDash = +(Math.round(((60 / intervalSeconds) * mod + "e+2") as any) + "e-2"); + this.totpLow = this.totpSec <= 7; + if (mod === 0) { + await this.totpUpdateCode(); + } + } + + private formatTotpCode(): string { + const half = Math.floor(this.totpCode.length / 2); + return this.totpCode.substring(0, half) + " " + this.totpCode.substring(half); + } + + private clearTotp() { + if (this.totpInterval) { + clearInterval(this.totpInterval); + } + } +}