1
0
mirror of https://github.com/bitwarden/web synced 2025-12-06 00:03:28 +00:00

Compare commits

...

31 Commits

Author SHA1 Message Date
Kyle Spearrin
4b583bea9b version bump - 1.4.0 2016-11-09 21:50:04 -05:00
Kyle Spearrin
3f0ca412c6 index checking 2016-11-09 21:49:15 -05:00
Kyle Spearrin
0050b570b4 Added delete option to edit site modal #25 2016-11-09 19:26:02 -05:00
Kyle Spearrin
9405be03b0 Added importer for universal password manager csv format 2016-11-09 18:18:46 -05:00
Kyle Spearrin
b5b706fe06 check badDataLength 2016-11-09 00:42:19 -05:00
Kyle Spearrin
8313d9fa90 Better error handling for imports. Check for bad CSV data on lastpass (when no header row is included). 2016-11-09 00:41:17 -05:00
Kyle Spearrin
93b96b3be7 adjusted add/edit site sort functions for folders 2016-10-22 09:08:17 -04:00
blindly
50e6818f2b Sdd site button to each folder (#17) 2016-10-22 08:43:50 -04:00
Kyle Spearrin
104fb57bd8 Added support for Firefox Password Exporter Addon import. ref #1 2016-10-21 00:10:17 -04:00
Kyle Spearrin
7ba6b2f00a version bump 2016-10-20 08:04:40 -04:00
Kyle Spearrin
d8e8939eca remove old code from 1Password 1pif importer 2016-10-20 08:03:20 -04:00
Kyle Spearrin
26c5f4049d link to help site 2016-10-19 22:52:53 -04:00
Kyle Spearrin
b6c9dba0fc Added Chrome csv importer. Adjustments to 1Password importer in preparation for detecting different formats. 2016-10-19 18:22:18 -04:00
blindly
8badc1a354 Folders should be sorted in alphabetical order when adding or editing a site. 2016-10-19 06:07:10 -04:00
Kyle Spearrin
15097eb1f0 version bump 2016-10-18 22:11:54 -04:00
Kyle Spearrin
986306f811 fix imports error 2016-10-18 21:34:08 -04:00
Kyle Spearrin
c5de290dd4 version bump 2016-10-17 23:26:20 -04:00
Kyle Spearrin
21e9e083f5 nothing was imported error 2016-10-17 23:24:16 -04:00
Kyle Spearrin
517ea65bc9 import validation 2016-10-17 23:18:19 -04:00
Kyle Spearrin
e7a9699226 show import errors as console warning to assist in debugging issues 2016-10-17 23:11:39 -04:00
Kyle Spearrin
52b3dfd0e3 set encoding for to UTF-8 for csv parser 2016-10-17 23:00:44 -04:00
Kyle Spearrin
4c0bde9d87 1.2.0 version bump 2016-10-15 12:30:20 -04:00
Kyle Spearrin
f29bbe4316 adjust password generator to use cryptographically secure RNG 2016-10-15 00:41:03 -04:00
Kyle Spearrin
3c03ed8636 trim url for safeincloud xml import 2016-10-14 23:55:02 -04:00
Kyle Spearrin
71ca4eb84a fix safeincloudcsv value in select option 2016-10-14 23:52:31 -04:00
Kyle Spearrin
30d19b4ee1 import for safeincloud xml 2016-10-14 23:50:39 -04:00
Kyle Spearrin
7b88d30aa8 lowered uri trim to 1000 2016-10-14 23:17:20 -04:00
Kyle Spearrin
0ae11cc40c import for 1password 1pif files 2016-10-14 23:16:37 -04:00
Kyle Spearrin
dd5cda867d trim ridiculously large URLs on import 2016-10-14 21:43:00 -04:00
Kyle Spearrin
2d11bef262 padlock csv import 2016-10-13 23:10:14 -04:00
Kyle Spearrin
5d5d0bfb66 gitter badge 2016-10-13 22:02:46 -04:00
15 changed files with 625 additions and 31 deletions

View File

@@ -1,3 +1,5 @@
[![Join the chat at https://gitter.im/bitwarden/Lobby](https://badges.gitter.im/bitwarden/Lobby.svg)](https://gitter.im/bitwarden/Lobby)
# bitwarden Web
The bitwarden Web project is an AngularJS application that powers the web vault (https://vault.bitwarden.com/).

View File

@@ -1,5 +1,5 @@
{
"version": "1.1.1",
"version": "1.4.0",
"environment": "Development",
"dependencies": {

View File

@@ -58,8 +58,9 @@
/// <reference path="lib/angular-resource/angular-resource.js" />
/// <reference path="lib/angulartics/angulartics.js" />
/// <reference path="lib/angulartics/angulartics-ga.js" />
/// <reference path="lib/angular-toastr/angular-toastr.js" />
/// <reference path="lib/angular-toastr/angular-toastr.min.js" />
/// <reference path="lib/angular-toastr/angular-toastr.tpls.js" />
/// <reference path="lib/angular-toastr/angular-toastr.tpls.min.js" />
/// <reference path="lib/angular-ui-router/angular-ui-router.js" />
/// <reference path="lib/bootstrap/js/bootstrap.min.js" />
/// <reference path="lib/clipboard/clipboard.js" />
@@ -69,3 +70,4 @@
/// <reference path="lib/papaparse/papaparse.js" />
/// <reference path="lib/sjcl/bitArray.js" />
/// <reference path="lib/sjcl/cbc.js" />
/// <reference path="lib/sjcl/sjcl.js" />

View File

@@ -5,6 +5,11 @@
var _service = {};
_service.import = function (source, file, success, error) {
if (!file) {
error();
return;
}
switch (source) {
case 'local':
importLocal(file, success, error);
@@ -15,19 +20,56 @@
case 'safeincloudcsv':
importSafeInCloudCsv(file, success, error);
break;
case 'safeincloudxml':
importSafeInCloudXml(file, success, error);
break;
case 'keypassxml':
importKeyPassXml(file, success, error);
break;
case 'padlockcsv':
importPadlockCsv(file, success, error);
break;
case '1password1pif':
import1Password1Pif(file, success, error);
break;
case 'chromecsv':
importChromeCsv(file, success, error);
break;
case 'firefoxpasswordexportercsvxml':
importFirefoxPasswordExporterCsvXml(file, success, error);
break;
case 'upmcsv':
importUpmCsv(file, success, error);
break;
default:
error();
break;
}
};
function trimUri(uri) {
if (uri.length > 1000) {
return uri.substring(0, 1000);
}
return uri;
}
function parseCsvErrors(results) {
if (results.errors && results.errors.length) {
for (var i = 0; i < results.errors.length; i++) {
console.warn('Error parsing row ' + results.errors[i].row + ': ' + results.errors[i].message);
}
}
}
function importLocal(file, success, error) {
Papa.parse(file, {
header: true,
encoding: 'UTF-8',
complete: function (results) {
parseCsvErrors(results);
var folders = [],
sites = [],
folderRelationships = [];
@@ -50,7 +92,7 @@
sites.push({
favorite: value.favorite !== null ? value.favorite : false,
uri: value.uri && value.uri !== '' ? value.uri : null,
uri: value.uri && value.uri !== '' ? trimUri(value.uri) : null,
username: value.username && value.username !== '' ? value.username : null,
password: value.password && value.password !== '' ? value.password : null,
notes: value.notes && value.notes !== '' ? value.notes : null,
@@ -88,16 +130,22 @@
if (pre.length === 1) {
csv = pre.text().trim();
results = Papa.parse(csv, { header: true });
results = Papa.parse(csv, {
header: true,
encoding: 'UTF-8'
});
parseData(results.data);
}
else {
var foundPre = false;
for (var i = 0; i < doc.length; i++) {
if (doc[i].tagName === 'PRE') {
if (doc[i].tagName.toLowerCase() === 'pre') {
foundPre = true;
csv = doc[i].outerText.trim();
results = Papa.parse(csv, { header: true });
results = Papa.parse(csv, {
header: true,
encoding: 'UTF-8'
});
parseData(results.data);
break;
}
@@ -116,7 +164,9 @@
else {
Papa.parse(file, {
header: true,
encoding: 'UTF-8',
complete: function (results) {
parseCsvErrors(results);
parseData(results.data);
}
});
@@ -125,7 +175,8 @@
function parseData(data) {
var folders = [],
sites = [],
siteRelationships = [];
siteRelationships = [],
badDataSites = 0;
angular.forEach(data, function (value, key) {
var folderIndex = folders.length,
@@ -143,9 +194,13 @@
}
}
if ((!value.name || value.name === '') && (!value.password || value.password === '')) {
badDataSites++;
}
sites.push({
favorite: value.fav === '1',
uri: value.url && value.url !== '' ? value.url : null,
uri: value.url && value.url !== '' ? trimUri(value.url) : null,
username: value.username && value.username !== '' ? value.username : null,
password: value.password && value.password !== '' ? value.password : null,
notes: value.extra && value.extra !== '' ? value.extra : null,
@@ -167,14 +222,22 @@
}
});
success(folders, sites, siteRelationships);
if (badDataSites && badDataSites > (data.length / 2)) {
error('CSV data is not formatted correctly from LastPass. Please check your import file and try again.');
}
else {
success(folders, sites, siteRelationships);
}
}
}
function importSafeInCloudCsv(file, success, error) {
Papa.parse(file, {
header: true,
encoding: 'UTF-8',
complete: function (results) {
parseCsvErrors(results);
var folders = [],
sites = [],
siteRelationships = [];
@@ -182,7 +245,7 @@
angular.forEach(results.data, function (value, key) {
sites.push({
favorite: false,
uri: value.URL && value.URL !== '' ? value.URL : null,
uri: value.URL && value.URL !== '' ? trimUri(value.URL) : null,
username: value.Login && value.Login !== '' ? value.Login : null,
password: value.Password && value.Password !== '' ? value.Password : null,
notes: value.Notes && value.Notes !== '' ? value.Notes : null,
@@ -195,6 +258,196 @@
});
}
function importSafeInCloudXml(file, success, error) {
var folders = [],
sites = [],
siteRelationships = [],
foldersIndex = [];
var i = 0,
j = 0;
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
var xmlDoc = $.parseXML(evt.target.result),
xml = $(xmlDoc);
var db = xml.find('database');
if (db.length) {
var labels = db.find('> label');
if (labels.length) {
for (i = 0; i < labels.length; i++) {
var label = $(labels[i]);
foldersIndex[label.attr('id')] = folders.length;
folders.push({
name: label.attr('name')
});
}
}
var cards = db.find('> card');
if (cards.length) {
for (i = 0; i < cards.length; i++) {
var card = $(cards[i]);
if (card.attr('template') === 'true') {
continue;
}
var site = {
favorite: false,
uri: null,
username: null,
password: null,
notes: null,
name: card.attr('title'),
};
var fields = card.find('> field');
for (j = 0; j < fields.length; j++) {
var field = $(fields[j]);
var text = field.text();
var type = field.attr('type');
if (text && text !== '') {
if (type === 'login') {
site.username = text;
}
else if (type === 'password') {
site.password = text;
}
else if (type === 'notes') {
site.notes = text;
}
else if (type === 'website') {
site.uri = trimUri(text);
}
}
}
sites.push(site);
labels = card.find('> label_id');
if (labels.length) {
var labelId = $(labels[0]).text();
var folderIndex = foldersIndex[labelId];
if (labelId !== null && labelId !== '' && folderIndex !== null) {
siteRelationships.push({
key: sites.length - 1,
value: folderIndex
});
}
}
}
}
success(folders, sites, siteRelationships);
}
else {
error();
}
};
reader.onerror = function (evt) {
error();
};
}
function importPadlockCsv(file, success, error) {
Papa.parse(file, {
encoding: 'UTF-8',
complete: function (results) {
parseCsvErrors(results);
var folders = [],
sites = [],
folderRelationships = [];
var customFieldHeaders = [];
// CSV index ref: 0 = name, 1 = category, 2 = username, 3 = password, 4+ = custom fields
var i = 0,
j = 0;
for (i = 0; i < results.data.length; i++) {
var value = results.data[i];
if (i === 0) {
// header row
for (j = 4; j < value.length; j++) {
customFieldHeaders.push(value[j]);
}
continue;
}
var folderIndex = folders.length,
siteIndex = sites.length,
hasFolder = value[1] && value[1] !== '',
addFolder = hasFolder;
if (hasFolder) {
for (j = 0; j < folders.length; j++) {
if (folders[j].name === value[1]) {
addFolder = false;
folderIndex = j;
break;
}
}
}
var site = {
favorite: false,
uri: null,
username: value[2] && value[2] !== '' ? value[2] : null,
password: value[3] && value[3] !== '' ? value[3] : null,
notes: null,
name: value[0] && value[0] !== '' ? value[0] : '--',
};
if (customFieldHeaders.length) {
for (j = 4; j < value.length; j++) {
var cf = value[j];
if (!cf || cf === '') {
continue;
}
var cfHeader = customFieldHeaders[j - 4];
if (cfHeader.toLowerCase() === 'url' || cfHeader.toLowerCase() === 'uri') {
site.uri = trimUri(cf);
}
else {
if (site.notes === null) {
site.notes = '';
}
site.notes += cfHeader + ': ' + cf + '\n';
}
}
}
sites.push(site);
if (addFolder) {
folders.push({
name: value[1]
});
}
if (hasFolder) {
folderRelationships.push({
key: siteIndex,
value: folderIndex
});
}
}
success(folders, sites, folderRelationships);
}
});
}
function importKeyPassXml(file, success, error) {
var folders = [],
sites = [],
@@ -264,7 +517,7 @@
switch (key) {
case 'URL':
site.uri = value;
site.uri = trimUri(value);
break;
case 'UserName':
site.username = value;
@@ -310,5 +563,222 @@
}
}
function import1Password1Pif(file, success, error) {
var folders = [],
sites = [],
siteRelationships = [];
var i = 0,
j = 0;
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
var fileContent = evt.target.result;
var fileLines = fileContent.split(/(?:\r\n|\r|\n)/);
for (i = 0; i < fileLines.length; i++) {
var line = fileLines[i];
if (!line.length || line[0] !== '{') {
continue;
}
var item = JSON.parse(line);
if (item.typeName !== 'webforms.WebForm') {
continue;
}
var site = {
favorite: item.openContents && item.openContents.faveIndex ? true : false,
uri: item.location && item.location !== '' ? trimUri(item.location) : null,
username: null,
password: null,
notes: null,
name: item.title && item.title !== '' ? item.title : '--',
};
if (item.secureContents) {
if (item.secureContents.notesPlain && item.secureContents.notesPlain !== '') {
site.notes = item.secureContents.notesPlain;
}
if (item.secureContents.fields) {
for (j = 0; j < item.secureContents.fields.length; j++) {
var field = item.secureContents.fields[j];
if (field.designation === 'username') {
site.username = field.value;
}
else if (field.designation === 'password') {
site.password = field.value;
}
else {
if (site.notes === null) {
site.notes = '';
}
else {
site.notes += '\n';
}
site.notes += (field.name + ': ' + field.value + '\n');
}
}
}
}
sites.push(site);
}
success(folders, sites, siteRelationships);
};
reader.onerror = function (evt) {
error();
};
}
function importChromeCsv(file, success, error) {
Papa.parse(file, {
header: true,
encoding: 'UTF-8',
complete: function (results) {
parseCsvErrors(results);
var folders = [],
sites = [],
siteRelationships = [];
angular.forEach(results.data, function (value, key) {
sites.push({
favorite: false,
uri: value.url && value.url !== '' ? trimUri(value.url) : null,
username: value.username && value.username !== '' ? value.username : null,
password: value.password && value.password !== '' ? value.password : null,
notes: null,
name: value.name && value.name !== '' ? value.name : '--',
});
});
success(folders, sites, siteRelationships);
}
});
}
function importFirefoxPasswordExporterCsvXml(file, success, error) {
var folders = [],
sites = [],
siteRelationships = [];
function getNameFromHost(host) {
var name = '--';
try {
if (host && host !== '') {
var parser = document.createElement('a');
parser.href = host;
if (parser.hostname) {
name = parser.hostname;
}
}
}
catch (e) {
// do nothing
}
return name;
}
if (file.type === 'text/xml') {
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
var xmlDoc = $.parseXML(evt.target.result),
xml = $(xmlDoc);
var entries = xml.find('entry');
for (var i = 0; i < entries.length; i++) {
var entry = $(entries[i]);
if (!entry) {
continue;
}
var host = entry.attr('host'),
user = entry.attr('user'),
password = entry.attr('password');
sites.push({
favorite: false,
uri: host && host !== '' ? trimUri(host) : null,
username: user && user !== '' ? user : null,
password: password && password !== '' ? password : null,
notes: null,
name: getNameFromHost(host),
});
}
success(folders, sites, siteRelationships);
};
reader.onerror = function (evt) {
error();
};
}
else {
// currently bugged due to the comment
// ref: https://github.com/mholt/PapaParse/issues/351
error('Only .xml exports are supported.');
return;
//Papa.parse(file, {
// comments: '#',
// header: true,
// encoding: 'UTF-8',
// complete: function (results) {
// parseCsvErrors(results);
// angular.forEach(results.data, function (value, key) {
// sites.push({
// favorite: false,
// uri: value.hostname && value.hostname !== '' ? trimUri(value.hostname) : null,
// username: value.username && value.username !== '' ? value.username : null,
// password: value.password && value.password !== '' ? value.password : null,
// notes: null,
// name: getNameFromHost(value.hostname),
// });
// });
// success(folders, sites, siteRelationships);
// }
//});
}
}
function importUpmCsv(file, success, error) {
Papa.parse(file, {
encoding: 'UTF-8',
complete: function (results) {
parseCsvErrors(results);
var folders = [],
sites = [],
siteRelationships = [];
angular.forEach(results.data, function (value, key) {
if (value.length === 5) {
sites.push({
favorite: false,
uri: value[3] && value[3] !== '' ? trimUri(value[3]) : null,
username: value[1] && value[1] !== '' ? value[1] : null,
password: value[2] && value[2] !== '' ? value[2] : null,
notes: value[4] && value[4] !== '' ? value[4] : null,
name: value[0] && value[0] !== '' ? value[0] : '--',
});
}
});
success(folders, sites, siteRelationships);
}
});
}
return _service;
});

View File

@@ -97,8 +97,39 @@ angular
return password;
};
// EFForg/OpenWireless
// ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
var rval = 0;
var range = max - min;
var bits_needed = Math.ceil(Math.log2(range));
if (bits_needed > 53) {
throw new Exception("We cannot generate numbers larger than 53 bits.");
}
var bytes_needed = Math.ceil(bits_needed / 8);
var mask = Math.pow(2, bits_needed) - 1;
// 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111
// Create byte array and fill with N random numbers
var byteArray = new Uint8Array(bytes_needed);
window.crypto.getRandomValues(byteArray);
var p = (bytes_needed - 1) * 8;
for (var i = 0; i < bytes_needed; i++) {
rval += byteArray[i] * Math.pow(2, p);
p -= 8;
}
// Use & to apply the mask and reduce the number of recursive lookups
rval = rval & mask;
if (rval >= range) {
// Integer out of acceptable range
return randomInt(min, max);
}
// Return an integer that falls within the range
return min + rval;
}
return _service;

View File

@@ -1,2 +1,2 @@
angular.module("bit")
.constant("appSettings", {"rememberedEmailCookieName":"bit.rememberedEmail","version":"1.1.1","environment":"Development","apiUri":"http://localhost:4000"});
.constant("appSettings", {"rememberedEmailCookieName":"bit.rememberedEmail","version":"1.2.2","environment":"Development","apiUri":"http://localhost:4000"});

View File

@@ -12,6 +12,12 @@
};
function importSuccess(folders, sites, folderRelationships) {
if (!folders.length && !sites.length) {
$uibModalInstance.dismiss('cancel');
toastr.error('Nothing was imported.');
return;
}
apiService.ciphers.import({
folders: cipherService.encryptFolders(folders, cryptoService.getKey()),
sites: cipherService.encryptSites(sites, cryptoService.getKey()),
@@ -25,8 +31,39 @@
}, importError);
}
function importError() {
function importError(error) {
$analytics.eventTrack('Import Data Failed', { label: $scope.model.source });
$uibModalInstance.dismiss('cancel');
if (error) {
var data = error.data;
if (data && data.ValidationErrors) {
var message = '';
for (var key in data.ValidationErrors) {
if (!data.ValidationErrors.hasOwnProperty(key)) {
continue;
}
for (var i = 0; i < data.ValidationErrors[key].length; i++) {
message += (key + ': ' + data.ValidationErrors[key][i] + ' ');
}
}
if (message !== '') {
toastr.error(message);
return;
}
}
else if (data && data.Message) {
toastr.error(data.Message);
return;
}
else {
toastr.error(error);
return;
}
}
toastr.error('Something went wrong. Try again.', 'Oh No!');
}

View File

@@ -9,13 +9,19 @@
<select id="source" name="source" class="form-control" ng-model="model.source">
<option value="local">bitwarden (csv)</option>
<option value="lastpass">LastPass (csv)</option>
<option value="chromecsv">Chrome (csv)</option>
<option value="firefoxpasswordexportercsvxml">Firefox Password Exporter (xml)</option>
<option value="safeincloudxml">SafeInCloud (xml)</option>
<option value="safeincloudcsv">SafeInCloud (csv)</option>
<option value="keypassxml">KeyPass (xml)</option>
<option value="padlockcsv">Padlock (csv)</option>
<option value="1password1pif">1Password (1pif)</option>
<option value="upmcsv">Universal Password Manager (csv)</option>
</select>
</div>
<div class="form-group">
<label for="file">File</label>
<input type="file" id="file" name="file" />
<input type="file" id="file" name="file" required />
</div>
</div>
<div class="modal-footer">

View File

@@ -37,6 +37,10 @@
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
};
$scope.folderSort = function (item) {
return item.name.toLowerCase();
};
function selectPassword(e) {
var target = $(e.trigger).parent().prev();
if (target.attr('type') === 'text') {

View File

@@ -77,13 +77,25 @@
}
});
editModel.result.then(function (editedSite) {
var site = $filter('filter')($scope.sites, { id: editedSite.id }, true);
if (site && site.length > 0) {
site[0].folderId = editedSite.folderId;
site[0].name = editedSite.name;
site[0].username = editedSite.username;
site[0].favorite = editedSite.favorite;
editModel.result.then(function (returnVal) {
if (returnVal.action === 'edit') {
var siteToUpdate = $filter('filter')($scope.sites, { id: returnVal.data.id }, true);
if (siteToUpdate && siteToUpdate.length > 0) {
siteToUpdate[0].folderId = returnVal.data.folderId;
siteToUpdate[0].name = returnVal.data.name;
siteToUpdate[0].username = returnVal.data.username;
siteToUpdate[0].favorite = returnVal.data.favorite;
}
}
else if (returnVal.action === 'delete') {
var siteToDelete = $filter('filter')($scope.sites, { id: returnVal.data }, true);
if (siteToDelete && siteToDelete.length > 0) {
var index = $scope.sites.indexOf(siteToDelete[0]);
if (index > -1) {
$scope.sites.splice(index, 1);
}
}
}
});
};
@@ -115,7 +127,9 @@
apiService.sites.del({ id: site.id }, function () {
var index = $scope.sites.indexOf(site);
$scope.sites.splice(index, 1);
if (index > -1) {
$scope.sites.splice(index, 1);
}
});
};
@@ -162,7 +176,9 @@
apiService.folders.del({ id: folder.id }, function () {
var index = $scope.folders.indexOf(folder);
$scope.folders.splice(index, 1);
if (index > -1) {
$scope.folders.splice(index, 1);
}
});
};

View File

@@ -15,7 +15,10 @@
$scope.savePromise = apiService.sites.put({ id: siteId }, site, function (siteResponse) {
$analytics.eventTrack('Edited Site');
var decSite = cipherService.decryptSite(siteResponse);
$uibModalInstance.close(decSite);
$uibModalInstance.close({
action: 'edit',
data: decSite
});
}).$promise;
};
@@ -38,6 +41,10 @@
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
};
$scope.folderSort = function (item) {
return item.name.toLowerCase();
};
function selectPassword(e) {
var target = $(e.trigger).parent().prev();
if (target.attr('type') === 'text') {
@@ -45,6 +52,19 @@
}
}
$scope.delete = function () {
if (!confirm('Are you sure you want to delete this site (' + $scope.site.name + ')?')) {
return;
}
apiService.sites.del({ id: $scope.site.id }, function () {
$uibModalInstance.close({
action: 'delete',
data: $scope.site.id
});
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};

View File

@@ -12,6 +12,9 @@
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-folder-open"></i> {{folder.name}}</h3>
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" ng-click="addSite(folder)" uib-tooltip="Add Site">
<i class="fa fa-plus"></i>
</button>
<button type="button" class="btn btn-box-tool" ng-click="deleteFolder(folder)" ng-show="canDeleteFolder(folder)" uib-tooltip="Delete">
<i class="fa fa-trash"></i>
</button>
@@ -54,4 +57,4 @@
</div>
</div>
</div>
</section>
</section>

View File

@@ -34,7 +34,7 @@
<div class="form-group" show-errors>
<label for="folder">Folder</label>
<select id="folder" name="FolderId" ng-model="site.folderId" class="form-control" api-field>
<option ng-repeat="folder in folders" value="{{folder.id}}">{{folder.name}}</option>
<option ng-repeat="folder in folders | orderBy: folderSort" value="{{folder.id}}">{{folder.name}}</option>
</select>
</div>
</div>
@@ -95,4 +95,4 @@
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>
</form>
</form>

View File

@@ -37,7 +37,7 @@
<div class="form-group" show-errors>
<label for="folder">Folder</label>
<select id="folder" name="FolderId" ng-model="site.folderId" class="form-control" api-field>
<option ng-repeat="folder in folders" value="{{folder.id}}">{{folder.name}}</option>
<option ng-repeat="folder in folders | orderBy: folderSort" value="{{folder.id}}">{{folder.name}}</option>
</select>
</div>
</div>
@@ -97,5 +97,8 @@
<i class="fa fa-refresh fa-spin loading-icon" ng-show="editSiteForm.$loading"></i>Save
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
<button type="button" class="btn btn-link pull-right" ng-click="delete()" uib-tooltip="Delete">
<i class="fa fa-trash fa-lg"></i>
</button>
</div>
</form>
</form>

View File

@@ -107,7 +107,7 @@
</ul>
</li>
<li>
<a href="https://bitwarden.com/contact/" target="_blank"
<a href="https://help.bitwarden.com/" target="_blank"
analytics-on="click" analytics-event="Clicked Get Help">
<i class="fa fa-info-circle"></i> <span>Get Help</span>
</a>