finish implementing view page

This commit is contained in:
Kyle Spearrin 2018-01-25 14:25:44 -05:00
parent 15868ab541
commit 8f9cc29661
9 changed files with 303 additions and 16 deletions

View File

@ -3,7 +3,7 @@ import {
PipeTransform, PipeTransform,
} from '@angular/core'; } from '@angular/core';
import { I18nService } from '../../services/i18n.service'; import { I18nService } from 'jslib/abstractions/i18n.service';
@Pipe({ @Pipe({
name: 'i18n', name: 'i18n',

View File

@ -41,6 +41,7 @@ import {
CryptoService as CryptoServiceAbstraction, CryptoService as CryptoServiceAbstraction,
EnvironmentService as EnvironmentServiceAbstraction, EnvironmentService as EnvironmentServiceAbstraction,
FolderService as FolderServiceAbstraction, FolderService as FolderServiceAbstraction,
I18nService as I18nServiceAbstraction,
LockService as LockServiceAbstraction, LockService as LockServiceAbstraction,
MessagingService as MessagingServiceAbstraction, MessagingService as MessagingServiceAbstraction,
PasswordGenerationService as PasswordGenerationServiceAbstraction, PasswordGenerationService as PasswordGenerationServiceAbstraction,
@ -58,7 +59,7 @@ webFrame.registerURLSchemeAsPrivileged('file');
const i18nService = new I18nService(window.navigator.language, './locales'); const i18nService = new I18nService(window.navigator.language, './locales');
const utilsService = new UtilsService(); const utilsService = new UtilsService();
const platformUtilsService = new DesktopPlatformUtilsService(); const platformUtilsService = new DesktopPlatformUtilsService(i18nService);
const messagingService = new DesktopMessagingService(); const messagingService = new DesktopMessagingService();
const storageService: StorageServiceAbstraction = new DesktopStorageService(); const storageService: StorageServiceAbstraction = new DesktopStorageService();
const secureStorageService: StorageServiceAbstraction = new DesktopSecureStorageService(); const secureStorageService: StorageServiceAbstraction = new DesktopSecureStorageService();
@ -109,11 +110,14 @@ function initFactory(i18n: I18nService): Function {
{ provide: EnvironmentServiceAbstraction, useValue: environmentService }, { provide: EnvironmentServiceAbstraction, useValue: environmentService },
{ provide: TotpServiceAbstraction, useValue: totpService }, { provide: TotpServiceAbstraction, useValue: totpService },
{ provide: TokenServiceAbstraction, useValue: tokenService }, { 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, provide: APP_INITIALIZER,
useFactory: initFactory, useFactory: initFactory,
deps: [I18nService], deps: [I18nServiceAbstraction],
multi: true, multi: true,
}, },
], ],

View File

@ -9,6 +9,7 @@
<span class="row-label">{{'name' | i18n}}</span> <span class="row-label">{{'name' | i18n}}</span>
{{cipher.name}} {{cipher.name}}
</div> </div>
<!-- Login -->
<div *ngIf="cipher.login"> <div *ngIf="cipher.login">
<div class="box-content-row" *ngIf="cipher.login.uri"> <div class="box-content-row" *ngIf="cipher.login.uri">
<div class="action-buttons"> <div class="action-buttons">
@ -16,7 +17,8 @@
*ngIf="cipher.login.canLaunch" (click)="launch()"> *ngIf="cipher.login.canLaunch" (click)="launch()">
<i class="fa fa-lg fa-share-square-o"></i> <i class="fa fa-lg fa-share-square-o"></i>
</a> </a>
<a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}"> <a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}"
(click)="copy(cipher.login.uri)">
<i class="fa fa-lg fa-clipboard"></i> <i class="fa fa-lg fa-clipboard"></i>
</a> </a>
</div> </div>
@ -26,7 +28,8 @@
</div> </div>
<div class="box-content-row" *ngIf="cipher.login.username"> <div class="box-content-row" *ngIf="cipher.login.username">
<div class="action-buttons"> <div class="action-buttons">
<a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}"> <a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}"
(click)="copy(cipher.login.username)">
<i class="fa fa-lg fa-clipboard"></i> <i class="fa fa-lg fa-clipboard"></i>
</a> </a>
</div> </div>
@ -40,7 +43,8 @@
<i class="fa fa-lg" <i class="fa fa-lg"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i> [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</a> </a>
<a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}"> <a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}"
(click)="copy(cipher.login.password)">
<i class="fa fa-lg fa-clipboard"></i> <i class="fa fa-lg fa-clipboard"></i>
</a> </a>
</div> </div>
@ -51,7 +55,8 @@
<div class="box-content-row totp" [ngClass]="{'low': totpLow}" <div class="box-content-row totp" [ngClass]="{'low': totpLow}"
*ngIf="cipher.login.totp && totpCode"> *ngIf="cipher.login.totp && totpCode">
<div class="action-buttons"> <div class="action-buttons">
<a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}"> <a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}"
(click)="copy(totpCode)">
<i class="fa fa-lg fa-clipboard"></i> <i class="fa fa-lg fa-clipboard"></i>
</a> </a>
</div> </div>
@ -69,6 +74,89 @@
<span class="totp-code">{{totpCodeFormatted}}</span> <span class="totp-code">{{totpCodeFormatted}}</span>
</div> </div>
</div> </div>
<!-- Card -->
<div *ngIf="cipher.card">
<div class="box-content-row" *ngIf="cipher.card.cardholderName">
<span class="row-label">{{'cardholderName' | i18n}}</span>
{{cipher.card.cardholderName}}
</div>
<div class="box-content-row" *ngIf="cipher.card.number">
<div class="action-buttons">
<a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}"
(click)="copy(cipher.card.number)">
<i class="fa fa-lg fa-clipboard"></i>
</a>
</div>
<span class="row-label">{{'number' | i18n}}</span>
{{cipher.card.number}}
</div>
<div class="box-content-row" *ngIf="cipher.card.brand">
<span class="row-label">{{'brand' | i18n}}</span>
{{cipher.card.brand}}
</div>
<div class="box-content-row" *ngIf="cipher.card.expiration">
<span class="row-label">{{'expiration' | i18n}}</span>
{{cipher.card.expiration}}
</div>
<div class="box-content-row" *ngIf="cipher.card.code">
<div class="action-buttons">
<a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}"
(click)="copy(cipher.card.code)">
<i class="fa fa-lg fa-clipboard"></i>
</a>
</div>
<span class="row-label">{{'securityCode' | i18n}}</span>
{{cipher.card.code}}
</div>
</div>
<!-- Identity -->
<div *ngIf="cipher.identity">
<div class="box-content-row" *ngIf="cipher.identity.fullName">
<span class="row-label">{{'identityName' | i18n}}</span>
{{cipher.identity.fullName}}
</div>
<div class="box-content-row" *ngIf="cipher.identity.username">
<span class="row-label">{{'username' | i18n}}</span>
{{cipher.identity.username}}
</div>
<div class="box-content-row" *ngIf="cipher.identity.company">
<span class="row-label">{{'company' | i18n}}</span>
{{cipher.identity.company}}
</div>
<div class="box-content-row" *ngIf="cipher.identity.ssn">
<span class="row-label">{{'ssn' | i18n}}</span>
{{cipher.identity.ssn}}
</div>
<div class="box-content-row" *ngIf="cipher.identity.passportNumber">
<span class="row-label">{{'passportNumber' | i18n}}</span>
{{cipher.identity.passportNumber}}
</div>
<div class="box-content-row" *ngIf="cipher.identity.licenseNumber">
<span class="row-label">{{'licenseNumber' | i18n}}</span>
{{cipher.identity.licenseNumber}}
</div>
<div class="box-content-row" *ngIf="cipher.identity.email">
<span class="row-label">{{'email' | i18n}}</span>
{{cipher.identity.email}}
</div>
<div class="box-content-row" *ngIf="cipher.identity.phone">
<span class="row-label">{{'phone' | i18n}}</span>
{{cipher.identity.phone}}
</div>
<div class="box-content-row"
*ngIf="cipher.identity.address1 || cipher.identity.city || cipher.identity.country">
<span class="row-label">{{'address' | i18n}}</span>
<div *ngIf="cipher.identity.address1">{{cipher.identity.address1}}</div>
<div *ngIf="cipher.identity.address2">{{cipher.identity.address2}}</div>
<div *ngIf="cipher.identity.address3">{{cipher.identity.address3}}</div>
<div *ngIf="cipher.identity.city || cipher.identity.state || cipher.identity.postalCode">
{{cipher.identity.city || '-'}},
{{cipher.identity.state || '-'}},
{{cipher.identity.postalCode || '-'}}
</div>
<div *ngIf="cipher.identity.country">{{cipher.identity.country}}</div>
</div>
</div>
</div> </div>
</div> </div>
<div class="box" *ngIf="cipher.notes"> <div class="box" *ngIf="cipher.notes">
@ -84,7 +172,31 @@
{{'customFields' | i18n}} {{'customFields' | i18n}}
</div> </div>
<div class="box-content"> <div class="box-content">
todo <div class="box-content-row" *ngFor="let field of cipher.fields">
<div class="action-buttons">
<a class="row-btn" href="#" appStopClick title="{{'toggleVisibility' | i18n}}"
*ngIf="field.type === fieldType.Hidden" (click)="toggleFieldValue(field)">
<i class="fa fa-lg"
[ngClass]="{'fa-eye': !field.showValue, 'fa-eye-slash': field.showValue}"></i>
</a>
<a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}"
*ngIf="field.value && field.type !== fieldType.Boolean" (click)="copy(field.value)">
<i class="fa fa-lg fa-clipboard"></i>
</a>
</div>
<span class="row-label">{{field.name}}</span>
<div *ngIf="field.type === fieldType.Text">
{{field.value || '&nbsp;'}}
</div>
<div *ngIf="field.type === fieldType.Hidden">
<span [hidden]="!field.showValue" class="monospaced">{{field.value}}</span>
<span [hidden]="field.showValue" class="monospaced">{{field.maskedValue}}</span>
</div>
<div *ngIf="field.type === fieldType.Boolean">
<i class="fa fa-check-square-o" *ngIf="field.value === 'true'"></i>
<i class="fa fa-square-o" *ngIf="field.value !== 'true'"></i>
</div>
</div>
</div> </div>
</div> </div>
<div class="box" *ngIf="cipher.hasAttachments && isPremium"> <div class="box" *ngIf="cipher.hasAttachments && isPremium">
@ -92,7 +204,13 @@
{{'attachments' | i18n}} {{'attachments' | i18n}}
</div> </div>
<div class="box-content"> <div class="box-content">
todo <a class="box-content-row" *ngFor="let attachment of cipher.attachments" href="#" appStopClick
(click)="downloadAttachment(attachment)">
<i class="right-icon fa fa-download fa-fw" *ngIf="!attachment.downloading"></i>
<i class="right-icon fa fa-spinner fa-fw fa-spin" *ngIf="attachment.downloading"></i>
<small class="row-sub-label">{{attachment.sizeName}}</small>
{{attachment.fileName}}
</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -10,12 +10,19 @@ import {
} from '@angular/core'; } from '@angular/core';
import { CipherType } from 'jslib/enums/cipherType'; import { CipherType } from 'jslib/enums/cipherType';
import { FieldType } from 'jslib/enums/fieldType';
import { CipherService } from 'jslib/abstractions/cipher.service'; 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 { TokenService } from 'jslib/abstractions/token.service';
import { TotpService } from 'jslib/abstractions/totp.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 { CipherView } from 'jslib/models/view/cipherView';
import { FieldView } from 'jslib/models/view/fieldView';
@Component({ @Component({
selector: 'app-vault-view', selector: 'app-vault-view',
@ -32,11 +39,14 @@ export class ViewComponent implements OnChanges, OnDestroy {
totpDash: number; totpDash: number;
totpSec: number; totpSec: number;
totpLow: boolean; totpLow: boolean;
fieldType = FieldType;
private totpInterval: NodeJS.Timer; private totpInterval: NodeJS.Timer;
constructor(private cipherService: CipherService, private totpService: TotpService, 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() { async ngOnChanges() {
@ -70,12 +80,57 @@ export class ViewComponent implements OnChanges, OnDestroy {
this.showPassword = !this.showPassword; this.showPassword = !this.showPassword;
} }
toggleFieldValue(field: FieldView) {
const f = (field as any);
f.showValue = !f.showValue;
}
launch() { 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) { 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() { private cleanUp() {

View File

@ -91,5 +91,56 @@
}, },
"toggleVisibility": { "toggleVisibility": {
"message": "Toggle Visibility" "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"
} }
} }

View File

@ -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 path from 'path';
import * as url from 'url'; 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'; import { I18nService } from './services/i18n.service';
const i18nService = new I18nService('en', './locales/'); const i18nService = new I18nService('en', './locales/');
i18nService.init().then(() => { }); i18nService.init().then(() => { });

View File

@ -589,6 +589,9 @@ a {
padding: 10px 15px; padding: 10px 15px;
position: relative; position: relative;
z-index: 1; z-index: 1;
display: block;
color: $text-color;
overflow-wrap: break-word;
&:before { &:before {
content: ""; content: "";
@ -626,6 +629,12 @@ a {
overflow-x: auto; overflow-x: auto;
} }
.no-wrap {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row-label { .row-label {
font-size: $font-size-small; font-size: $font-size-small;
color: $text-muted; 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;
}
} }
} }

View File

@ -1,6 +1,9 @@
import { ipcRenderer, shell } from 'electron';
import { DeviceType } from 'jslib/enums'; 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 = { const AnalyticsIds = {
[DeviceType.Windows]: 'UA-81915606-17', [DeviceType.Windows]: 'UA-81915606-17',
@ -12,6 +15,9 @@ export class DesktopPlatformUtilsService implements PlatformUtilsService {
private deviceCache: DeviceType = null; private deviceCache: DeviceType = null;
private analyticsIdCache: string = null; private analyticsIdCache: string = null;
constructor(private i18nService: I18nService) {
}
getDevice(): DeviceType { getDevice(): DeviceType {
if (!this.deviceCache) { if (!this.deviceCache) {
switch (process.platform) { switch (process.platform) {
@ -93,4 +99,25 @@ export class DesktopPlatformUtilsService implements PlatformUtilsService {
isViewOpen(): boolean { isViewOpen(): boolean {
return true; 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,
});
}
} }

View File

@ -1,11 +1,13 @@
import * as path from 'path'; import * as path from 'path';
import { I18nService as I18nServiceAbstraction } from 'jslib/abstractions/i18n.service';
// First locale is the default (English) // First locale is the default (English)
const SupportedLocales = [ const SupportedLocales = [
'en', 'es', 'en', 'es',
]; ];
export class I18nService { export class I18nService implements I18nServiceAbstraction {
defaultMessages: any = {}; defaultMessages: any = {};
localeMessages: any = {}; localeMessages: any = {};
language: string; language: string;