diff --git a/src/app/services/services.module.ts b/src/app/services/services.module.ts index 80fb7041f8..c53e47938c 100644 --- a/src/app/services/services.module.ts +++ b/src/app/services/services.module.ts @@ -107,6 +107,8 @@ function initFactory(i18n: I18nService): Function { { provide: FolderServiceAbstraction, useValue: folderService }, { provide: CollectionServiceAbstraction, useValue: collectionService }, { provide: EnvironmentServiceAbstraction, useValue: environmentService }, + { provide: TotpServiceAbstraction, useValue: totpService }, + { provide: TokenServiceAbstraction, useValue: tokenService }, { provide: I18nService, useValue: i18nService }, { provide: APP_INITIALIZER, diff --git a/src/app/vault/view.component.html b/src/app/vault/view.component.html index 29d73fb4be..4cd532b856 100644 --- a/src/app/vault/view.component.html +++ b/src/app/vault/view.component.html @@ -1,31 +1,98 @@
-
+
{{'itemInformation' | i18n}}
-
+
{{'name' | i18n}} {{cipher.name}}
-
- {{'uri' | i18n}} - {{cipher.login.uri}} +
+ + {{'uri' | i18n}} + {{'website' | i18n}} + {{cipher.login.domainOrUri}}
-
+
+
+ + + +
{{'username' | i18n}} {{cipher.login.username}}
-
+
+ {{'password' | i18n}} - {{cipher.login.password}} + {{cipher.login.maskedPassword}} + {{cipher.login.password}} +
+
+
+ + + +
+ + {{totpSec}} + + + + + + + + {{'verificationCodeTotp' | i18n}} + {{totpCodeFormatted}}
- +
+
+ {{'notes' | i18n}} +
+
+
{{cipher.notes}}
+
+
+
+
+ {{'customFields' | i18n}} +
+
+ todo +
+
+
+
+ {{'attachments' | i18n}} +
+
+ todo
diff --git a/src/app/vault/view.component.ts b/src/app/vault/view.component.ts index 8b12ff3b95..35588f0f9c 100644 --- a/src/app/vault/view.component.ts +++ b/src/app/vault/view.component.ts @@ -5,10 +5,15 @@ import { EventEmitter, Input, OnChanges, + OnDestroy, Output, } from '@angular/core'; +import { CipherType } from 'jslib/enums/cipherType'; + import { CipherService } from 'jslib/abstractions/cipher.service'; +import { TokenService } from 'jslib/abstractions/token.service'; +import { TotpService } from 'jslib/abstractions/totp.service'; import { CipherView } from 'jslib/models/view/cipherView'; @@ -16,20 +21,95 @@ import { CipherView } from 'jslib/models/view/cipherView'; selector: 'app-vault-view', template: template, }) -export class ViewComponent implements OnChanges { +export class ViewComponent implements OnChanges, OnDestroy { @Input() cipherId: string; @Output() onEditCipher = new EventEmitter(); cipher: CipherView; + showPassword: boolean; + isPremium: boolean; + totpCode: string; + totpCodeFormatted: string; + totpDash: number; + totpSec: number; + totpLow: boolean; - constructor(private cipherService: CipherService) { + private totpInterval: NodeJS.Timer; + + constructor(private cipherService: CipherService, private totpService: TotpService, + private tokenService: TokenService) { } async ngOnChanges() { + this.showPassword = false; + const cipher = await this.cipherService.get(this.cipherId); this.cipher = await cipher.decrypt(); + + this.isPremium = this.tokenService.getPremium(); + + if (this.cipher.type == CipherType.Login && this.cipher.login.totp && + (cipher.organizationUseTotp || this.isPremium)) { + await this.totpUpdateCode(); + await this.totpTick(); + + if (this.totpInterval) { + clearInterval(this.totpInterval); + } + + this.totpInterval = setInterval(async () => { + await this.totpTick(); + }, 1000); + } + } + + ngOnDestroy() { + if (this.totpInterval) { + clearInterval(this.totpInterval); + } } edit() { this.onEditCipher.emit(this.cipher.id); } + + togglePassword() { + this.showPassword = !this.showPassword; + } + + launch() { + // TODO + } + + copy(value: string) { + // TODO + } + + private async totpUpdateCode() { + if (this.cipher.type !== CipherType.Login || this.cipher.login.totp == null) { + 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) { + 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 a49f4f6ba3..3a5ebe3b00 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -70,5 +70,26 @@ }, "masterPassword": { "message": "Master Password" + }, + "verificationCodeTotp": { + "message": "Verification Code (TOTP)" + }, + "website": { + "message": "Website" + }, + "notes": { + "message": "Notes" + }, + "customFields": { + "message": "Custom Fields" + }, + "launch": { + "message": "Launch" + }, + "copyValue": { + "message": "Copy Value" + }, + "toggleVisibility": { + "message": "Toggle Visibility" } } diff --git a/src/scss/styles.scss b/src/scss/styles.scss index 0e5b8e3a80..485eb5488e 100644 --- a/src/scss/styles.scss +++ b/src/scss/styles.scss @@ -2,6 +2,7 @@ @import "~font-awesome/scss/font-awesome.scss"; $font-family-sans-serif: 'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif; +$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace; $font-size-base: 14px; $font-size-large: 18px; $font-size-small: 12px; @@ -81,6 +82,10 @@ a { text-decoration: none; } +.monospaced { + font-family: $font-family-monospace; +} + #vault { height: 100vh; display: flex; @@ -359,11 +364,6 @@ a { } } - &.pre { - white-space: pre; - overflow-x: auto; - } - &.text-primary { color: $brand-primary !important; } @@ -568,7 +568,11 @@ a { min-width: 400px; max-width: 550px; width: 100%; - margin: 0 auto; + margin: 30px auto 0 auto; + + &:first-child { + margin-top: 10px; + } .box-header { margin: 0 10px 5px 10px; @@ -579,7 +583,7 @@ a { .box-content { background: $box-background-color; border-radius: $border-radius; - box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2); + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12), 0 1px 5px 0 rgba(0,0,0,0.2); .box-content-row { padding: 10px 15px; @@ -617,6 +621,11 @@ a { background-color: $box-background-hover-color; } + &.pre { + white-space: pre; + overflow-x: auto; + } + .row-label { font-size: $font-size-small; color: $text-muted; @@ -628,6 +637,35 @@ a { input { } + + .action-buttons { + float: right; + + .row-btn { + float: left; + cursor: pointer; + padding: 10px 8px; + background: none; + border: none; + color: $brand-primary; + + &:hover, &:focus { + color: darken($brand-primary, 10%); + } + + &.disabled { + color: $list-icon-color; + + &:hover { + color: $list-icon-color; + } + } + + &:last-child { + padding-right: 2px !important; + } + } + } } } @@ -637,3 +675,58 @@ a { color: $text-muted; } } + +.totp { + .totp-code { + font-family: $font-family-monospace; + font-size: 1.1em; + } + + .totp-countdown { + margin: 3px 3px 0 0; + display: block; + user-select: none; + float: right; + + .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 { + stroke: $brand-primary; + fill: none; + + &.inner { + stroke-width: 3; + stroke-dasharray: 78.6; + stroke-dashoffset: 0; + } + + &.outer { + stroke-width: 2; + stroke-dasharray: 88; + stroke-dashoffset: 0; + } + } + } + + &.low { + .totp-sec, .totp-code { + color: $brand-danger; + } + + .totp-circle { + stroke: $brand-danger; + } + } +} diff --git a/src/services/desktopPlatformUtils.service.ts b/src/services/desktopPlatformUtils.service.ts index 3483db74c7..6894743405 100644 --- a/src/services/desktopPlatformUtils.service.ts +++ b/src/services/desktopPlatformUtils.service.ts @@ -71,7 +71,23 @@ export class DesktopPlatformUtilsService implements PlatformUtilsService { } getDomain(uriString: string): string { - return uriString; + if (uriString == null) { + return null; + } + + uriString = uriString.trim(); + if (uriString === '') { + return null; + } + + if (uriString.indexOf('://') > -1) { + try { + const url = new URL(uriString); + return url.hostname; + } catch (e) { } + } + + return null; } isViewOpen(): boolean {