add support for add/edit/view of login uris

This commit is contained in:
Kyle Spearrin 2018-03-02 15:37:49 -05:00
parent d67b95421c
commit f952cd5642
9 changed files with 276 additions and 64 deletions

2
jslib

@ -1 +1 @@
Subproject commit 0d80216921efa53dfc095fd25dde9b370a070da8
Subproject commit 848f50afe7b9dc9e193a036c062ef2f0e4d93018

View File

@ -455,6 +455,9 @@
"uri": {
"message": "URI"
},
"newUri": {
"message": "New URI"
},
"addedItem": {
"message": "Added item"
},
@ -989,5 +992,33 @@
},
"passwordSafe": {
"message": "This password was not found in any known data breaches. It should be safe to use."
},
"baseDomain": {
"message": "Base domain"
},
"host": {
"message": "Host",
"description": "A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'."
},
"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."
},
"toggleOptions": {
"message": "Toggle Options"
}
}

View File

@ -14,7 +14,12 @@ angular
folderId: folderId,
name: $stateParams.name,
type: constantsService.cipherType.login,
login: {},
login: {
uris: [{
uri: null,
match: null
}]
},
identity: {},
card: {},
secureNote: {
@ -23,13 +28,15 @@ angular
};
if ($stateParams.uri) {
$scope.cipher.login.uri = $stateParams.uri;
$scope.cipher.login.uris[0].uri = $stateParams.uri;
}
if ($stateParams.cipher) {
angular.extend($scope.cipher, $stateParams.cipher);
}
setUriMatchValues();
$timeout(function () {
popupUtilsService.initListSectionItemListeners(document, angular);
@ -109,6 +116,50 @@ angular
});
};
$scope.addUri = function () {
if (!$scope.cipher.login) {
return;
}
if (!$scope.cipher.login.uris) {
$scope.cipher.login.uris = [];
}
$scope.cipher.login.uris.push({
uri: null,
match: null,
});
$timeout(function () {
popupUtilsService.initListSectionItemListeners(document, angular);
}, 500);
};
$scope.removeUri = function (uri) {
if (!$scope.cipher.login || !$scope.cipher.login.uris) {
return;
}
var index = $scope.cipher.login.uris.indexOf(uri);
if (index > -1) {
$scope.cipher.login.uris.splice(index, 1);
}
};
$scope.uriMatchChanged = function (uri) {
uri.showOptions = uri.showOptions == null ? true : uri.showOptions;
if ((!uri.matchValue && uri.matchValue !== 0) || uri.matchValue === '') {
uri.match = null;
}
else {
uri.match = parseInt(uri.matchValue);
}
};
$scope.toggleUriOptions = function (u) {
u.showOptions = u.showOptions == null && u.match != null ? false : !u.showOptions;
};
$scope.addField = function (type) {
if (!$scope.cipher.fields) {
$scope.cipher.fields = [];
@ -142,4 +193,14 @@ angular
}
});
};
function setUriMatchValues() {
if ($scope.cipher.login && $scope.cipher.login.uris) {
for (var i = 0; i < $scope.cipher.login.uris.length; i++) {
$scope.cipher.login.uris[i].matchValue =
$scope.cipher.login.uris[i].match || $scope.cipher.login.uris[i].match === 0 ?
$scope.cipher.login.uris[i].match.toString() : '';
}
}
}
});

View File

@ -24,12 +24,14 @@ angular
if ($stateParams.cipher) {
angular.extend($scope.cipher, $stateParams.cipher);
setUriMatchValues();
}
else {
cipherService.get(cipherId).then(function (cipher) {
return cipher.decrypt();
}).then(function (model) {
$scope.cipher = model;
setUriMatchValues();
});
}
@ -127,6 +129,50 @@ angular
});
};
$scope.addUri = function () {
if (!$scope.cipher.login) {
return;
}
if (!$scope.cipher.login.uris) {
$scope.cipher.login.uris = [];
}
$scope.cipher.login.uris.push({
uri: null,
match: null,
});
$timeout(function () {
popupUtilsService.initListSectionItemListeners(document, angular);
}, 500);
};
$scope.removeUri = function (uri) {
if (!$scope.cipher.login || !$scope.cipher.login.uris) {
return;
}
var index = $scope.cipher.login.uris.indexOf(uri);
if (index > -1) {
$scope.cipher.login.uris.splice(index, 1);
}
};
$scope.uriMatchChanged = function (uri) {
uri.showOptions = uri.showOptions == null ? true : uri.showOptions;
if ((!uri.matchValue && uri.matchValue !== 0) || uri.matchValue === '') {
uri.match = null;
}
else {
uri.match = parseInt(uri.matchValue);
}
};
$scope.toggleUriOptions = function (u) {
u.showOptions = u.showOptions == null && u.match != null ? false : !u.showOptions;
};
$scope.addField = function (type) {
if (!$scope.cipher.fields) {
$scope.cipher.fields = [];
@ -182,4 +228,14 @@ angular
}
});
}
function setUriMatchValues() {
if ($scope.cipher.login && $scope.cipher.login.uris) {
for (var i = 0; i < $scope.cipher.login.uris.length; i++) {
$scope.cipher.login.uris[i].matchValue =
$scope.cipher.login.uris[i].match || $scope.cipher.login.uris[i].match === 0 ?
$scope.cipher.login.uris[i].match.toString() : '';
}
}
}
});

View File

@ -28,20 +28,6 @@ angular
if (model.login.password) {
$scope.cipher.maskedPassword = $scope.maskValue(model.login.password);
}
if (model.login.uri) {
$scope.cipher.showLaunch = model.login.uri.startsWith('http://') || model.login.uri.startsWith('https://');
var domain = platformUtilsService.getDomain(model.login.uri);
if (domain) {
$scope.cipher.login.website = domain;
}
else {
$scope.cipher.login.website = model.login.uri;
}
}
else {
$scope.cipher.showLaunch = false;
}
}
if (model.login && model.login.totp && (cipherObj.organizationUseTotp || tokenService.getPremium())) {
@ -90,11 +76,13 @@ angular
}
};
$scope.launchWebsite = function (cipher) {
if (cipher.showLaunch) {
$analytics.eventTrack('Launched Website');
BrowserApi.createNewTab(cipher.login.uri);
$scope.launch = function (uri) {
if (!uri.canLaunch) {
return;
}
$analytics.eventTrack('Launched Login URI');
BrowserApi.createNewTab(uri.uri);
};
$scope.clipboardError = function (e, password) {

View File

@ -30,10 +30,6 @@
<input id="name" type="text" name="Name" ng-model="cipher.name">
</div>
<div ng-if="cipher.type === constants.cipherType.login">
<div class="list-section-item">
<label for="loginUri" class="item-label">{{i18n.uri}}</label>
<input id="loginUri" type="text" name="Login.Uri" ng-model="cipher.login.uri">
</div>
<div class="list-section-item">
<label for="loginUsername" class="item-label">{{i18n.username}}</label>
<input id="loginUsername" type="text" name="Login.Username" ng-model="cipher.login.username">
@ -50,12 +46,15 @@
<a class="btn-list" href="" title="{{i18n.togglePassword}}" ng-click="togglePassword()">
<i class="fa fa-lg" ng-class="[{'fa-eye': !showPassword}, {'fa-eye-slash': showPassword}]"></i>
</a>
<a class="btn-list" href="" title="{{i18n.generatePassword}}" ng-click="generatePassword()">
<i class="fa fa-lg fa-refresh"></i>
</a>
</div>
</div>
<a class="list-section-item" href="" ng-click="generatePassword()">
{{i18n.generatePassword}}
<i class="fa fa-chevron-right"></i>
</a>
<div class="list-section-item">
<label for="loginTotp" class="item-label">{{i18n.authenticatorKeyTotp}}</label>
<input id="loginTotp" type="text" name="Login.Totp" ng-model="cipher.login.totp">
</div>
</div>
<div ng-if="cipher.type === constants.cipherType.card">
<div class="list-section-item">
@ -202,12 +201,44 @@
</div>
</div>
</div>
<div class="list-section" ng-if="cipher.type === constants.cipherType.login">
<div class="list-section-items">
<div class="list-section-item list-section-item-table"
ng-if="cipher.login.uris && cipher.login.uris.length" ng-repeat="u in cipher.login.uris">
<a href="#" stop-click ng-click="removeUri(u)" class="action-button text-danger">
<i class="fa fa-minus-circle fa-lg"></i>
</a>
<div class="action-button-content">
<label for="loginUri{{$index}}" class="item-label">{{i18n.uri}} {{$index + 1}}</label>
<input id="loginUri{{$index}}" type="text" name="Login.Uris[{{$index}}].Uri"
ng-model="u.uri" placeholder="{{i18n.ex}} https://google.com">
<label for="loginUriMatch{{$index}}" class="sr-only">
{{i18n.autofillDetection}} {{$index + 1}}
</label>
<select id="loginUriMatch{{$index}}" name="Login.Uris[{{$index}}].Match"
ng-hide="u.showOptions === false || (u.showOptions == null && u.match == null)"
ng-model="u.matchValue" ng-change="uriMatchChanged(u)">
<option value="">{{i18n.defaultAutofillDetection}}</option>
<option value="0">{{i18n.baseDomain}}</option>
<option value="1">{{i18n.host}}</option>
<option value="2">{{i18n.startsWith}}</option>
<option value="4">{{i18n.regEx}}</option>
<option value="3">{{i18n.exact}}</option>
<option value="5">{{i18n.never}}</option>
</select>
</div>
<a href="#" stop-click ng-click="toggleUriOptions(u)" class="action-button"
title="{{i18n.toggleOptions}}">
<i class="fa fa-cog fa-lg"></i>
</a>
</div>
<a class="list-section-item text-primary" href="#" stop-click ng-click="addUri()">
<i class="fa fa-plus-circle fa-fw fa-lg"></i> {{i18n.newUri}}
</a>
</div>
</div>
<div class="list-section">
<div class="list-section-items">
<div class="list-section-item" ng-if="cipher.type === constants.cipherType.login">
<label for="loginTotp" class="item-label">{{i18n.authenticatorKeyTotp}}</label>
<input id="loginTotp" type="text" name="Login.Totp" ng-model="cipher.login.totp">
</div>
<div class="list-section-item">
<label for="folder" class="item-label">{{i18n.folder}}</label>
<select id="folder" name="FolderId" ng-model="cipher.folderId">
@ -241,7 +272,7 @@
ng-if="cipher.fields && cipher.fields.length" ng-repeat="field in cipher.fields"
ng-class="{'list-section-item-checkbox' : field.type === constants.fieldType.boolean}">
<a href="#" stop-click ng-click="removeField(field)" class="action-button text-danger">
<i class="fa fa-close fa-lg"></i>
<i class="fa fa-minus-circle fa-lg"></i>
</a>
<div class="action-button-content">
<input id="field_name{{$index}}" type="text" name="Field.Name{{$index}}"

View File

@ -23,10 +23,6 @@
</div>
<div ng-if="cipher.type === constants.cipherType.login">
<div class="list-section-item">
<label for="loginUri" class="item-label">{{i18n.uri}}</label>
<input id="loginUri" type="text" name="Login.Uri" ng-model="cipher.login.uri">
</div>
<div class="list-section-item">
<label for="loginUsername" class="item-label">{{i18n.username}}</label>
<input id="loginUsername" type="text" name="Login.Username" ng-model="cipher.login.username">
@ -43,12 +39,15 @@
<a class="btn-list" href="" title="{{i18n.togglePassword}}" ng-click="togglePassword()">
<i class="fa fa-lg" ng-class="[{'fa-eye': !showPassword}, {'fa-eye-slash': showPassword}]"></i>
</a>
<a class="btn-list" href="" title="{{i18n.generatePassword}}" ng-click="generatePassword()">
<i class="fa fa-lg fa-refresh"></i>
</a>
</div>
</div>
<a class="list-section-item" href="" ng-click="generatePassword()">
{{i18n.generatePassword}}
<i class="fa fa-chevron-right"></i>
</a>
<div class="list-section-item">
<label for="loginTotp" class="item-label">{{i18n.authenticatorKeyTotp}}</label>
<input id="loginTotp" type="text" name="Login.Totp" ng-model="cipher.login.totp">
</div>
</div>
<div ng-if="cipher.type === constants.cipherType.card">
<div class="list-section-item">
@ -195,12 +194,44 @@
</div>
</div>
</div>
<div class="list-section" ng-if="cipher.type === constants.cipherType.login">
<div class="list-section-items">
<div class="list-section-item list-section-item-table"
ng-if="cipher.login.uris && cipher.login.uris.length" ng-repeat="u in cipher.login.uris">
<a href="#" stop-click ng-click="removeUri(u)" class="action-button text-danger">
<i class="fa fa-minus-circle fa-lg"></i>
</a>
<div class="action-button-content">
<label for="loginUri{{$index}}" class="item-label">{{i18n.uri}} {{$index + 1}}</label>
<input id="loginUri{{$index}}" type="text" name="Login.Uris[{{$index}}].Uri"
ng-model="u.uri" placeholder="{{i18n.ex}} https://google.com">
<label for="loginUriMatch{{$index}}" class="sr-only">
{{i18n.autofillDetection}} {{$index + 1}}
</label>
<select id="loginUriMatch{{$index}}" name="Login.Uris[{{$index}}].Match"
ng-hide="u.showOptions === false || (u.showOptions == null && u.match == null)"
ng-model="u.matchValue" ng-change="uriMatchChanged(u)">
<option value="">{{i18n.defaultAutofillDetection}}</option>
<option value="0">{{i18n.baseDomain}}</option>
<option value="1">{{i18n.host}}</option>
<option value="2">{{i18n.startsWith}}</option>
<option value="4">{{i18n.regEx}}</option>
<option value="3">{{i18n.exact}}</option>
<option value="5">{{i18n.never}}</option>
</select>
</div>
<a href="#" stop-click ng-click="toggleUriOptions(u)" class="action-button"
title="{{i18n.toggleOptions}}">
<i class="fa fa-cog fa-lg"></i>
</a>
</div>
<a class="list-section-item text-primary" href="#" stop-click ng-click="addUri()">
<i class="fa fa-plus-circle fa-fw fa-lg"></i> {{i18n.newUri}}
</a>
</div>
</div>
<div class="list-section">
<div class="list-section-items">
<div class="list-section-item" ng-if="cipher.type === constants.cipherType.login">
<label for="loginTotp" class="item-label">{{i18n.authenticatorKeyTotp}}</label>
<input id="loginTotp" type="text" name="Login.Totp" ng-model="cipher.login.totp">
</div>
<div class="list-section-item">
<label for="folder" class="item-label">{{i18n.folder}}</label>
<select id="folder" name="FolderId" ng-model="cipher.folderId">
@ -238,7 +269,7 @@
ng-if="cipher.fields && cipher.fields.length" ng-repeat="field in cipher.fields"
ng-class="{'list-section-item-checkbox' : field.type === constants.fieldType.boolean}">
<a href="#" stop-click ng-click="removeField(field)" class="action-button text-danger">
<i class="fa fa-close fa-lg"></i>
<i class="fa fa-minus-circle fa-lg"></i>
</a>
<div class="action-button-content">
<input id="field_name{{$index}}" type="text" name="Field.Name{{$index}}"

View File

@ -20,22 +20,6 @@
{{cipher.name}}
</div>
<div ng-if="cipher.type === constants.cipherType.login">
<div class="list-section-item wrap" ng-if="cipher.login.uri" title="{{cipher.login.uri}}">
<div class="action-buttons">
<a class="btn-list" href="" title="{{i18n.launchWebsite}}" ng-click="launchWebsite(cipher)"
ng-show="cipher.showLaunch">
<i class="fa fa-lg fa-share-square-o"></i>
</a>
<a class="btn-list" href="" title="{{i18n.copyUri}}"
ngclipboard ngclipboard-error="clipboardError(e)"
ngclipboard-success="clipboardSuccess(e, i18n.uri, 'URI')"
data-clipboard-text="{{cipher.login.uri}}">
<i class="fa fa-lg fa-clipboard"></i>
</a>
</div>
<span class="item-label">{{i18n.website}}</span>
{{cipher.login.website}}
</div>
<div class="list-section-item wrap" ng-if="cipher.login.username">
<div class="action-buttons">
<a class="btn-list" href="" title="{{i18n.copyUsername}}"
@ -183,6 +167,28 @@
</div>
</div>
</div>
<div class="list-section"
ng-if="cipher.type === constants.cipherType.login && cipher.login.uris && cipher.login.uris.length">
<div class="list-section-items">
<div class="list-section-item wrap" title="{{u.uri}}" ng-repeat="u in cipher.login.uris">
<div class="action-buttons">
<a class="btn-list" href="" title="{{i18n.launchWebsite}}" ng-click="launch(u)"
ng-show="u.canLaunch">
<i class="fa fa-lg fa-share-square-o"></i>
</a>
<a class="btn-list" href="" title="{{i18n.copyUri}}"
ngclipboard ngclipboard-error="clipboardError(e)"
ngclipboard-success="clipboardSuccess(e, i18n.uri, 'URI')"
data-clipboard-text="{{u.uri}}">
<i class="fa fa-lg fa-clipboard"></i>
</a>
</div>
<span class="item-label" ng-if="u.isWebsite">{{i18n.website}}</span>
<span class="item-label" ng-if="!u.isWebsite">{{i18n.uri}}</span>
{{u.domainOrUri}}
</div>
</div>
</div>
<div class="list-section" ng-if="cipher.notes">
<div class="list-section-header">
{{i18n.notes}}

View File

@ -329,7 +329,7 @@
}
.action-button {
padding: 8px 10px 8px 5px;
padding: 8px 8px 8px 4px;
display: table-cell;
width: 20px;
vertical-align: middle;
@ -342,6 +342,14 @@
.action-button-content {
display: table-cell;
vertical-align: middle;
input + label.sr-only + select {
margin-top: 5px;
}
}
.action-button-content + .action-button {
padding: 8px 0 8px 14px;
}
.field-type {