diff --git a/src/app/organization/organizationSettingsController.js b/src/app/organization/organizationSettingsController.js index dfe39c5e9f..7b5b3c6dd3 100644 --- a/src/app/organization/organizationSettingsController.js +++ b/src/app/organization/organizationSettingsController.js @@ -28,6 +28,22 @@ }).$promise; }; + $scope.import = function () { + $uibModal.open({ + animation: true, + templateUrl: 'app/tools/views/toolsImport.html', + controller: 'organizationSettingsImportController' + }); + }; + + $scope.export = function () { + $uibModal.open({ + animation: true, + templateUrl: 'app/tools/views/toolsExport.html', + controller: 'organizationSettingsExportController' + }); + }; + $scope.delete = function () { $uibModal.open({ animation: true, diff --git a/src/app/organization/organizationSettingsExportController.js b/src/app/organization/organizationSettingsExportController.js new file mode 100644 index 0000000000..874b918d43 --- /dev/null +++ b/src/app/organization/organizationSettingsExportController.js @@ -0,0 +1,114 @@ +angular + .module('bit.organization') + + .controller('organizationSettingsExportController', function ($scope, apiService, $uibModalInstance, cipherService, + $q, toastr, $analytics, $state) { + $analytics.eventTrack('organizationSettingsExportController', { category: 'Modal' }); + $scope.export = function (model) { + $scope.startedExport = true; + var decLogins = [], + decCollections = []; + + var collectionsPromise = apiService.collections.listOrganization({ orgId: $state.params.orgId }, + function (collections) { + decCollections = cipherService.decryptCollections(collections.Data, $state.params.orgId, true); + }).$promise; + + var loginsPromise = apiService.ciphers.listOrganizationDetails({ organizationId: $state.params.orgId }, + function (ciphers) { + for (var i = 0; i < ciphers.Data.length; i++) { + if (ciphers.Data[i].Type === 1) { + var decLogin = cipherService.decryptLogin(ciphers.Data[i]); + decLogins.push(decLogin); + } + } + }).$promise; + + $q.all([collectionsPromise, loginsPromise]).then(function () { + if (!decLogins.length) { + toastr.error('Nothing to export.', 'Error!'); + $scope.close(); + return; + } + + var collectionsDict = {}; + for (var i = 0; i < decCollections.length; i++) { + collectionsDict[decCollections[i].id] = decCollections[i]; + } + + try { + var exportLogins = []; + for (i = 0; i < decLogins.length; i++) { + var login = { + name: decLogins[i].name, + uri: decLogins[i].uri, + username: decLogins[i].username, + password: decLogins[i].password, + notes: decLogins[i].notes, + totp: decLogins[i].totp, + collections: [] + }; + + if (decLogins[i].collectionIds) { + for (var j = 0; j < decLogins[i].collectionIds.length; j++) { + if (collectionsDict.hasOwnProperty(decLogins[i].collectionIds[j])) { + login.collections.push(collectionsDict[decLogins[i].collectionIds[j]].name); + } + } + } + + exportLogins.push(login); + } + + var csvString = Papa.unparse(exportLogins); + var csvBlob = new Blob([csvString]); + + // IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx + if (window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(csvBlob, makeFileName()); + } + else { + var a = window.document.createElement('a'); + a.href = window.URL.createObjectURL(csvBlob, { type: 'text/plain' }); + a.download = makeFileName(); + document.body.appendChild(a); + // IE: "Access is denied". + // ref: https://connect.microsoft.com/IE/feedback/details/797361/ie-10-treats-blob-url-as-cross-origin-and-denies-access + a.click(); + document.body.removeChild(a); + } + + $analytics.eventTrack('Exported Organization Data'); + toastr.success('Your data has been exported. Check your browser\'s downloads folder.', 'Success!'); + $scope.close(); + } + catch (err) { + toastr.error('Something went wrong. Please try again.', 'Error!'); + $scope.close(); + } + }, function () { + toastr.error('Something went wrong. Please try again.', 'Error!'); + $scope.close(); + }); + }; + + $scope.close = function () { + $uibModalInstance.dismiss('cancel'); + }; + + function makeFileName() { + var now = new Date(); + var dateString = + now.getFullYear() + '' + padNumber(now.getMonth() + 1, 2) + '' + padNumber(now.getDate(), 2) + + padNumber(now.getHours(), 2) + '' + padNumber(now.getMinutes(), 2) + + padNumber(now.getSeconds(), 2); + + return 'bitwarden_org_export_' + dateString + '.csv'; + } + + function padNumber(number, width, paddingCharacter) { + paddingCharacter = paddingCharacter || '0'; + number = number + ''; + return number.length >= width ? number : new Array(width - number.length + 1).join(paddingCharacter) + number; + } + }); diff --git a/src/app/organization/organizationSettingsImportController.js b/src/app/organization/organizationSettingsImportController.js new file mode 100644 index 0000000000..cf2ca776ed --- /dev/null +++ b/src/app/organization/organizationSettingsImportController.js @@ -0,0 +1,119 @@ +angular + .module('bit.organization') + + .controller('organizationSettingsImportController', function ($scope, $state, apiService, $uibModalInstance, cipherService, + toastr, importService, $analytics, $sce, validationService, cryptoService) { + $analytics.eventTrack('organizationSettingsImportController', { category: 'Modal' }); + $scope.model = { source: '' }; + $scope.source = {}; + $scope.splitFeatured = false; + + $scope.options = [ + { + id: 'bitwardencsv', + name: 'bitwarden (csv)', + featured: true, + sort: 1, + instructions: $sce.trustAsHtml('Export using the web vault (vault.bitwarden.com). ' + + 'Log into the web vault and navigate to your organization\'s admin area. Then to go ' + + '"Settings" > "Tools" > "Export".') + } + ]; + + $scope.setSource = function () { + for (var i = 0; i < $scope.options.length; i++) { + if ($scope.options[i].id === $scope.model.source) { + $scope.source = $scope.options[i]; + break; + } + } + }; + $scope.setSource(); + + $scope.import = function (model, form) { + if (!model.source || model.source === '') { + validationService.addError(form, 'source', 'Select the format of the import file.', true); + return; + } + + var file = document.getElementById('file').files[0]; + if (!file && (!model.fileContents || model.fileContents === '')) { + validationService.addError(form, 'file', 'Select the import file or copy/paste the import file contents.', true); + return; + } + + $scope.processing = true; + importService.importOrg(model.source, file || model.fileContents, importSuccess, importError); + }; + + function importSuccess(collections, logins, collectionRelationships) { + if (!collections.length && !logins.length) { + importError('Nothing was imported.'); + return; + } + else if (logins.length) { + var halfway = Math.floor(logins.length / 2); + var last = logins.length - 1; + if (loginIsBadData(logins[0]) && loginIsBadData(logins[halfway]) && loginIsBadData(logins[last])) { + importError('CSV data is not formatted correctly. Please check your import file and try again.'); + return; + } + } + + apiService.ciphers.importOrg({ orgId: $state.params.orgId }, { + collections: cipherService.encryptCollections(collections, $state.params.orgId), + logins: cipherService.encryptLogins(logins, cryptoService.getOrgKey($state.params.orgId)), + collectionRelationships: collectionRelationships + }, function () { + $uibModalInstance.dismiss('cancel'); + $state.go('backend.user.vault', { refreshFromServer: true }).then(function () { + $analytics.eventTrack('Imported Org Data', { label: $scope.model.source }); + toastr.success('Data has been successfully imported into your vault.', 'Import Success'); + }); + }, importError); + } + + function loginIsBadData(login) { + return (login.name === null || login.name === '--') && (login.password === null || login.password === ''); + } + + function importError(error) { + $analytics.eventTrack('Import Org 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!'); + } + + $scope.close = function () { + $uibModalInstance.dismiss('cancel'); + }; + }); diff --git a/src/app/organization/views/organizationSettings.html b/src/app/organization/views/organizationSettings.html index 7007daf5a9..cdc954ce11 100644 --- a/src/app/organization/views/organizationSettings.html +++ b/src/app/organization/views/organizationSettings.html @@ -49,6 +49,21 @@ +
+
+

Import/Export

+
+
+

+ Quickly import logins, collections, and other data. You can also export all of your organization's + vault data in .csv format. +

+
+ +

Danger Zone

diff --git a/src/app/services/apiService.js b/src/app/services/apiService.js index 7abd50c106..af3156c315 100644 --- a/src/app/services/apiService.js +++ b/src/app/services/apiService.js @@ -32,6 +32,7 @@ listDetails: { url: _apiUri + '/ciphers/details', method: 'GET', params: {} }, listOrganizationDetails: { url: _apiUri + '/ciphers/organization-details', method: 'GET', params: {} }, 'import': { url: _apiUri + '/ciphers/import', method: 'POST', params: {} }, + importOrg: { url: _apiUri + '/ciphers/import-organization?organizationId=:orgId', method: 'POST', params: { orgId: '@orgId' } }, favorite: { url: _apiUri + '/ciphers/:id/favorite', method: 'POST', params: { id: '@id' } }, putPartial: { url: _apiUri + '/ciphers/:id/partial', method: 'POST', params: { id: '@id' } }, putShare: { url: _apiUri + '/ciphers/:id/share', method: 'POST', params: { id: '@id' } }, diff --git a/src/app/services/importService.js b/src/app/services/importService.js index 4e4b50c7b9..ee7a793967 100644 --- a/src/app/services/importService.js +++ b/src/app/services/importService.js @@ -109,6 +109,22 @@ } }; + _service.importOrg = function (source, file, success, error) { + if (!file) { + error(); + return; + } + + switch (source) { + case 'bitwardencsv': + importBitwardenOrgCsv(file, success, error); + break; + default: + error(); + break; + } + }; + var _passwordFieldNames = [ 'password', 'pass word', 'passphrase', 'pass phrase', 'pass', 'code', 'code word', 'codeword', @@ -272,6 +288,64 @@ }); } + function importBitwardenOrgCsv(file, success, error) { + Papa.parse(file, { + header: true, + encoding: 'UTF-8', + complete: function (results) { + parseCsvErrors(results); + + var collections = [], + logins = [], + collectionRelationships = []; + + angular.forEach(results.data, function (value, key) { + var loginIndex = logins.length; + + if (value.collections && value.collections !== '') { + var loginCollections = value.collections.split(','); + + for (var i = 0; i < loginCollections.length; i++) { + var addCollection = true; + var collectionIndex = collections.length; + + for (var j = 0; j < collections.length; j++) { + if (collections[j].name === loginCollections[i]) { + addCollection = false; + collectionIndex = j; + break; + } + } + + if (addCollection) { + collections.push({ + name: loginCollections[i] + }); + } + + collectionRelationships.push({ + key: loginIndex, + value: collectionIndex + }); + } + } + + logins.push({ + favorite: false, + 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, + name: value.name && value.name !== '' ? value.name : '--', + totp: value.totp && value.totp !== '' ? value.totp : null + }); + }); + + success(collections, logins, collectionRelationships); + } + }); + } + function importLastPass(file, success, error) { if (typeof file !== 'string' && file.type && file.type === 'text/html') { var reader = new FileReader(); diff --git a/src/app/tools/toolsExportController.js b/src/app/tools/toolsExportController.js index 69242c97a1..8000c18513 100644 --- a/src/app/tools/toolsExportController.js +++ b/src/app/tools/toolsExportController.js @@ -1,8 +1,8 @@ angular .module('bit.tools') - .controller('toolsExportController', function ($scope, apiService, authService, $uibModalInstance, cryptoService, - cipherService, $q, toastr, $analytics) { + .controller('toolsExportController', function ($scope, apiService, $uibModalInstance, cipherService, $q, + toastr, $analytics) { $analytics.eventTrack('toolsExportController', { category: 'Modal' }); $scope.export = function (model) { $scope.startedExport = true; diff --git a/src/app/tools/toolsImportController.js b/src/app/tools/toolsImportController.js index 2b943a0b04..7dee93f860 100644 --- a/src/app/tools/toolsImportController.js +++ b/src/app/tools/toolsImportController.js @@ -6,6 +6,7 @@ $analytics.eventTrack('toolsImportController', { category: 'Modal' }); $scope.model = { source: '' }; $scope.source = {}; + $scope.splitFeatured = true; $scope.options = [ { diff --git a/src/app/tools/views/toolsImport.html b/src/app/tools/views/toolsImport.html index 07213649f6..b0d28d74c8 100644 --- a/src/app/tools/views/toolsImport.html +++ b/src/app/tools/views/toolsImport.html @@ -16,7 +16,7 @@ - + diff --git a/src/index.html b/src/index.html index 3da5319ffc..f9f2e2c469 100644 --- a/src/index.html +++ b/src/index.html @@ -231,6 +231,8 @@ + +