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 @@
+
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;
+ }
+ }
+}