From 8f9cc2966107266f2c6bf47b710242a6fc21f022 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 25 Jan 2018 14:25:44 -0500 Subject: [PATCH] finish implementing view page --- src/app/pipes/i18n.pipe.ts | 2 +- src/app/services/services.module.ts | 10 +- src/app/vault/view.component.html | 130 ++++++++++++++++++- src/app/vault/view.component.ts | 61 ++++++++- src/locales/en/messages.json | 51 ++++++++ src/main.ts | 6 +- src/scss/styles.scss | 26 ++++ src/services/desktopPlatformUtils.service.ts | 29 ++++- src/services/i18n.service.ts | 4 +- 9 files changed, 303 insertions(+), 16 deletions(-) diff --git a/src/app/pipes/i18n.pipe.ts b/src/app/pipes/i18n.pipe.ts index 0e0c3a1a1f..2a6cef7cc0 100644 --- a/src/app/pipes/i18n.pipe.ts +++ b/src/app/pipes/i18n.pipe.ts @@ -3,7 +3,7 @@ import { PipeTransform, } from '@angular/core'; -import { I18nService } from '../../services/i18n.service'; +import { I18nService } from 'jslib/abstractions/i18n.service'; @Pipe({ name: 'i18n', diff --git a/src/app/services/services.module.ts b/src/app/services/services.module.ts index c53e47938c..02312c4476 100644 --- a/src/app/services/services.module.ts +++ b/src/app/services/services.module.ts @@ -41,6 +41,7 @@ import { CryptoService as CryptoServiceAbstraction, EnvironmentService as EnvironmentServiceAbstraction, FolderService as FolderServiceAbstraction, + I18nService as I18nServiceAbstraction, LockService as LockServiceAbstraction, MessagingService as MessagingServiceAbstraction, PasswordGenerationService as PasswordGenerationServiceAbstraction, @@ -58,7 +59,7 @@ webFrame.registerURLSchemeAsPrivileged('file'); const i18nService = new I18nService(window.navigator.language, './locales'); const utilsService = new UtilsService(); -const platformUtilsService = new DesktopPlatformUtilsService(); +const platformUtilsService = new DesktopPlatformUtilsService(i18nService); const messagingService = new DesktopMessagingService(); const storageService: StorageServiceAbstraction = new DesktopStorageService(); const secureStorageService: StorageServiceAbstraction = new DesktopSecureStorageService(); @@ -109,11 +110,14 @@ function initFactory(i18n: I18nService): Function { { provide: EnvironmentServiceAbstraction, useValue: environmentService }, { provide: TotpServiceAbstraction, useValue: totpService }, { provide: TokenServiceAbstraction, useValue: tokenService }, - { provide: I18nService, useValue: i18nService }, + { provide: I18nServiceAbstraction, useValue: i18nService }, + { provide: UtilsServiceAbstraction, useValue: utilsService }, + { provide: CryptoServiceAbstraction, useValue: cryptoService }, + { provide: PlatformUtilsServiceAbstraction, useValue: platformUtilsService }, { provide: APP_INITIALIZER, useFactory: initFactory, - deps: [I18nService], + deps: [I18nServiceAbstraction], multi: true, }, ], diff --git a/src/app/vault/view.component.html b/src/app/vault/view.component.html index 4cd532b856..fc85682f86 100644 --- a/src/app/vault/view.component.html +++ b/src/app/vault/view.component.html @@ -9,6 +9,7 @@ {{'name' | i18n}} {{cipher.name}} +
@@ -16,7 +17,8 @@ *ngIf="cipher.login.canLaunch" (click)="launch()"> - +
@@ -26,7 +28,8 @@
@@ -40,7 +43,8 @@ - +
@@ -51,7 +55,8 @@
@@ -69,6 +74,89 @@ {{totpCodeFormatted}}
+ +
+
+ {{'cardholderName' | i18n}} + {{cipher.card.cardholderName}} +
+
+
+ + + +
+ {{'number' | i18n}} + {{cipher.card.number}} +
+
+ {{'brand' | i18n}} + {{cipher.card.brand}} +
+
+ {{'expiration' | i18n}} + {{cipher.card.expiration}} +
+
+
+ + + +
+ {{'securityCode' | i18n}} + {{cipher.card.code}} +
+
+ +
+
+ {{'identityName' | i18n}} + {{cipher.identity.fullName}} +
+
+ {{'username' | i18n}} + {{cipher.identity.username}} +
+
+ {{'company' | i18n}} + {{cipher.identity.company}} +
+
+ {{'ssn' | i18n}} + {{cipher.identity.ssn}} +
+
+ {{'passportNumber' | i18n}} + {{cipher.identity.passportNumber}} +
+
+ {{'licenseNumber' | i18n}} + {{cipher.identity.licenseNumber}} +
+
+ {{'email' | i18n}} + {{cipher.identity.email}} +
+
+ {{'phone' | i18n}} + {{cipher.identity.phone}} +
+
+ {{'address' | i18n}} +
{{cipher.identity.address1}}
+
{{cipher.identity.address2}}
+
{{cipher.identity.address3}}
+
+ {{cipher.identity.city || '-'}}, + {{cipher.identity.state || '-'}}, + {{cipher.identity.postalCode || '-'}} +
+
{{cipher.identity.country}}
+
+
@@ -84,7 +172,31 @@ {{'customFields' | i18n}}
- todo +
+ + {{field.name}} +
+ {{field.value || ' '}} +
+
+ {{field.value}} + {{field.maskedValue}} +
+
+ + +
+
@@ -92,7 +204,13 @@ {{'attachments' | i18n}}
- todo + + + + {{attachment.sizeName}} + {{attachment.fileName}} +
diff --git a/src/app/vault/view.component.ts b/src/app/vault/view.component.ts index e2b0d722c3..3a681b2ef3 100644 --- a/src/app/vault/view.component.ts +++ b/src/app/vault/view.component.ts @@ -10,12 +10,19 @@ import { } from '@angular/core'; import { CipherType } from 'jslib/enums/cipherType'; +import { FieldType } from 'jslib/enums/fieldType'; import { CipherService } from 'jslib/abstractions/cipher.service'; +import { CryptoService } from 'jslib/abstractions/crypto.service'; +import { I18nService } from 'jslib/abstractions/i18n.service'; +import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { TokenService } from 'jslib/abstractions/token.service'; import { TotpService } from 'jslib/abstractions/totp.service'; +import { UtilsService } from 'jslib/abstractions/utils.service'; +import { AttachmentView } from 'jslib/models/view/attachmentView'; import { CipherView } from 'jslib/models/view/cipherView'; +import { FieldView } from 'jslib/models/view/fieldView'; @Component({ selector: 'app-vault-view', @@ -32,11 +39,14 @@ export class ViewComponent implements OnChanges, OnDestroy { totpDash: number; totpSec: number; totpLow: boolean; + fieldType = FieldType; private totpInterval: NodeJS.Timer; constructor(private cipherService: CipherService, private totpService: TotpService, - private tokenService: TokenService) { + private tokenService: TokenService, private utilsService: UtilsService, + private cryptoService: CryptoService, private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService) { } async ngOnChanges() { @@ -70,12 +80,57 @@ export class ViewComponent implements OnChanges, OnDestroy { this.showPassword = !this.showPassword; } + toggleFieldValue(field: FieldView) { + const f = (field as any); + f.showValue = !f.showValue; + } + launch() { - // TODO + if (this.cipher.login.uri == null || this.cipher.login.uri.indexOf('://') === -1) { + return; + } + + this.platformUtilsService.launchUri(this.cipher.login.uri); } copy(value: string) { - // TODO + if (value == null) { + return; + } + + this.utilsService.copyToClipboard(value, window.document); + } + + async downloadAttachment(attachment: AttachmentView) { + const a = (attachment as any); + if (a.downloading) { + return; + } + + if (!this.cipher.organizationId && !this.isPremium) { + this.platformUtilsService.alertError(this.i18nService.t('premiumRequired'), + this.i18nService.t('premiumRequiredDesc')); + return; + } + + a.downloading = true; + const response = await fetch(new Request(attachment.url, { cache: 'no-cache' })); + if (response.status !== 200) { + this.platformUtilsService.alertError(null, this.i18nService.t('errorOccurred')); + a.downloading = false; + return; + } + + try { + const buf = await response.arrayBuffer(); + const key = await this.cryptoService.getOrgKey(this.cipher.organizationId); + const decBuf = await this.cryptoService.decryptFromBytes(buf, key); + this.platformUtilsService.saveFile(window, decBuf, null, attachment.fileName); + } catch (e) { + this.platformUtilsService.alertError(null, this.i18nService.t('errorOccurred')); + } + + a.downloading = false; } private cleanUp() { diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 3a5ebe3b00..620e47640e 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -91,5 +91,56 @@ }, "toggleVisibility": { "message": "Toggle Visibility" + }, + "cardholderName": { + "message": "Cardholder Name" + }, + "number": { + "message": "Number" + }, + "brand": { + "message": "Brand" + }, + "expiration": { + "message": "Expiration" + }, + "securityCode": { + "message": "Security Code" + }, + "identityName": { + "message": "identityName" + }, + "company": { + "message": "Company" + }, + "ssn": { + "message": "Social Security Number" + }, + "passportNumber": { + "message": "Passport Number" + }, + "licenseNumber": { + "message": "License Number" + }, + "email": { + "message": "Email" + }, + "phone": { + "message": "Phone" + }, + "address": { + "message": "Address" + }, + "premiumRequired": { + "message": "Premium Required" + }, + "premiumRequiredDesc": { + "message": "A premium membership is required to use this feature." + }, + "errorOccurred": { + "message": "An error has occurred." + }, + "error": { + "message": "Error" } } diff --git a/src/main.ts b/src/main.ts index b530aa71bd..23226659dc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, ipcMain, screen } from 'electron'; +import { app, BrowserWindow, dialog, ipcMain, screen } from 'electron'; import * as path from 'path'; import * as url from 'url'; /* @@ -26,6 +26,10 @@ ipcMain.on('keytar', async (event: any, message: any) => { }); */ +ipcMain.on('showError', async (event: any, message: any) => { + dialog.showErrorBox(message.title, message.message); +}); + import { I18nService } from './services/i18n.service'; const i18nService = new I18nService('en', './locales/'); i18nService.init().then(() => { }); diff --git a/src/scss/styles.scss b/src/scss/styles.scss index 485eb5488e..2b93eab7b4 100644 --- a/src/scss/styles.scss +++ b/src/scss/styles.scss @@ -589,6 +589,9 @@ a { padding: 10px 15px; position: relative; z-index: 1; + display: block; + color: $text-color; + overflow-wrap: break-word; &:before { content: ""; @@ -626,6 +629,12 @@ a { overflow-x: auto; } + .no-wrap { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .row-label { font-size: $font-size-small; color: $text-muted; @@ -666,6 +675,23 @@ a { } } } + + .right-icon { + float: right; + margin-top: 4px; + color: $list-icon-color; + } + + .row-sub-label { + float: right; + display: block; + margin-right: 15px; + color: $gray-light; + } + + small.row-sub-label { + margin-top: 2px; + } } } diff --git a/src/services/desktopPlatformUtils.service.ts b/src/services/desktopPlatformUtils.service.ts index 6894743405..112fcddab7 100644 --- a/src/services/desktopPlatformUtils.service.ts +++ b/src/services/desktopPlatformUtils.service.ts @@ -1,6 +1,9 @@ +import { ipcRenderer, shell } from 'electron'; + import { DeviceType } from 'jslib/enums'; -import { PlatformUtilsService } from 'jslib/abstractions'; +import { I18nService } from 'jslib/abstractions/i18n.service'; +import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; const AnalyticsIds = { [DeviceType.Windows]: 'UA-81915606-17', @@ -12,6 +15,9 @@ export class DesktopPlatformUtilsService implements PlatformUtilsService { private deviceCache: DeviceType = null; private analyticsIdCache: string = null; + constructor(private i18nService: I18nService) { + } + getDevice(): DeviceType { if (!this.deviceCache) { switch (process.platform) { @@ -93,4 +99,25 @@ export class DesktopPlatformUtilsService implements PlatformUtilsService { isViewOpen(): boolean { return true; } + + launchUri(uri: string): void { + shell.openExternal(uri); + } + + saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void { + const blob = new Blob([blobData], blobOptions); + const a = win.document.createElement('a'); + a.href = win.URL.createObjectURL(blob); + a.download = fileName; + window.document.body.appendChild(a); + a.click(); + window.document.body.removeChild(a); + } + + alertError(title: string, message: string): void { + ipcRenderer.send('showError', { + title: title || this.i18nService.t('error'), + message: message, + }); + } } diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts index 621abad8b7..97fcd2521d 100644 --- a/src/services/i18n.service.ts +++ b/src/services/i18n.service.ts @@ -1,11 +1,13 @@ import * as path from 'path'; +import { I18nService as I18nServiceAbstraction } from 'jslib/abstractions/i18n.service'; + // First locale is the default (English) const SupportedLocales = [ 'en', 'es', ]; -export class I18nService { +export class I18nService implements I18nServiceAbstraction { defaultMessages: any = {}; localeMessages: any = {}; language: string;