mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-25 16:59:17 +01:00
add support for login uris
This commit is contained in:
parent
9b566e5990
commit
72771d4b90
@ -18,10 +18,6 @@
|
||||
</div>
|
||||
<!-- 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>
|
||||
<label for="loginUsername">{{'username' | i18n}}</label>
|
||||
<input id="loginUsername" type="text" name="Login.Username"
|
||||
@ -52,6 +48,11 @@
|
||||
</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">
|
||||
@ -177,13 +178,34 @@
|
||||
</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-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>
|
||||
<label for="folder">{{'folder' | i18n}}</label>
|
||||
<select id="folder" name="FolderId" [(ngModel)]="cipher.folderId">
|
||||
@ -216,12 +238,12 @@
|
||||
{{'customFields' | i18n}}
|
||||
</div>
|
||||
<div class="box-content">
|
||||
<div *ngIf="cipher.hasFields">
|
||||
<div class="box-content-row box-content-row-cf" appBoxRow
|
||||
<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-close fa-lg"></i>
|
||||
<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>
|
||||
@ -244,7 +266,7 @@
|
||||
</a>
|
||||
</div>
|
||||
</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}}
|
||||
|
@ -14,6 +14,7 @@ import { Angulartics2 } from 'angulartics2';
|
||||
import { CipherType } from 'jslib/enums/cipherType';
|
||||
import { FieldType } from 'jslib/enums/fieldType';
|
||||
import { SecureNoteType } from 'jslib/enums/secureNoteType';
|
||||
import { UriMatchType } from 'jslib/enums/uriMatchType';
|
||||
|
||||
import { AuditService } from 'jslib/abstractions/audit.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 { FolderView } from 'jslib/models/view/folderView';
|
||||
import { IdentityView } from 'jslib/models/view/identityView';
|
||||
import { LoginUriView } from 'jslib/models/view/loginUriView';
|
||||
import { LoginView } from 'jslib/models/view/loginView';
|
||||
import { SecureNoteView } from 'jslib/models/view/secureNoteView';
|
||||
|
||||
@ -59,6 +61,7 @@ export class AddEditComponent implements OnChanges {
|
||||
cardExpMonthOptions: any[];
|
||||
identityTitleOptions: any[];
|
||||
addFieldTypeOptions: any[];
|
||||
uriMatchOptions: any[];
|
||||
|
||||
constructor(private cipherService: CipherService, private folderService: FolderService,
|
||||
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('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() {
|
||||
@ -125,6 +137,7 @@ export class AddEditComponent implements OnChanges {
|
||||
this.cipher.folderId = this.folderId;
|
||||
this.cipher.type = this.type == null ? CipherType.Login : this.type;
|
||||
this.cipher.login = new LoginView();
|
||||
this.cipher.login.uris = [new LoginUriView()];
|
||||
this.cipher.card = new CardView();
|
||||
this.cipher.identity = new IdentityView();
|
||||
this.cipher.secureNote = new SecureNoteView();
|
||||
@ -154,6 +167,29 @@ export class AddEditComponent implements OnChanges {
|
||||
} 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() {
|
||||
if (this.cipher.fields == null) {
|
||||
this.cipher.fields = [];
|
||||
|
@ -11,23 +11,6 @@
|
||||
</div>
|
||||
<!-- 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="row-main">
|
||||
<span class="row-label">{{'username' | i18n}}</span>
|
||||
@ -177,6 +160,27 @@
|
||||
</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-header">
|
||||
{{'notes' | i18n}}
|
||||
|
@ -26,6 +26,7 @@ import { TotpService } from 'jslib/abstractions/totp.service';
|
||||
import { AttachmentView } from 'jslib/models/view/attachmentView';
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
import { FieldView } from 'jslib/models/view/fieldView';
|
||||
import { LoginUriView } from 'jslib/models/view/loginUriView';
|
||||
|
||||
@Component({
|
||||
selector: 'app-vault-view',
|
||||
@ -106,13 +107,13 @@ export class ViewComponent implements OnChanges, OnDestroy {
|
||||
f.showValue = !f.showValue;
|
||||
}
|
||||
|
||||
launch() {
|
||||
if (!this.cipher.login.canLaunch) {
|
||||
launch(uri: LoginUriView) {
|
||||
if (!uri.canLaunch) {
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -167,7 +168,10 @@ export class ViewComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,18 @@
|
||||
"uri": {
|
||||
"message": "URI"
|
||||
},
|
||||
"uriPosition": {
|
||||
"message": "URI $POSITION$",
|
||||
"placeholders": {
|
||||
"position": {
|
||||
"content": "$1",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newUri": {
|
||||
"message": "New URI"
|
||||
},
|
||||
"username": {
|
||||
"message": "Username"
|
||||
},
|
||||
@ -172,7 +184,8 @@
|
||||
"message": "December"
|
||||
},
|
||||
"ex": {
|
||||
"message": "ex."
|
||||
"message": "ex.",
|
||||
"description": "Short abbreviation for 'example'."
|
||||
},
|
||||
"title": {
|
||||
"message": "Title"
|
||||
@ -969,5 +982,29 @@
|
||||
},
|
||||
"passwordSafe": {
|
||||
"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;
|
||||
}
|
||||
|
||||
&:last-child:not(.box-content-row-cf) {
|
||||
&:last-child {
|
||||
&:before {
|
||||
border: none;
|
||||
height: 0;
|
||||
@ -95,21 +95,25 @@
|
||||
}
|
||||
|
||||
&.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;
|
||||
align-items: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&.box-content-row-cf {
|
||||
&.box-content-row-multi {
|
||||
width: 100%;
|
||||
|
||||
input:not([type="checkbox"]) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input + label.sr-only + select {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
> a {
|
||||
padding: 8px 10px 8px 5px;
|
||||
padding: 8px 8px 8px 4px;
|
||||
color: $brand-danger;
|
||||
margin: 0;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user