org export/import
This commit is contained in:
parent
ff9030e7af
commit
aaa91e50b7
|
@ -28,6 +28,22 @@
|
||||||
}).$promise;
|
}).$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 () {
|
$scope.delete = function () {
|
||||||
$uibModal.open({
|
$uibModal.open({
|
||||||
animation: true,
|
animation: true,
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
|
@ -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');
|
||||||
|
};
|
||||||
|
});
|
|
@ -49,6 +49,21 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="box box-default">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">Import/Export</h3>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
<p>
|
||||||
|
Quickly import logins, collections, and other data. You can also export all of your organization's
|
||||||
|
vault data in <code>.csv</code> format.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="box-footer">
|
||||||
|
<button class="btn btn-default btn-flat" type="button" ng-click="import()">Import Data</button>
|
||||||
|
<button class="btn btn-default btn-flat" type="button" ng-click="export()">Export Data</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="box box-danger">
|
<div class="box box-danger">
|
||||||
<div class="box-header with-border">
|
<div class="box-header with-border">
|
||||||
<h3 class="box-title">Danger Zone</h3>
|
<h3 class="box-title">Danger Zone</h3>
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
listDetails: { url: _apiUri + '/ciphers/details', method: 'GET', params: {} },
|
listDetails: { url: _apiUri + '/ciphers/details', method: 'GET', params: {} },
|
||||||
listOrganizationDetails: { url: _apiUri + '/ciphers/organization-details', method: 'GET', params: {} },
|
listOrganizationDetails: { url: _apiUri + '/ciphers/organization-details', method: 'GET', params: {} },
|
||||||
'import': { url: _apiUri + '/ciphers/import', method: 'POST', 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' } },
|
favorite: { url: _apiUri + '/ciphers/:id/favorite', method: 'POST', params: { id: '@id' } },
|
||||||
putPartial: { url: _apiUri + '/ciphers/:id/partial', 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' } },
|
putShare: { url: _apiUri + '/ciphers/:id/share', method: 'POST', params: { id: '@id' } },
|
||||||
|
|
|
@ -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 = [
|
var _passwordFieldNames = [
|
||||||
'password', 'pass word', 'passphrase', 'pass phrase',
|
'password', 'pass word', 'passphrase', 'pass phrase',
|
||||||
'pass', 'code', 'code word', 'codeword',
|
'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) {
|
function importLastPass(file, success, error) {
|
||||||
if (typeof file !== 'string' && file.type && file.type === 'text/html') {
|
if (typeof file !== 'string' && file.type && file.type === 'text/html') {
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
angular
|
angular
|
||||||
.module('bit.tools')
|
.module('bit.tools')
|
||||||
|
|
||||||
.controller('toolsExportController', function ($scope, apiService, authService, $uibModalInstance, cryptoService,
|
.controller('toolsExportController', function ($scope, apiService, $uibModalInstance, cipherService, $q,
|
||||||
cipherService, $q, toastr, $analytics) {
|
toastr, $analytics) {
|
||||||
$analytics.eventTrack('toolsExportController', { category: 'Modal' });
|
$analytics.eventTrack('toolsExportController', { category: 'Modal' });
|
||||||
$scope.export = function (model) {
|
$scope.export = function (model) {
|
||||||
$scope.startedExport = true;
|
$scope.startedExport = true;
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
$analytics.eventTrack('toolsImportController', { category: 'Modal' });
|
$analytics.eventTrack('toolsImportController', { category: 'Modal' });
|
||||||
$scope.model = { source: '' };
|
$scope.model = { source: '' };
|
||||||
$scope.source = {};
|
$scope.source = {};
|
||||||
|
$scope.splitFeatured = true;
|
||||||
|
|
||||||
$scope.options = [
|
$scope.options = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<option value="">-- Select --</option>
|
<option value="">-- Select --</option>
|
||||||
<option ng-repeat="option in options | filter: { featured: true } | orderBy: ['sort', 'name']"
|
<option ng-repeat="option in options | filter: { featured: true } | orderBy: ['sort', 'name']"
|
||||||
value="{{option.id}}">{{option.name}}</option>
|
value="{{option.id}}">{{option.name}}</option>
|
||||||
<option value="-" disabled></option>
|
<option value="-" disabled ng-if="splitFeatured"></option>
|
||||||
<option ng-repeat="option in options | filter: { featured: '!true' } | orderBy: ['sort', 'name']"
|
<option ng-repeat="option in options | filter: { featured: '!true' } | orderBy: ['sort', 'name']"
|
||||||
value="{{option.id}}">{{option.name}}</option>
|
value="{{option.id}}">{{option.name}}</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
|
@ -231,6 +231,8 @@
|
||||||
<script src="app/organization/organizationCollectionsEditController.js"></script>
|
<script src="app/organization/organizationCollectionsEditController.js"></script>
|
||||||
<script src="app/organization/organizationCollectionsUsersController.js"></script>
|
<script src="app/organization/organizationCollectionsUsersController.js"></script>
|
||||||
<script src="app/organization/organizationSettingsController.js"></script>
|
<script src="app/organization/organizationSettingsController.js"></script>
|
||||||
|
<script src="app/organization/organizationSettingsImportController.js"></script>
|
||||||
|
<script src="app/organization/organizationSettingsExportController.js"></script>
|
||||||
<script src="app/organization/organizationBillingController.js"></script>
|
<script src="app/organization/organizationBillingController.js"></script>
|
||||||
<script src="app/organization/organizationBillingChangePaymentController.js"></script>
|
<script src="app/organization/organizationBillingChangePaymentController.js"></script>
|
||||||
<script src="app/organization/organizationBillingAdjustSeatsController.js"></script>
|
<script src="app/organization/organizationBillingAdjustSeatsController.js"></script>
|
||||||
|
|
Loading…
Reference in New Issue