add support for login uris
This commit is contained in:
parent
9b566e5990
commit
72771d4b90
|
@ -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}}
|
||||||
|
|
|
@ -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 = [];
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue