get vault working

This commit is contained in:
Kyle Spearrin 2018-06-06 17:25:57 -04:00
parent cc9410602c
commit a89cf28812
18 changed files with 960 additions and 151 deletions

6
package-lock.json generated
View File

@ -165,6 +165,12 @@
"debug": "2.6.9"
}
},
"@types/jquery": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.2.tgz",
"integrity": "sha512-ByZwKSEqteAta4VrIalqGJZmMq9lWPD3H3f5Xs6RR8B7zQRDPGUtjoKBYNtKTz/7LgBEQMdlxVbbjQfUaEIItA==",
"dev": true
},
"@types/lunr": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@types/lunr/-/lunr-2.1.5.tgz",

View File

@ -17,6 +17,7 @@
"devDependencies": {
"@angular/compiler-cli": "5.2.0",
"@ngtools/webpack": "1.10.2",
"@types/jquery": "^3.3.2",
"@types/lunr": "2.1.5",
"@types/node": "8.0.19",
"@types/node-forge": "0.6.10",

View File

@ -15,6 +15,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ServicesModule } from './services.module';
import { AppComponent } from './app.component';
import { ModalComponent } from './modal.component';
import { HintComponent } from './accounts/hint.component';
import { LoginComponent } from './accounts/login.component';
@ -22,6 +23,10 @@ import { RegisterComponent } from './accounts/register.component';
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
import { AttachmentsComponent } from './vault/attachments.component';
import { CiphersComponent } from './vault/ciphers.component';
import { FolderAddEditComponent } from './vault/folder-add-edit.component';
import { GroupingsComponent } from './vault/groupings.component';
import { VaultComponent } from './vault/vault.component';
import { IconComponent } from 'jslib/angular/components/icon.component';
@ -37,6 +42,7 @@ import { StopPropDirective } from 'jslib/angular/directives/stop-prop.directive'
import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe';
import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
import { Folder } from 'jslib/models/domain';
@NgModule({
imports: [
@ -55,15 +61,20 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
declarations: [
ApiActionDirective,
AppComponent,
AttachmentsComponent,
AutofocusDirective,
BlurClickDirective,
BoxRowDirective,
CiphersComponent,
FallbackSrcDirective,
FolderAddEditComponent,
GroupingsComponent,
HintComponent,
IconComponent,
I18nPipe,
InputVerbatimDirective,
LoginComponent,
ModalComponent,
RegisterComponent,
SearchCiphersPipe,
StopClickDirective,
@ -73,7 +84,9 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
VaultComponent,
],
entryComponents: [
AttachmentsComponent,
FolderAddEditComponent,
ModalComponent,
],
providers: [],
bootstrap: [AppComponent],

View File

@ -0,0 +1,62 @@
import * as jq from 'jquery';
import {
Component,
ComponentFactoryResolver,
Type,
ViewContainerRef,
} from '@angular/core';
import { ModalComponent as BaseModalComponent } from 'jslib/angular/components/modal.component';
@Component({
selector: 'app-modal',
template: `<ng-template #container></ng-template>`,
})
export class ModalComponent extends BaseModalComponent {
el: any = null;
constructor(componentFactoryResolver: ComponentFactoryResolver) {
super(componentFactoryResolver);
}
ngOnDestroy() { /* Nothing */ }
show<T>(type: Type<T>, parentContainer: ViewContainerRef, fade: boolean = true): T {
this.parentContainer = parentContainer;
this.fade = fade;
const factory = this.componentFactoryResolver.resolveComponentFactory<T>(type);
const componentRef = this.container.createComponent<T>(factory);
const modals = Array.from(document.querySelectorAll('.modal'));
if (modals.length > 0) {
this.el = jq(modals[0]);
this.el.modal('show');
this.el.on('show.bs.modal', () => {
this.onShow.emit();
});
this.el.on('shown.bs.modal', () => {
this.onShown.emit();
});
this.el.on('hide.bs.modal', () => {
this.onClose.emit();
});
this.el.on('hidden.bs.modal', () => {
this.onClosed.emit();
if (this.parentContainer != null) {
this.parentContainer.clear();
}
});
}
return componentRef.instance;
}
close() {
if (this.el != null) {
this.el.modal('hide');
}
}
}

View File

@ -0,0 +1,309 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="content">
<div class="inner-content" *ngIf="cipher">
<div class="box">
<div class="box-header">
{{title}}
</div>
<div class="box-content">
<div class="box-content-row" *ngIf="!editMode" appBoxRow>
<label for="type">{{'type' | i18n}}</label>
<select id="type" name="Type" [(ngModel)]="cipher.type">
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
<div class="box-content-row" appBoxRow>
<label for="name">{{'name' | i18n}}</label>
<input id="name" type="text" name="Name" [(ngModel)]="cipher.name" [appAutofocus]="!editMode">
</div>
<!-- Login -->
<div *ngIf="cipher.type === cipherType.Login">
<div class="box-content-row" appBoxRow>
<label for="loginUsername">{{'username' | i18n}}</label>
<input id="loginUsername" type="text" name="Login.Username"
[(ngModel)]="cipher.login.username">
</div>
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="loginPassword">{{'password' | i18n}}</label>
<input id="loginPassword" class="monospaced"
type="{{showPassword ? 'text' : 'password'}}" name="Login.Password"
[(ngModel)]="cipher.login.password">
</div>
<div class="action-buttons">
<button type="button" #checkPasswordBtn class="row-btn btn" appBlurClick
title="{{'checkPassword' | i18n}}" (click)="checkPassword()"
[appApiAction]="checkPasswordPromise" [disabled]="checkPasswordBtn.loading">
<i class="fa fa-lg fa-check-circle" [hidden]="checkPasswordBtn.loading"></i>
<i class="fa fa-lg fa-spinner fa-spin" [hidden]="!checkPasswordBtn.loading"></i>
</button>
<a class="row-btn" href="#" appStopClick appBlurClick
title="{{'toggleVisibility' | i18n}}" (click)="togglePassword()">
<i class="fa fa-lg"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</a>
<a class="row-btn" href="#" appStopClick appBlurClick
title="{{'generatePassword' | i18n}}" (click)="generatePassword()">
<i class="fa fa-lg fa-refresh"></i>
</a>
</div>
</div>
<div class="box-content-row" appBoxRow>
<label for="loginTotp">{{'authenticatorKeyTotp' | i18n}}</label>
<input id="loginTotp" type="text" name="Login.Totp" class="monospaced"
[(ngModel)]="cipher.login.totp">
</div>
</div>
<!-- Card -->
<div *ngIf="cipher.type === cipherType.Card">
<div class="box-content-row" appBoxRow>
<label for="cardCardholderName">{{'cardholderName' | i18n}}</label>
<input id="cardCardholderName" type="text" name="Card.CardCardholderName"
[(ngModel)]="cipher.card.cardholderName">
</div>
<div class="box-content-row" appBoxRow>
<label for="cardNumber">{{'number' | i18n}}</label>
<input id="cardNumber" type="text" name="Card.Number" [(ngModel)]="cipher.card.number">
</div>
<div class="box-content-row" appBoxRow>
<label for="cardBrand">{{'brand' | i18n}}</label>
<select id="cardBrand" name="Card.Brand" [(ngModel)]="cipher.card.brand">
<option *ngFor="let o of cardBrandOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
<div class="box-content-row" appBoxRow>
<label for="cardExpMonth">{{'expirationMonth' | i18n}}</label>
<select id="cardExpMonth" name="Card.ExpMonth" [(ngModel)]="cipher.card.expMonth">
<option *ngFor="let o of cardExpMonthOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
<div class="box-content-row" appBoxRow>
<label for="cardExpYear">{{'expirationYear' | i18n}}</label>
<input id="cardExpYear" type="text" name="Card.ExpYear" [(ngModel)]="cipher.card.expYear"
placeholder="{{'ex' | i18n}} 2019">
</div>
<div class="box-content-row" appBoxRow>
<label for="cardCode">{{'securityCode' | i18n}}</label>
<input id="cardCode" type="text" name="Card.Code" [(ngModel)]="cipher.card.code">
</div>
</div>
<!-- Identity -->
<div *ngIf="cipher.type === cipherType.Identity">
<div class="box-content-row" appBoxRow>
<label for="idTitle">{{'title' | i18n}}</label>
<select id="idTitle" name="Identity.Title" [(ngModel)]="cipher.identity.title">
<option *ngFor="let o of identityTitleOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
<div class="box-content-row" appBoxRow>
<label for="idFirstName">{{'firstName' | i18n}}</label>
<input id="idFirstName" type="text" name="Identity.FirstName"
[(ngModel)]="cipher.identity.firstName">
</div>
<div class="box-content-row" appBoxRow>
<label for="idMiddleName">{{'middleName' | i18n}}</label>
<input id="idMiddleName" type="text" name="Identity.MiddleName"
[(ngModel)]="cipher.identity.middleName">
</div>
<div class="box-content-row" appBoxRow>
<label for="idLastName">{{'lastName' | i18n}}</label>
<input id="idLastName" type="text" name="Identity.LastName"
[(ngModel)]="cipher.identity.lastName">
</div>
<div class="box-content-row" appBoxRow>
<label for="idUsername">{{'username' | i18n}}</label>
<input id="idUsername" type="text" name="Identity.Username"
[(ngModel)]="cipher.identity.username">
</div>
<div class="box-content-row" appBoxRow>
<label for="idCompany">{{'company' | i18n}}</label>
<input id="idCompany" type="text" name="Identity.Company"
[(ngModel)]="cipher.identity.company">
</div>
<div class="box-content-row" appBoxRow>
<label for="idSsn">{{'ssn' | i18n}}</label>
<input id="idSsn" type="text" name="Identity.SSN" [(ngModel)]="cipher.identity.ssn">
</div>
<div class="box-content-row" appBoxRow>
<label for="idPassportNumber">{{'passportNumber' | i18n}}</label>
<input id="idPassportNumber" type="text" name="Identity.PassportNumber"
[(ngModel)]="cipher.identity.passportNumber">
</div>
<div class="box-content-row" appBoxRow>
<label for="idLicenseNumber">{{'licenseNumber' | i18n}}</label>
<input id="idLicenseNumber" type="text" name="Identity.LicenseNumber"
[(ngModel)]="cipher.identity.licenseNumber">
</div>
<div class="box-content-row" appBoxRow>
<label for="idEmail">{{'email' | i18n}}</label>
<input id="idEmail" type="text" name="Identity.Email" [(ngModel)]="cipher.identity.email">
</div>
<div class="box-content-row" appBoxRow>
<label for="idPhone">{{'phone' | i18n}}</label>
<input id="idPhone" type="text" name="Identity.Phone" [(ngModel)]="cipher.identity.phone">
</div>
<div class="box-content-row" appBoxRow>
<label for="idAddress1">{{'address1' | i18n}}</label>
<input id="idAddress1" type="text" name="Identity.Address1"
[(ngModel)]="cipher.identity.address1">
</div>
<div class="box-content-row" appBoxRow>
<label for="idAddress2">{{'address2' | i18n}}</label>
<input id="idAddress2" type="text" name="Identity.Address2"
[(ngModel)]="cipher.identity.address2">
</div>
<div class="box-content-row" appBoxRow>
<label for="idAddress3">{{'address3' | i18n}}</label>
<input id="idAddress3" type="text" name="Identity.Address3"
[(ngModel)]="cipher.identity.address3">
</div>
<div class="box-content-row" appBoxRow>
<label for="idCity">{{'cityTown' | i18n}}</label>
<input id="idCity" type="text" name="Identity.City" [(ngModel)]="cipher.identity.city">
</div>
<div class="box-content-row" appBoxRow>
<label for="idState">{{'stateProvince' | i18n}}</label>
<input id="idState" type="text" name="Identity.State" [(ngModel)]="cipher.identity.state">
</div>
<div class="box-content-row" appBoxRow>
<label for="idPostalCode">{{'zipPostalCode' | i18n}}</label>
<input id="idPostalCode" type="text" name="Identity.PostalCode"
[(ngModel)]="cipher.identity.postalCode">
</div>
<div class="box-content-row" appBoxRow>
<label for="idCountry">{{'country' | i18n}}</label>
<input id="idCountry" type="text" name="Identity.Country"
[(ngModel)]="cipher.identity.country">
</div>
</div>
</div>
</div>
<div class="box" *ngIf="cipher.type === cipherType.Login">
<div class="box-content">
<ng-container *ngIf="cipher.login.hasUris">
<div class="box-content-row box-content-row-multi" appBoxRow
*ngFor="let u of cipher.login.uris; let i = index">
<a href="#" appStopClick (click)="removeUri(u)" title="{{'remove' | i18n}}">
<i class="fa fa-minus-circle fa-lg"></i>
</a>
<div class="row-main">
<label for="loginUri{{i}}">{{'uriPosition' | i18n : (i + 1)}}</label>
<input id="loginUri{{i}}" type="text" name="Login.Uris[{{i}}].Uri" [(ngModel)]="u.uri"
placeholder="{{'ex' | i18n}} https://google.com">
<label for="loginUriMatch{{i}}" class="sr-only">
{{'matchDetection' | i18n}} {{(i + 1)}}
</label>
<select id="loginUriMatch{{i}}" name="Login.Uris[{{i}}].Match" [(ngModel)]="u.match"
[hidden]="u.showOptions === false || (u.showOptions == null && u.match == null)"
(change)="loginUriMatchChanged(u)">
<option *ngFor="let o of uriMatchOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
<div class="action-buttons">
<a class="row-btn" href="#" appStopClick appBlurClick
title="{{'toggleOptions' | i18n}}" (click)="toggleUriOptions(u)">
<i class="fa fa-lg fa-cog"></i>
</a>
</div>
</div>
</ng-container>
<a href="#" appStopClick appBlurClick (click)="addUri()" class="box-content-row">
<i class="fa fa-plus-circle fa-fw fa-lg"></i> {{'newUri' | i18n}}
</a>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="folder">{{'folder' | i18n}}</label>
<select id="folder" name="FolderId" [(ngModel)]="cipher.folderId">
<option *ngFor="let f of folders" [ngValue]="f.id">{{f.name}}</option>
</select>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="favorite">{{'favorite' | i18n}}</label>
<input id="favorite" type="checkbox" name="Favorite" [(ngModel)]="cipher.favorite">
</div>
<a class="box-content-row box-content-row-flex text-default" href="#" appStopClick appBlurClick
(click)="attachments()" *ngIf="editMode">
<div class="row-main">{{'attachments' | i18n}}</div>
<i class="fa fa-chevron-right row-sub-icon"></i>
</a>
</div>
</div>
<div class="box">
<div class="box-header">
<label for="notes">{{'notes' | i18n}}</label>
</div>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="cipher.notes"></textarea>
</div>
</div>
</div>
<div class="box">
<div class="box-header">
{{'customFields' | i18n}}
</div>
<div class="box-content">
<ng-container *ngIf="cipher.hasFields">
<div class="box-content-row box-content-row-multi" appBoxRow
*ngFor="let f of cipher.fields; let i = index"
[ngClass]="{'box-content-row-checkbox': f.type === fieldType.Boolean}">
<a href="#" appStopClick (click)="removeField(f)" title="{{'remove' | i18n}}">
<i class="fa fa-minus-circle fa-lg"></i>
</a>
<label for="fieldName{{i}}" class="sr-only">{{'name' | i18n}}</label>
<label for="fieldValue{{i}}" class="sr-only">{{'value' | i18n}}</label>
<div class="row-main">
<input id="fieldName{{i}}" type="text" name="Field.Name{{i}}" [(ngModel)]="f.name"
class="row-label" placeholder="{{'name' | i18n}}">
<input id="fieldValue{{i}}" type="text" name="Field.Value{{i}}" [(ngModel)]="f.value"
*ngIf="f.type === fieldType.Text" placeholder="{{'value' | i18n}}">
<input id="fieldValue{{i}}" type="{{f.showValue ? 'text' : 'password'}}"
name="Field.Value{{i}}" [(ngModel)]="f.value" class="monospaced"
*ngIf="f.type === fieldType.Hidden" placeholder="{{'value' | i18n}}">
</div>
<input id="fieldValue{{i}}" name="Field.Value{{i}}" type="checkbox"
[(ngModel)]="f.value" *ngIf="f.type === fieldType.Boolean"
appTrueFalseValue trueValue="true" falseValue="false">
<div class="action-buttons" *ngIf="f.type === fieldType.Hidden">
<a class="row-btn" href="#" appStopClick appBlurClick
title="{{'toggleVisibility' | i18n}}" (click)="toggleFieldValue(f)">
<i class="fa fa-lg"
[ngClass]="{'fa-eye': !f.showValue, 'fa-eye-slash': f.showValue}"></i>
</a>
</div>
</div>
</ng-container>
<div class="box-content-row" appBoxRow>
<a href="#" appStopClick (click)="addField()">
<i class="fa fa-plus-circle fa-fw fa-lg"></i> {{'newCustomField' | i18n}}
</a>
<label for="addFieldType" class="sr-only">{{'type' | i18n}}</label>
<select id="addFieldType" name="AddFieldType" [(ngModel)]="addFieldType" class="field-type">
<option *ngFor="let o of addFieldTypeOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="footer">
<button appBlurClick type="submit" class="primary" title="{{'save' | i18n}}" [disabled]="form.loading">
<i class="fa fa-save fa-lg fa-fw" [hidden]="form.loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!form.loading"></i>
</button>
<button appBlurClick type="button" (click)="cancel()" title="{{'cancel' | i18n}}">
{{'cancel' | i18n}}
</button>
<div class="right">
<button #deleteBtn appBlurClick type="button" (click)="delete()" class="danger"
title="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
[appApiAction]="deletePromise">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"></i>
</button>
</div>
</div>
</form>

View File

@ -0,0 +1,34 @@
import {
Component,
OnChanges,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
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 { AddEditComponent as BaseAddEditComponent } from 'jslib/angular/components/add-edit.component';
@Component({
selector: 'app-vault-add-edit',
templateUrl: 'add-edit.component.html',
})
export class AddEditComponent extends BaseAddEditComponent implements OnChanges {
constructor(cipherService: CipherService, folderService: FolderService,
i18nService: I18nService, platformUtilsService: PlatformUtilsService,
analytics: Angulartics2, toasterService: ToasterService,
auditService: AuditService, stateService: StateService) {
super(cipherService, folderService, i18nService, platformUtilsService, analytics,
toasterService, auditService, stateService);
}
async ngOnChanges() {
await super.load();
}
}

View File

@ -0,0 +1,50 @@
<div class="modal fade">
<div class="modal-dialog">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-body">
<div class="box" *ngIf="cipher && cipher.hasAttachments">
<div class="box-header">
{{'attachments' | i18n}}
</div>
<div class="box-content no-hover">
<div class="box-content-row box-content-row-flex" *ngFor="let a of cipher.attachments">
<div class="row-main">
{{a.fileName}}
</div>
<small class="row-sub-label">{{a.sizeName}}</small>
<div class="action-buttons no-pad">
<button class="row-btn btn" type="button" appStopClick appBlurClick
title="{{'delete' | i18n}}" (click)="delete(a)" #deleteBtn
[appApiAction]="deletePromises[a.id]" [disabled]="deleteBtn.loading">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"></i>
</button>
</div>
</div>
</div>
</div>
<div class="box">
<div class="box-header">
{{'newAttachment' | i18n}}
</div>
<div class="box-content no-hover">
<div class="box-content-row">
<label for="file">{{'file' | i18n}}</label>
<input type="file" id="file" name="file" required>
</div>
</div>
<div class="box-footer">
{{'maxFileSize' | i18n}}
</div>
</div>
</div>
<div class="modal-footer">
<button appBlurClick type="submit" class="primary" title="{{'save' | i18n}}" [disabled]="form.loading">
<i class="fa fa-save fa-lg fa-fw" [hidden]="form.loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!form.loading"></i>
</button>
<button type="button" data-dismiss="modal">{{'close' | i18n}}</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,26 @@
import { Component } from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
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 { AttachmentsComponent as BaseAttachmentsComponent } from 'jslib/angular/components/attachments.component';
@Component({
selector: 'app-vault-attachments',
templateUrl: 'attachments.component.html',
})
export class AttachmentsComponent extends BaseAttachmentsComponent {
constructor(cipherService: CipherService, analytics: Angulartics2,
toasterService: ToasterService, i18nService: I18nService,
cryptoService: CryptoService, tokenService: TokenService,
platformUtilsService: PlatformUtilsService) {
super(cipherService, analytics, toasterService, i18nService, cryptoService, tokenService,
platformUtilsService);
}
}

View File

@ -0,0 +1,43 @@
<ng-container *ngIf="(ciphers | searchCiphers: searchText) as searchedCiphers">
<table class="table table-hover table-sm" *ngIf="searchedCiphers.length > 0">
<tbody>
<tr *ngFor="let c of searchedCiphers">
<td>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<i class="fa fa-cog"></i>
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<a class="dropdown-item" href="#">Something else here</a>
</div>
</div>
</td>
<td>
<input type="checkbox">
</td>
<td>
<app-vault-icon [cipher]="c"></app-vault-icon>
</td>
<td>
<a href="#" appStopClick (click)="selectCipher(c)" title="{{'viewItem' | i18n}}">
{{c.name}}
<i class="fa fa-share-alt text-muted" *ngIf="c.organizationId" title="{{'shared' | i18n}}"></i>
<i class="fa fa-paperclip text-muted" *ngIf="c.hasAttachments" title="{{'attachments' | i18n}}"></i>
</a>
<small class="text-muted">{{c.subTitle}}</small>
</td>
</tr>
</tbody>
</table>
<div class="no-items" *ngIf="searchedCiphers.length === 0">
<i class="fa fa-spinner fa-spin fa-3x" *ngIf="!loaded"></i>
<ng-container *ngIf="loaded">
<i class="fa fa-frown-o fa-4x"></i>
<p>{{'noItemsInList' | i18n}}</p>
<button (click)="addCipher()" class="btn block primary link">{{'addItem' | i18n}}</button>
</ng-container>
</div>
</ng-container>

View File

@ -0,0 +1,15 @@
import { Component } from '@angular/core';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { CiphersComponent as BaseCiphersComponent } from 'jslib/angular/components/ciphers.component';
@Component({
selector: 'app-vault-ciphers',
templateUrl: 'ciphers.component.html',
})
export class CiphersComponent extends BaseCiphersComponent {
constructor(cipherService: CipherService) {
super(cipherService);
}
}

View File

@ -0,0 +1,35 @@
<div class="modal fade">
<div class="modal-dialog modal-sm">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-body">
<div class="box">
<div class="box-header">
{{title}}
</div>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="name">{{'name' | i18n}}</label>
<input id="name" type="text" name="Name" [(ngModel)]="folder.name"
[appAutofocus]="!editMode">
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button appBlurClick type="submit" class="primary" title="{{'save' | i18n}}" [disabled]="form.loading">
<i class="fa fa-save fa-lg fa-fw" [hidden]="form.loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!form.loading"></i>
</button>
<button type="button" data-dismiss="modal">{{'close' | i18n}}</button>
<div class="right">
<button #deleteBtn appBlurClick type="button" (click)="delete()" class="danger"
title="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
[appApiAction]="deletePromise">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,24 @@
import { Component } from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { FolderService } from 'jslib/abstractions/folder.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import {
FolderAddEditComponent as BaseFolderAddEditComponent,
} from 'jslib/angular/components/folder-add-edit.component';
@Component({
selector: 'app-folder-add-edit',
templateUrl: 'folder-add-edit.component.html',
})
export class FolderAddEditComponent extends BaseFolderAddEditComponent {
constructor(folderService: FolderService, i18nService: I18nService,
analytics: Angulartics2, toasterService: ToasterService,
platformUtilsService: PlatformUtilsService) {
super(folderService, i18nService, analytics, toasterService, platformUtilsService);
}
}

View File

@ -0,0 +1,72 @@
<div class="card">
<div class="card-header">
Filters
</div>
<div class="card-body">
<input type="search" class="form-control" id="search" placeholder="Search vault">
<ul class="fa-ul">
<li [ngClass]="{active: selectedAll}">
<a href="#" appStopClick appBlurClick (click)="selectAll()">
<i class="fa-li fa fa-fw fa-th"></i>All Items
</a>
</li>
<li [ngClass]="{active: selectedFavorites}">
<a href="#" appStopClick appBlurClick (click)="selectFavorites()">
<i class="fa-li fa fa-fw fa-star"></i>Favorites
</a>
</li>
</ul>
<h3>Types</h3>
<ul class="fa-ul">
<li [ngClass]="{active: selectedType === cipherType.Login}">
<a href="#" appStopClick appBlurClick (click)="selectType(cipherType.Login)">
<i class="fa-li fa fa-fw fa-globe"></i>Login
</a>
</li>
<li [ngClass]="{active: selectedType === cipherType.Card}">
<a href="#" appStopClick appBlurClick (click)="selectType(cipherType.Card)">
<i class="fa-li fa fa-fw fa-credit-card"></i>Card
</a>
</li>
<li [ngClass]="{active: selectedType === cipherType.Identity}">
<a href="#" appStopClick appBlurClick (click)="selectType(cipherType.Identity)">
<i class="fa-li fa fa-fw fa-id-card-o"></i>Identity
</a>
</li>
<li [ngClass]="{active: selectedType === cipherType.SecureNote}">
<a href="#" appStopClick appBlurClick (click)="selectType(cipherType.SecureNote)">
<i class="fa-li fa fa-fw fa-sticky-note-o"></i>Secure Note
</a>
</li>
</ul>
<p *ngIf="!loaded" class="text-muted">Loading...</p>
<ng-container *ngIf="loaded">
<h3>
Folders
<button appBlurClick (click)="addFolder()" title="{{'addFolder' | i18n}}">
<i class="fa fa-plus fa-fw"></i>
</button>
</h3>
<ul class="fa-ul">
<li *ngFor="let f of folders" [ngClass]="{active: selectedFolder && f.id === selectedFolderId}">
<a href="#" appStopClick appBlurClick (click)="selectFolder(f)">
<i class="fa-li fa fa-caret-right"></i> {{f.name}}
<span appStopProp appStopClick (click)="editFolder(f)" title="{{'editFolder' | i18n}}" *ngIf="f.id">
<i class="fa fa-pencil fa-fw"></i>
</span>
</a>
</li>
</ul>
<div *ngIf="collections && collections.length">
<h3>Collections</h3>
<ul class="fa-ul">
<li *ngFor="let c of collections" [ngClass]="{active: c.id === selectedCollectionId}">
<a href="#" appStopClick appBlurClick (click)="selectCollection(c)">
<i class="fa-li fa fa-caret-right"></i> {{c.name}}
</a>
</li>
</ul>
</div>
</ng-container>
</div>
</div>

View File

@ -0,0 +1,16 @@
import { Component } from '@angular/core';
import { CollectionService } from 'jslib/abstractions/collection.service';
import { FolderService } from 'jslib/abstractions/folder.service';
import { GroupingsComponent as BaseGroupingsComponent } from 'jslib/angular/components/groupings.component';
@Component({
selector: 'app-vault-groupings',
templateUrl: 'groupings.component.html',
})
export class GroupingsComponent extends BaseGroupingsComponent {
constructor(collectionService: CollectionService, folderService: FolderService) {
super(collectionService, folderService);
}
}

View File

@ -11,16 +11,6 @@
<li class="nav-item">
<a class="nav-link" href="#">Settings</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="nav-list" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Some link
</a>
<div class="dropdown-menu" aria-labelledby="nav-list">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<a class="dropdown-item" href="#">Something else here</a>
</div>
</li>
</ul>
</div>
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
@ -39,140 +29,29 @@
</nav>
<div class="container page-content">
<div class="row">
<div class="col-4">
<div class="col-3">
<app-vault-groupings
(onAllClicked)="clearGroupingFilters()"
(onFavoritesClicked)="filterFavorites()"
(onCipherTypeClicked)="filterCipherType($event)"
(onFolderClicked)="filterFolder($event.id)"
(onAddFolder)="addFolder()"
(onEditFolder)="editFolder($event.id)"
(onCollectionClicked)="filterCollection($event.id)">
</app-vault-groupings>
</div>
<div class="col-6">
<app-vault-ciphers
(onCipherClicked)="editCipher($event)">
</app-vault-ciphers>
</div>
<div class="col-3">
<div class="card">
<div class="card-header">
Filters
</div>
<div class="card-body">
<input type="search" class="form-control" id="search" placeholder="Search vault">
<ul class="fa-ul">
<li>
<a href="#">
<i class="fa-li fa fa-fw fa-th"></i>All Items
</a>
</li>
<li>
<a href="#">
<i class="fa-li fa fa-fw fa-star"></i>Favorites
</a>
</li>
</ul>
<h3>Types</h3>
<ul class="fa-ul">
<li>
<a href="#">
<i class="fa-li fa fa-fw fa-globe"></i>Login
</a>
</li>
<li>
<a href="#">
<i class="fa-li fa fa-fw fa-credit-card"></i>Card
</a>
</li>
<li>
<a href="#">
<i class="fa-li fa fa-fw fa-id-card-o"></i>Identity
</a>
</li>
<li>
<a href="#">
<i class="fa-li fa fa-fw fa-sticky-note-o"></i>
Secure Note
</a>
</li>
</ul>
<h3>Folders</h3>
<ul class="fa-ul">
<li>
<a href="#">
<i class="fa-li fa fa-fw fa-caret-right"></i>
Folder 1
</a>
</li>
<li>
<a href="#">
<i class="fa-li fa fa-fw fa-caret-right"></i>
Folder 1
</a>
</li>
<li>
<a href="#">
<i class="fa-li fa fa-fw fa-caret-right"></i>
Folder 1
</a>
</li>
<li>
<a href="#">
<i class="fa-li fa fa-fw fa-caret-right"></i>
Folder 1
</a>
</li>
<li>
<a href="#">
<i class="fa-li fa fa-fw fa-caret-right"></i>
Folder 1
</a>
</li>
<li>
<a href="#">
<i class="fa-li fa fa-fw fa-caret-right"></i>
Folder 1
</a>
</li>
</ul>
<h3>Collections</h3>
<ul class="fa-ul">
<li>
<a href="#">
<i class="fa-li fa fa-fw fa-caret-right"></i>
Collection 1
</a>
</li>
<li>
<a href="#">
<i class="fa-li fa fa-fw fa-caret-right"></i>
Collection 1
</a>
</li>
<li>
<a href="#">
<i class="fa-li fa fa-fw fa-caret-right"></i>
Collection 1
</a>
</li>
</ul>
Some callout
</div>
</div>
</div>
<div class="col-8">
<table class="table table-hover table-sm">
<tbody>
<tr>
<td>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<i class="fa fa-cog"></i>
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<a class="dropdown-item" href="#">Something else here</a>
</div>
</div>
</td>
<td>
<input type="checkbox">
</td>
<td>
<a href="#">Google</a>
<small class="text-muted">MyUsername</small>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="container footer text-muted">
@ -188,3 +67,5 @@
</div>
</div>
</div>
<ng-template #attachments></ng-template>
<ng-template #folderAddEdit></ng-template>

View File

@ -1,13 +1,214 @@
import { Location } from '@angular/common';
import {
Component,
ComponentFactoryResolver,
OnInit,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { CipherType } from 'jslib/enums/cipherType';
import { CipherView } from 'jslib/models/view/cipherView';
import { FolderView } from 'jslib/models/view/folderView';
import { ModalComponent } from '../modal.component';
import { AttachmentsComponent } from './attachments.component';
import { CiphersComponent } from './ciphers.component';
import { FolderAddEditComponent } from './folder-add-edit.component';
import { GroupingsComponent } from './groupings.component';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { SyncService } from 'jslib/abstractions/sync.service';
@Component({
selector: 'app-vault',
templateUrl: 'vault.component.html',
})
export class VaultComponent {
export class VaultComponent implements OnInit {
@ViewChild(GroupingsComponent) groupingsComponent: GroupingsComponent;
@ViewChild(CiphersComponent) ciphersComponent: CiphersComponent;
@ViewChild('attachments', { read: ViewContainerRef }) attachmentsModalRef: ViewContainerRef;
@ViewChild('folderAddEdit', { read: ViewContainerRef }) folderAddEditModalRef: ViewContainerRef;
cipherId: string = null;
favorites: boolean = false;
type: CipherType = null;
folderId: string = null;
collectionId: string = null;
private modal: ModalComponent = null;
constructor(private syncService: SyncService, private route: ActivatedRoute,
private router: Router, private location: Location,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver) { }
async ngOnInit() {
this.route.queryParams.subscribe(async (params) => {
await this.syncService.fullSync(true);
await this.groupingsComponent.load();
if (params == null) {
this.groupingsComponent.selectedAll = true;
await this.ciphersComponent.load();
return;
}
if (params.favorites) {
this.groupingsComponent.selectedFavorites = true;
await this.filterFavorites();
} else if (params.type) {
const t = parseInt(params.type, null);
this.groupingsComponent.selectedType = t;
await this.filterCipherType(t);
} else if (params.folderId) {
this.groupingsComponent.selectedFolder = true;
this.groupingsComponent.selectedFolderId = params.folderId;
await this.filterFolder(params.folderId);
} else if (params.collectionId) {
this.groupingsComponent.selectedCollectionId = params.collectionId;
await this.filterCollection(params.collectionId);
} else {
this.groupingsComponent.selectedAll = true;
await this.ciphersComponent.load();
}
});
}
editCipher(cipher: CipherView) {
console.log(cipher);
}
addCipher(type: CipherType = null) {
//
}
async clearGroupingFilters() {
this.ciphersComponent.searchPlaceholder = this.i18nService.t('searchVault');
await this.ciphersComponent.load();
this.clearFilters();
this.go();
}
async filterFavorites() {
this.ciphersComponent.searchPlaceholder = this.i18nService.t('searchFavorites');
await this.ciphersComponent.load((c) => c.favorite);
this.clearFilters();
this.favorites = true;
this.go();
}
async filterCipherType(type: CipherType) {
this.ciphersComponent.searchPlaceholder = this.i18nService.t('searchType');
await this.ciphersComponent.load((c) => c.type === type);
this.clearFilters();
this.type = type;
this.go();
}
async filterFolder(folderId: string) {
folderId = folderId === 'none' ? null : folderId;
this.ciphersComponent.searchPlaceholder = this.i18nService.t('searchFolder');
await this.ciphersComponent.load((c) => c.folderId === folderId);
this.clearFilters();
this.folderId = folderId == null ? 'none' : folderId;
this.go();
}
async filterCollection(collectionId: string) {
this.ciphersComponent.searchPlaceholder = this.i18nService.t('searchCollection');
await this.ciphersComponent.load((c) => c.collectionIds.indexOf(collectionId) > -1);
this.clearFilters();
this.collectionId = collectionId;
this.go();
}
editCipherAttachments(cipher: CipherView) {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.attachmentsModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<AttachmentsComponent>(AttachmentsComponent, this.attachmentsModalRef);
childComponent.cipherId = cipher.id;
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
async addFolder() {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.folderAddEditModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<FolderAddEditComponent>(
FolderAddEditComponent, this.folderAddEditModalRef);
childComponent.folderId = null;
childComponent.onSavedFolder.subscribe(async (folder: FolderView) => {
this.modal.close();
await this.groupingsComponent.loadFolders();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
async editFolder(folderId: string) {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.folderAddEditModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<FolderAddEditComponent>(
FolderAddEditComponent, this.folderAddEditModalRef);
childComponent.folderId = folderId;
childComponent.onSavedFolder.subscribe(async (folder: FolderView) => {
this.modal.close();
await this.groupingsComponent.loadFolders();
});
childComponent.onDeletedFolder.subscribe(async (folder: FolderView) => {
this.modal.close();
await this.groupingsComponent.loadFolders();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
private clearFilters() {
this.folderId = null;
this.collectionId = null;
this.favorites = false;
this.type = null;
}
private go(queryParams: any = null) {
if (queryParams == null) {
queryParams = {
cipherId: this.cipherId,
favorites: this.favorites ? true : null,
type: this.type,
folderId: this.folderId,
collectionId: this.collectionId,
};
}
const url = this.router.createUrlTree(['vault'], { queryParams: queryParams }).toString();
this.location.go(url);
}
}

View File

@ -63,7 +63,7 @@ body {
.footer {
margin-top: 40px;
padding: 40px 0 10px 0;
padding: 40px 0 20px 0;
border-top: 1px solid $border-color;
}
@ -83,6 +83,12 @@ app-vault {
}
}
tr:hover {
td:first-child {
background-color: $body-bg;
}
}
td {
vertical-align: middle;
@ -92,11 +98,22 @@ app-vault {
}
td:first-child {
width: 80px;
width: 65px;
border: none;
}
td:nth-child(2) {
width: 40px;
width: 25px;
}
td:nth-child(3) {
width: 25px;
text-align: center;
img {
@extend .rounded;
@extend .img-fluid;
}
}
}
}

View File

@ -1,23 +1,27 @@
import { StorageService } from 'jslib/abstractions/storage.service';
export class WebStorageService implements StorageService {
private store: any = {};
get<T>(key: string): Promise<T> {
const val = this.store[key];
if (val == null) {
const json = window.sessionStorage.getItem(key);
if (json == null) {
return Promise.resolve(null);
}
return Promise.resolve(val as T);
const obj = JSON.parse(json);
return Promise.resolve(obj as T);
}
save(key: string, obj: any): Promise<any> {
this.store[key] = obj;
if (obj == null) {
this.remove(key);
return;
}
const json = JSON.stringify(obj);
window.sessionStorage.setItem(key, json);
return Promise.resolve();
}
remove(key: string): Promise<any> {
delete this.store[key];
window.sessionStorage.removeItem(key);
return Promise.resolve();
}