diff --git a/jslib b/jslib index e5db01083c..d75543e6c8 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit e5db01083cc13df3696bb30562a83d729280ac03 +Subproject commit d75543e6c88b220d8e3c7d66fb768d39f9480587 diff --git a/src/app/vault/add-edit.component.html b/src/app/vault/add-edit.component.html index 9b468198ab..138fec18b8 100644 --- a/src/app/vault/add-edit.component.html +++ b/src/app/vault/add-edit.component.html @@ -78,6 +78,26 @@ +
+
+ +
+
+ + {{totpSec}} + + + + + + + + {{totpCodeFormatted}} + +
+
diff --git a/src/app/vault/add-edit.component.ts b/src/app/vault/add-edit.component.ts index 60ef944ef0..b1b96ae415 100644 --- a/src/app/vault/add-edit.component.ts +++ b/src/app/vault/add-edit.component.ts @@ -6,12 +6,16 @@ import { import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; +import { CipherType } from 'jslib/enums/cipherType'; + import { AuditService } from 'jslib/abstractions/audit.service'; import { CipherService } from 'jslib/abstractions/cipher.service'; import { FolderService } from 'jslib/abstractions/folder.service'; import { I18nService } from 'jslib/abstractions/i18n.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { StateService } from 'jslib/abstractions/state.service'; +import { TokenService } from 'jslib/abstractions/token.service'; +import { TotpService } from 'jslib/abstractions/totp.service'; import { AddEditComponent as BaseAddEditComponent } from 'jslib/angular/components/add-edit.component'; import { LoginUriView } from 'jslib/models/view/loginUriView'; @@ -21,16 +25,38 @@ import { LoginUriView } from 'jslib/models/view/loginUriView'; templateUrl: 'add-edit.component.html', }) export class AddEditComponent extends BaseAddEditComponent implements OnInit { + isPremium: boolean; + totpCode: string; + totpCodeFormatted: string; + totpDash: number; + totpSec: number; + totpLow: boolean; + + private totpInterval: number; + constructor(cipherService: CipherService, folderService: FolderService, i18nService: I18nService, platformUtilsService: PlatformUtilsService, analytics: Angulartics2, toasterService: ToasterService, - auditService: AuditService, stateService: StateService) { + auditService: AuditService, stateService: StateService, + private tokenService: TokenService, private totpService: TotpService) { super(cipherService, folderService, i18nService, platformUtilsService, analytics, toasterService, auditService, stateService); } async ngOnInit() { await super.load(); + this.cleanUp(); + + this.isPremium = this.tokenService.getPremium(); + if (this.cipher.type === CipherType.Login && this.cipher.login.totp && + (this.cipher.organizationUseTotp || this.isPremium)) { + await this.totpUpdateCode(); + await this.totpTick(); + + this.totpInterval = window.setInterval(async () => { + await this.totpTick(); + }, 1000); + } } toggleFavorite() { @@ -56,4 +82,41 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit { this.toasterService.popAsync('info', null, this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey))); } + + private cleanUp() { + if (this.totpInterval) { + window.clearInterval(this.totpInterval); + } + } + + private async totpUpdateCode() { + if (this.cipher == null || this.cipher.type !== CipherType.Login || this.cipher.login.totp == null) { + if (this.totpInterval) { + window.clearInterval(this.totpInterval); + } + return; + } + + this.totpCode = await this.totpService.getCode(this.cipher.login.totp); + if (this.totpCode != null) { + this.totpCodeFormatted = this.totpCode.substring(0, 3) + ' ' + this.totpCode.substring(3); + } else { + this.totpCodeFormatted = null; + if (this.totpInterval) { + window.clearInterval(this.totpInterval); + } + } + } + + private async totpTick() { + const epoch = Math.round(new Date().getTime() / 1000.0); + const mod = epoch % 30; + + this.totpSec = 30 - mod; + this.totpDash = +(Math.round(((2.62 * mod) + 'e+2') as any) + 'e-2'); + this.totpLow = this.totpSec <= 7; + if (mod === 0) { + await this.totpUpdateCode(); + } + } } diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 9a8671b2a7..977394c6c2 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -711,5 +711,11 @@ "example": "2" } } + }, + "verificationCodeTotp": { + "message": "Verification Code (TOTP)" + }, + "copyVerificationCode": { + "message": "Copy Verification Code" } } diff --git a/src/scss/styles.scss b/src/scss/styles.scss index cce8effb59..fc439ed3f7 100644 --- a/src/scss/styles.scss +++ b/src/scss/styles.scss @@ -306,3 +306,61 @@ app-login { border: none; } } + +.totp { + .totp-code { + @extend .text-monospace; + font-size: 1.2rem; + } + + .totp-countdown { + margin: 3px 3px 0 0; + display: block; + user-select: none; + + .totp-sec { + font-size: 0.85em; + position: absolute; + line-height: 32px; + width: 32px; + text-align: center; + } + + svg { + width: 32px; + height: 32px; + transform: rotate(-90deg); + } + + .totp-circle { + fill: none; + stroke: $primary; + + &.inner { + stroke-width: 3; + stroke-dasharray: 78.6; + stroke-dashoffset: 0; + } + + &.outer { + stroke-width: 2; + stroke-dasharray: 88; + stroke-dashoffset: 0; + } + } + } + + > .align-items-center { + margin-bottom: -5px; + } + + &.low { + .totp-sec, .totp-code { + color: $danger; + } + + .totp-circle { + stroke: $danger; + } + } +}