add support for login uris

This commit is contained in:
Kyle Spearrin 2018-03-01 23:45:12 -05:00
parent 9b566e5990
commit 72771d4b90
6 changed files with 146 additions and 39 deletions

View File

@ -18,10 +18,6 @@
</div> </div>
<!-- Login --> <!-- Login -->
<div *ngIf="cipher.type === cipherType.Login"> <div *ngIf="cipher.type === cipherType.Login">
<div class="box-content-row" appBoxRow>
<label for="loginUri">{{'uri' | i18n}}</label>
<input id="loginUri" type="text" name="Login.Uri" [(ngModel)]="cipher.login.uri">
</div>
<div class="box-content-row" appBoxRow> <div class="box-content-row" appBoxRow>
<label for="loginUsername">{{'username' | i18n}}</label> <label for="loginUsername">{{'username' | i18n}}</label>
<input id="loginUsername" type="text" name="Login.Username" <input id="loginUsername" type="text" name="Login.Username"
@ -52,6 +48,11 @@
</a> </a>
</div> </div>
</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> </div>
<!-- Card --> <!-- Card -->
<div *ngIf="cipher.type === cipherType.Card"> <div *ngIf="cipher.type === cipherType.Card">
@ -177,13 +178,34 @@
</div> </div>
</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">
{{'autofillDetection' | i18n}} {{(i + 1)}}
</label>
<select id="loginUriMatch{{i}}" name="Login.Uris[{{i}}].Match" [(ngModel)]="u.match">
<option *ngFor="let o of uriMatchOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</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">
<div class="box-content"> <div class="box-content">
<div class="box-content-row" *ngIf="cipher.type === cipherType.Login" appBoxRow>
<label for="loginTotp">{{'authenticatorKeyTotp' | i18n}}</label>
<input id="loginTotp" type="text" name="Login.Totp" class="monospaced"
[(ngModel)]="cipher.login.totp">
</div>
<div class="box-content-row" appBoxRow> <div class="box-content-row" appBoxRow>
<label for="folder">{{'folder' | i18n}}</label> <label for="folder">{{'folder' | i18n}}</label>
<select id="folder" name="FolderId" [(ngModel)]="cipher.folderId"> <select id="folder" name="FolderId" [(ngModel)]="cipher.folderId">
@ -216,12 +238,12 @@
{{'customFields' | i18n}} {{'customFields' | i18n}}
</div> </div>
<div class="box-content"> <div class="box-content">
<div *ngIf="cipher.hasFields"> <ng-container *ngIf="cipher.hasFields">
<div class="box-content-row box-content-row-cf" appBoxRow <div class="box-content-row box-content-row-multi" appBoxRow
*ngFor="let f of cipher.fields; let i = index" *ngFor="let f of cipher.fields; let i = index"
[ngClass]="{'box-content-row-checkbox': f.type === fieldType.Boolean}"> [ngClass]="{'box-content-row-checkbox': f.type === fieldType.Boolean}">
<a href="#" appStopClick (click)="removeField(f)" title="{{'remove' | i18n}}"> <a href="#" appStopClick (click)="removeField(f)" title="{{'remove' | i18n}}">
<i class="fa fa-close fa-lg"></i> <i class="fa fa-minus-circle fa-lg"></i>
</a> </a>
<label for="fieldName{{i}}" class="sr-only">{{'name' | i18n}}</label> <label for="fieldName{{i}}" class="sr-only">{{'name' | i18n}}</label>
<label for="fieldValue{{i}}" class="sr-only">{{'value' | i18n}}</label> <label for="fieldValue{{i}}" class="sr-only">{{'value' | i18n}}</label>
@ -244,7 +266,7 @@
</a> </a>
</div> </div>
</div> </div>
</div> </ng-container>
<div class="box-content-row" appBoxRow> <div class="box-content-row" appBoxRow>
<a href="#" appStopClick (click)="addField()"> <a href="#" appStopClick (click)="addField()">
<i class="fa fa-plus-circle fa-fw fa-lg"></i> {{'newCustomField' | i18n}} <i class="fa fa-plus-circle fa-fw fa-lg"></i> {{'newCustomField' | i18n}}

View File

@ -14,6 +14,7 @@ import { Angulartics2 } from 'angulartics2';
import { CipherType } from 'jslib/enums/cipherType'; import { CipherType } from 'jslib/enums/cipherType';
import { FieldType } from 'jslib/enums/fieldType'; import { FieldType } from 'jslib/enums/fieldType';
import { SecureNoteType } from 'jslib/enums/secureNoteType'; import { SecureNoteType } from 'jslib/enums/secureNoteType';
import { UriMatchType } from 'jslib/enums/uriMatchType';
import { AuditService } from 'jslib/abstractions/audit.service'; import { AuditService } from 'jslib/abstractions/audit.service';
import { CipherService } from 'jslib/abstractions/cipher.service'; import { CipherService } from 'jslib/abstractions/cipher.service';
@ -26,6 +27,7 @@ import { CipherView } from 'jslib/models/view/cipherView';
import { FieldView } from 'jslib/models/view/fieldView'; import { FieldView } from 'jslib/models/view/fieldView';
import { FolderView } from 'jslib/models/view/folderView'; import { FolderView } from 'jslib/models/view/folderView';
import { IdentityView } from 'jslib/models/view/identityView'; import { IdentityView } from 'jslib/models/view/identityView';
import { LoginUriView } from 'jslib/models/view/loginUriView';
import { LoginView } from 'jslib/models/view/loginView'; import { LoginView } from 'jslib/models/view/loginView';
import { SecureNoteView } from 'jslib/models/view/secureNoteView'; import { SecureNoteView } from 'jslib/models/view/secureNoteView';
@ -59,6 +61,7 @@ export class AddEditComponent implements OnChanges {
cardExpMonthOptions: any[]; cardExpMonthOptions: any[];
identityTitleOptions: any[]; identityTitleOptions: any[];
addFieldTypeOptions: any[]; addFieldTypeOptions: any[];
uriMatchOptions: any[];
constructor(private cipherService: CipherService, private folderService: FolderService, constructor(private cipherService: CipherService, private folderService: FolderService,
private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
@ -109,6 +112,15 @@ export class AddEditComponent implements OnChanges {
{ name: i18nService.t('cfTypeHidden'), value: FieldType.Hidden }, { name: i18nService.t('cfTypeHidden'), value: FieldType.Hidden },
{ name: i18nService.t('cfTypeBoolean'), value: FieldType.Boolean }, { name: i18nService.t('cfTypeBoolean'), value: FieldType.Boolean },
]; ];
this.uriMatchOptions = [
{ name: i18nService.t('defaultAutofillDetection'), value: null },
{ name: i18nService.t('baseDomain'), value: UriMatchType.BaseDomain },
{ name: i18nService.t('fullHostname'), value: UriMatchType.FullHostname },
{ name: i18nService.t('startsWith'), value: UriMatchType.StartsWith },
{ name: i18nService.t('regEx'), value: UriMatchType.RegularExpression },
{ name: i18nService.t('exact'), value: UriMatchType.Exact },
{ name: i18nService.t('never'), value: UriMatchType.Never },
];
} }
async ngOnChanges() { async ngOnChanges() {
@ -125,6 +137,7 @@ export class AddEditComponent implements OnChanges {
this.cipher.folderId = this.folderId; this.cipher.folderId = this.folderId;
this.cipher.type = this.type == null ? CipherType.Login : this.type; this.cipher.type = this.type == null ? CipherType.Login : this.type;
this.cipher.login = new LoginView(); this.cipher.login = new LoginView();
this.cipher.login.uris = [new LoginUriView()];
this.cipher.card = new CardView(); this.cipher.card = new CardView();
this.cipher.identity = new IdentityView(); this.cipher.identity = new IdentityView();
this.cipher.secureNote = new SecureNoteView(); this.cipher.secureNote = new SecureNoteView();
@ -154,6 +167,29 @@ export class AddEditComponent implements OnChanges {
} catch { } } catch { }
} }
addUri() {
if (this.cipher.type !== CipherType.Login) {
return;
}
if (this.cipher.login.uris == null) {
this.cipher.login.uris = [];
}
this.cipher.login.uris.push(new LoginUriView());
}
removeUri(uri: LoginUriView) {
if (this.cipher.type !== CipherType.Login || this.cipher.login.uris == null) {
return;
}
const i = this.cipher.login.uris.indexOf(uri);
if (i > -1) {
this.cipher.login.uris.splice(i, 1);
}
}
addField() { addField() {
if (this.cipher.fields == null) { if (this.cipher.fields == null) {
this.cipher.fields = []; this.cipher.fields = [];

View File

@ -11,23 +11,6 @@
</div> </div>
<!-- Login --> <!-- Login -->
<div *ngIf="cipher.login"> <div *ngIf="cipher.login">
<div class="box-content-row box-content-row-flex" *ngIf="cipher.login.uri">
<div class="row-main">
<span class="row-label" *ngIf="!cipher.login.isWebsite">{{'uri' | i18n}}</span>
<span class="row-label" *ngIf="cipher.login.isWebsite">{{'website' | i18n}}</span>
<span title="{{cipher.login.uri}}">{{cipher.login.domainOrUri}}</span>
</div>
<div class="action-buttons">
<a class="row-btn" href="#" appStopClick title="{{'launch' | i18n}}"
*ngIf="cipher.login.canLaunch" (click)="launch()">
<i class="fa fa-lg fa-share-square-o"></i>
</a>
<a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}"
(click)="copy(cipher.login.uri, 'uri', 'URI')">
<i class="fa fa-lg fa-clipboard"></i>
</a>
</div>
</div>
<div class="box-content-row box-content-row-flex" *ngIf="cipher.login.username"> <div class="box-content-row box-content-row-flex" *ngIf="cipher.login.username">
<div class="row-main"> <div class="row-main">
<span class="row-label">{{'username' | i18n}}</span> <span class="row-label">{{'username' | i18n}}</span>
@ -177,6 +160,27 @@
</div> </div>
</div> </div>
</div> </div>
<div class="box" *ngIf="cipher.login && cipher.login.hasUris">
<div class="box-content">
<div class="box-content-row box-content-row-flex" *ngFor="let u of cipher.login.uris; let i = index">
<div class="row-main">
<span class="row-label" *ngIf="!u.isWebsite">{{'uri' | i18n}}</span>
<span class="row-label" *ngIf="u.isWebsite">{{'website' | i18n}}</span>
<span title="{{u.uri}}">{{u.domainOrUri}}</span>
</div>
<div class="action-buttons">
<a class="row-btn" href="#" appStopClick title="{{'launch' | i18n}}"
*ngIf="u.canLaunch" (click)="launch(u)">
<i class="fa fa-lg fa-share-square-o"></i>
</a>
<a class="row-btn" href="#" appStopClick title="{{'copyValue' | i18n}}"
(click)="copy(u.uri, u.isWebsite ? 'website' : 'uri', 'URI')">
<i class="fa fa-lg fa-clipboard"></i>
</a>
</div>
</div>
</div>
</div>
<div class="box" *ngIf="cipher.notes"> <div class="box" *ngIf="cipher.notes">
<div class="box-header"> <div class="box-header">
{{'notes' | i18n}} {{'notes' | i18n}}

View File

@ -26,6 +26,7 @@ import { TotpService } from 'jslib/abstractions/totp.service';
import { AttachmentView } from 'jslib/models/view/attachmentView'; 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'; import { FieldView } from 'jslib/models/view/fieldView';
import { LoginUriView } from 'jslib/models/view/loginUriView';
@Component({ @Component({
selector: 'app-vault-view', selector: 'app-vault-view',
@ -106,13 +107,13 @@ export class ViewComponent implements OnChanges, OnDestroy {
f.showValue = !f.showValue; f.showValue = !f.showValue;
} }
launch() { launch(uri: LoginUriView) {
if (!this.cipher.login.canLaunch) { if (!uri.canLaunch) {
return; return;
} }
this.analytics.eventTrack.next({ action: 'Launched Login URI' }); this.analytics.eventTrack.next({ action: 'Launched Login URI' });
this.platformUtilsService.launchUri(this.cipher.login.uri); this.platformUtilsService.launchUri(uri.uri);
} }
copy(value: string, typeI18nKey: string, aType: string) { copy(value: string, typeI18nKey: string, aType: string) {
@ -167,7 +168,10 @@ export class ViewComponent implements OnChanges, OnDestroy {
} }
private async totpUpdateCode() { private async totpUpdateCode() {
if (this.cipher.type !== CipherType.Login || this.cipher.login.totp == null) { if (this.cipher == null || this.cipher.type !== CipherType.Login || this.cipher.login.totp == null) {
if (this.totpInterval) {
clearInterval(this.totpInterval);
}
return; return;
} }

View File

@ -50,6 +50,18 @@
"uri": { "uri": {
"message": "URI" "message": "URI"
}, },
"uriPosition": {
"message": "URI $POSITION$",
"placeholders": {
"position": {
"content": "$1",
"example": "2"
}
}
},
"newUri": {
"message": "New URI"
},
"username": { "username": {
"message": "Username" "message": "Username"
}, },
@ -172,7 +184,8 @@
"message": "December" "message": "December"
}, },
"ex": { "ex": {
"message": "ex." "message": "ex.",
"description": "Short abbreviation for 'example'."
}, },
"title": { "title": {
"message": "Title" "message": "Title"
@ -969,5 +982,29 @@
}, },
"passwordSafe": { "passwordSafe": {
"message": "This password was not found in any known data breaches. It should be safe to use." "message": "This password was not found in any known data breaches. It should be safe to use."
},
"baseDomain": {
"message": "Base domain"
},
"fullHostname": {
"message": "Full hostname"
},
"exact": {
"message": "Exact"
},
"startsWith": {
"message": "Starts with"
},
"regEx": {
"message": "Regular expression",
"description": "A programming term, also known as 'RegEx'."
},
"autofillDetection": {
"message": "Auto-fill Detection",
"description": "URI auto-fill match detection."
},
"defaultAutofillDetection": {
"message": "Default auto-fill detection",
"description": "Default URI auto-fill match detection."
} }
} }

View File

@ -45,7 +45,7 @@
border-radius: $border-radius; border-radius: $border-radius;
} }
&:last-child:not(.box-content-row-cf) { &:last-child {
&:before { &:before {
border: none; border: none;
height: 0; height: 0;
@ -95,21 +95,25 @@
} }
&.box-content-row-flex, &.box-content-row-checkbox, &.box-content-row-input, &.box-content-row-flex, &.box-content-row-checkbox, &.box-content-row-input,
&.box-content-row-slider, &.box-content-row-cf { &.box-content-row-slider, &.box-content-row-multi {
display: flex; display: flex;
align-items: center; align-items: center;
word-break: break-word; word-break: break-word;
} }
&.box-content-row-cf { &.box-content-row-multi {
width: 100%; width: 100%;
input:not([type="checkbox"]) { input:not([type="checkbox"]) {
width: 100%; width: 100%;
} }
input + label.sr-only + select {
margin-top: 5px;
}
> a { > a {
padding: 8px 10px 8px 5px; padding: 8px 8px 8px 4px;
color: $brand-danger; color: $brand-danger;
margin: 0; margin: 0;
} }