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 @@ +
+ Quickly import logins, collections, and other data. You can also export all of your organization's
+ vault data in .csv
format.
+