[PM-10514] New TOTP Counter Component (#10486)
* new vault totp countdown component
This commit is contained in:
parent
0b24789449
commit
72767dec74
|
@ -9,11 +9,7 @@
|
||||||
</app-item-details-v2>
|
</app-item-details-v2>
|
||||||
|
|
||||||
<!-- LOGIN CREDENTIALS -->
|
<!-- LOGIN CREDENTIALS -->
|
||||||
<app-login-credentials-view
|
<app-login-credentials-view *ngIf="hasLogin" [cipher]="cipher"></app-login-credentials-view>
|
||||||
*ngIf="hasLogin"
|
|
||||||
[login]="cipher.login"
|
|
||||||
[viewPassword]="cipher.viewPassword"
|
|
||||||
></app-login-credentials-view>
|
|
||||||
|
|
||||||
<!-- AUTOFILL OPTIONS -->
|
<!-- AUTOFILL OPTIONS -->
|
||||||
<app-autofill-options-view *ngIf="hasAutofill" [loginUris]="cipher.login.uris">
|
<app-autofill-options-view *ngIf="hasAutofill" [loginUris]="cipher.login.uris">
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<h2 bitTypography="h6">{{ "loginCredentials" | i18n }}</h2>
|
<h2 bitTypography="h6">{{ "loginCredentials" | i18n }}</h2>
|
||||||
</bit-section-header>
|
</bit-section-header>
|
||||||
<bit-card>
|
<bit-card>
|
||||||
<bit-form-field [disableMargin]="!login.password && !login.totp">
|
<bit-form-field [disableMargin]="!cipher.login.password && !cipher.login.totp">
|
||||||
<bit-label>
|
<bit-label>
|
||||||
{{ "username" | i18n }}
|
{{ "username" | i18n }}
|
||||||
</bit-label>
|
</bit-label>
|
||||||
|
@ -11,7 +11,7 @@
|
||||||
readonly
|
readonly
|
||||||
bitInput
|
bitInput
|
||||||
type="text"
|
type="text"
|
||||||
[value]="login.username"
|
[value]="cipher.login.username"
|
||||||
aria-readonly="true"
|
aria-readonly="true"
|
||||||
data-testid="login-username"
|
data-testid="login-username"
|
||||||
/>
|
/>
|
||||||
|
@ -19,20 +19,20 @@
|
||||||
bitIconButton="bwi-clone"
|
bitIconButton="bwi-clone"
|
||||||
bitSuffix
|
bitSuffix
|
||||||
type="button"
|
type="button"
|
||||||
[appCopyClick]="login.username"
|
[appCopyClick]="cipher.login.username"
|
||||||
[valueLabel]="'username' | i18n"
|
[valueLabel]="'username' | i18n"
|
||||||
showToast
|
showToast
|
||||||
[appA11yTitle]="'copyValue' | i18n"
|
[appA11yTitle]="'copyValue' | i18n"
|
||||||
data-testid="toggle-username"
|
data-testid="toggle-username"
|
||||||
></button>
|
></button>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
<bit-form-field [disableMargin]="!login.totp">
|
<bit-form-field [disableMargin]="!cipher.login.totp">
|
||||||
<bit-label>{{ "password" | i18n }}</bit-label>
|
<bit-label>{{ "password" | i18n }}</bit-label>
|
||||||
<input
|
<input
|
||||||
readonly
|
readonly
|
||||||
bitInput
|
bitInput
|
||||||
type="password"
|
type="password"
|
||||||
[value]="login.password"
|
[value]="cipher.login.password"
|
||||||
aria-readonly="true"
|
aria-readonly="true"
|
||||||
data-testid="login-password"
|
data-testid="login-password"
|
||||||
/>
|
/>
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
(toggledChange)="pwToggleValue($event)"
|
(toggledChange)="pwToggleValue($event)"
|
||||||
></button>
|
></button>
|
||||||
<button
|
<button
|
||||||
*ngIf="viewPassword && passwordRevealed"
|
*ngIf="cipher.viewPassword && passwordRevealed"
|
||||||
bitIconButton="bwi-numbered-list"
|
bitIconButton="bwi-numbered-list"
|
||||||
bitSuffix
|
bitSuffix
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
bitIconButton="bwi-clone"
|
bitIconButton="bwi-clone"
|
||||||
bitSuffix
|
bitSuffix
|
||||||
type="button"
|
type="button"
|
||||||
[appCopyClick]="login.password"
|
[appCopyClick]="cipher.login.password"
|
||||||
[valueLabel]="'password' | i18n"
|
[valueLabel]="'password' | i18n"
|
||||||
showToast
|
showToast
|
||||||
[appA11yTitle]="'copyValue' | i18n"
|
[appA11yTitle]="'copyValue' | i18n"
|
||||||
|
@ -66,9 +66,12 @@
|
||||||
></button>
|
></button>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
<ng-container *ngIf="showPasswordCount && passwordRevealed">
|
<ng-container *ngIf="showPasswordCount && passwordRevealed">
|
||||||
<bit-color-password [password]="login.password" [showCount]="true"></bit-color-password>
|
<bit-color-password
|
||||||
|
[password]="cipher.login.password"
|
||||||
|
[showCount]="true"
|
||||||
|
></bit-color-password>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<bit-form-field disableMargin *ngIf="login.totp">
|
<bit-form-field disableMargin *ngIf="cipher.login.totp">
|
||||||
<bit-label
|
<bit-label
|
||||||
>{{ "verificationCodeTotp" | i18n }}
|
>{{ "verificationCodeTotp" | i18n }}
|
||||||
<span
|
<span
|
||||||
|
@ -84,17 +87,25 @@
|
||||||
<input
|
<input
|
||||||
readonly
|
readonly
|
||||||
bitInput
|
bitInput
|
||||||
type="text"
|
[type]="!(isPremium$ | async) ? 'password' : 'text'"
|
||||||
[value]="login.totp"
|
[value]="totpCopyCode || '*** ***'"
|
||||||
aria-readonly="true"
|
aria-readonly="true"
|
||||||
data-testid="login-totp"
|
data-testid="login-totp"
|
||||||
[disabled]="!(isPremium$ | async)"
|
[disabled]="!(isPremium$ | async)"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
*ngIf="isPremium$ | async"
|
||||||
|
bitTotpCountdown
|
||||||
|
[cipher]="cipher"
|
||||||
|
bitSuffix
|
||||||
|
type="button"
|
||||||
|
(sendCopyCode)="setTotpCopyCode($event)"
|
||||||
|
></button>
|
||||||
<button
|
<button
|
||||||
bitIconButton="bwi-clone"
|
bitIconButton="bwi-clone"
|
||||||
bitSuffix
|
bitSuffix
|
||||||
type="button"
|
type="button"
|
||||||
[appCopyClick]="login.totp"
|
[appCopyClick]="totpCopyCode"
|
||||||
[valueLabel]="'verificationCodeTotp' | i18n"
|
[valueLabel]="'verificationCodeTotp' | i18n"
|
||||||
showToast
|
showToast
|
||||||
[appA11yTitle]="'copyValue' | i18n"
|
[appA11yTitle]="'copyValue' | i18n"
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Observable, shareReplay } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import {
|
import {
|
||||||
CardComponent,
|
CardComponent,
|
||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
|
@ -17,6 +17,8 @@ import {
|
||||||
ColorPasswordModule,
|
ColorPasswordModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { BitTotpCountdownComponent } from "../../components/totp-countdown/totp-countdown.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-login-credentials-view",
|
selector: "app-login-credentials-view",
|
||||||
templateUrl: "login-credentials-view.component.html",
|
templateUrl: "login-credentials-view.component.html",
|
||||||
|
@ -32,17 +34,19 @@ import {
|
||||||
IconButtonModule,
|
IconButtonModule,
|
||||||
BadgeModule,
|
BadgeModule,
|
||||||
ColorPasswordModule,
|
ColorPasswordModule,
|
||||||
|
BitTotpCountdownComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class LoginCredentialsViewComponent {
|
export class LoginCredentialsViewComponent {
|
||||||
@Input() login: LoginView;
|
@Input() cipher: CipherView;
|
||||||
@Input() viewPassword: boolean;
|
|
||||||
isPremium$: Observable<boolean> =
|
isPremium$: Observable<boolean> =
|
||||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$.pipe(
|
this.billingAccountProfileStateService.hasPremiumFromAnySource$.pipe(
|
||||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
);
|
);
|
||||||
showPasswordCount: boolean = false;
|
showPasswordCount: boolean = false;
|
||||||
passwordRevealed: boolean = false;
|
passwordRevealed: boolean = false;
|
||||||
|
totpCopyCode: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
|
@ -60,4 +64,8 @@ export class LoginCredentialsViewComponent {
|
||||||
togglePasswordCount() {
|
togglePasswordCount() {
|
||||||
this.showPasswordCount = !this.showPasswordCount;
|
this.showPasswordCount = !this.showPasswordCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTotpCopyCode(e: any) {
|
||||||
|
this.totpCopyCode = e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
<div class="tw-flex tw-items-center tw-justify-center totp-v2">
|
||||||
|
<span class="tw-relative tw-flex tw-justify-center tw-items-center" aria-hidden="true">
|
||||||
|
<span
|
||||||
|
class="tw-absolute"
|
||||||
|
[ngClass]="{ 'tw-text-main': !totpLow, 'tw-text-danger': totpLow }"
|
||||||
|
bitTypography="helper"
|
||||||
|
>{{ totpSec }}</span
|
||||||
|
>
|
||||||
|
<svg class="tw-w-7 tw-h-7" transform="rotate(-90)">
|
||||||
|
<g>
|
||||||
|
<circle
|
||||||
|
class="tw-fill-none"
|
||||||
|
[ngClass]="{ 'tw-stroke-text-main': !totpLow, 'tw-stroke-danger-600': totpLow }"
|
||||||
|
r="9.5"
|
||||||
|
cy="11.8"
|
||||||
|
cx="11.8"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-dasharray="60"
|
||||||
|
[ngStyle]="{ 'stroke-dashoffset.px': totpDash }"
|
||||||
|
></circle>
|
||||||
|
<circle
|
||||||
|
class="tw-fill-none"
|
||||||
|
[ngClass]="{ 'tw-stroke-text-main': !totpLow, 'tw-stroke-danger-600': totpLow }"
|
||||||
|
r="11"
|
||||||
|
cy="11.8"
|
||||||
|
cx="11.8"
|
||||||
|
stroke-width="1"
|
||||||
|
stroke-dasharray="71"
|
||||||
|
stroke-dashoffset="0"
|
||||||
|
></circle>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue