1
0
mirror of https://github.com/bitwarden/web synced 2025-12-15 07:43:16 +00:00

create and mange org through licensing

This commit is contained in:
Kyle Spearrin
2017-08-14 22:06:51 -04:00
parent 4660ad824d
commit 995fc96a5d
7 changed files with 859 additions and 675 deletions

View File

@@ -1,18 +1,26 @@
angular angular
.module('bit.organization') .module('bit.organization')
.controller('organizationBillingController', function ($scope, apiService, $state, $uibModal, toastr, $analytics) { .controller('organizationBillingController', function ($scope, apiService, $state, $uibModal, toastr, $analytics,
appSettings) {
$scope.selfHosted = appSettings.selfHosted;
$scope.charges = []; $scope.charges = [];
$scope.paymentSource = null; $scope.paymentSource = null;
$scope.plan = null; $scope.plan = null;
$scope.subscription = null; $scope.subscription = null;
$scope.loading = true; $scope.loading = true;
var license = null;
$scope.expiration = null;
$scope.$on('$viewContentLoaded', function () { $scope.$on('$viewContentLoaded', function () {
load(); load();
}); });
$scope.changePayment = function () { $scope.changePayment = function () {
if ($scope.selfHosted) {
return;
}
var modal = $uibModal.open({ var modal = $uibModal.open({
animation: true, animation: true,
templateUrl: 'app/settings/views/settingsBillingChangePayment.html', templateUrl: 'app/settings/views/settingsBillingChangePayment.html',
@@ -30,6 +38,10 @@
}; };
$scope.changePlan = function () { $scope.changePlan = function () {
if ($scope.selfHosted) {
return;
}
var modal = $uibModal.open({ var modal = $uibModal.open({
animation: true, animation: true,
templateUrl: 'app/organization/views/organizationBillingChangePlan.html', templateUrl: 'app/organization/views/organizationBillingChangePlan.html',
@@ -47,6 +59,10 @@
}; };
$scope.adjustSeats = function (add) { $scope.adjustSeats = function (add) {
if ($scope.selfHosted) {
return;
}
var modal = $uibModal.open({ var modal = $uibModal.open({
animation: true, animation: true,
templateUrl: 'app/organization/views/organizationBillingAdjustSeats.html', templateUrl: 'app/organization/views/organizationBillingAdjustSeats.html',
@@ -64,6 +80,10 @@
}; };
$scope.adjustStorage = function (add) { $scope.adjustStorage = function (add) {
if ($scope.selfHosted) {
return;
}
var modal = $uibModal.open({ var modal = $uibModal.open({
animation: true, animation: true,
templateUrl: 'app/settings/views/settingsBillingAdjustStorage.html', templateUrl: 'app/settings/views/settingsBillingAdjustStorage.html',
@@ -81,6 +101,10 @@
}; };
$scope.verifyBank = function () { $scope.verifyBank = function () {
if ($scope.selfHosted) {
return;
}
var modal = $uibModal.open({ var modal = $uibModal.open({
animation: true, animation: true,
templateUrl: 'app/organization/views/organizationBillingVerifyBank.html', templateUrl: 'app/organization/views/organizationBillingVerifyBank.html',
@@ -93,6 +117,10 @@
}; };
$scope.cancel = function () { $scope.cancel = function () {
if ($scope.selfHosted) {
return;
}
if (!confirm('Are you sure you want to cancel? All users will lose access to the organization ' + if (!confirm('Are you sure you want to cancel? All users will lose access to the organization ' +
'at the end of this billing cycle.')) { 'at the end of this billing cycle.')) {
return; return;
@@ -107,6 +135,10 @@
}; };
$scope.reinstate = function () { $scope.reinstate = function () {
if ($scope.selfHosted) {
return;
}
if (!confirm('Are you sure you want to remove the cancellation request and reinstate this organization?')) { if (!confirm('Are you sure you want to remove the cancellation request and reinstate this organization?')) {
return; return;
} }
@@ -119,12 +151,54 @@
}); });
}; };
$scope.updateLicense = function () {
if (!$scope.selfHosted) {
return;
}
var modal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsBillingUpdateLicense.html',
controller: 'organizationBillingUpdateLicenseController'
});
modal.result.then(function () {
load();
});
};
$scope.license = function () {
if ($scope.selfHosted) {
return;
}
var licenseString = JSON.stringify(license, null, 2);
var licenseBlob = new Blob([licenseString]);
// IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(licenseBlob, 'bitwarden_organization_license.json');
}
else {
var a = window.document.createElement('a');
a.href = window.URL.createObjectURL(licenseBlob, { type: 'text/plain' });
a.download = 'bitwarden_premium_license.json';
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);
}
};
function load() { function load() {
apiService.organizations.getBilling({ id: $state.params.orgId }, function (org) { apiService.organizations.getBilling({ id: $state.params.orgId }, function (org) {
$scope.loading = false; $scope.loading = false;
$scope.noSubscription = org.PlanType === 0; $scope.noSubscription = org.PlanType === 0;
var i = 0; var i = 0;
$scope.expiration = org.Expiration;
license = org.License;
$scope.plan = { $scope.plan = {
name: org.Plan, name: org.Plan,

View File

@@ -0,0 +1,30 @@
angular
.module('bit.organization')
.controller('organizationBillingUpdateLicenseController', function ($scope, $state, $uibModalInstance, apiService,
$analytics, toastr, validationService) {
$analytics.eventTrack('organizationBillingUpdateLicenseController', { category: 'Modal' });
$scope.submit = function (form) {
var fileEl = document.getElementById('file');
var files = fileEl.files;
if (!files || !files.length) {
validationService.addError(form, 'file', 'Select a license file.', true);
return;
}
var fd = new FormData();
fd.append('license', files[0]);
$scope.submitPromise = apiService.organizations.putLicense({ id: $state.params.orgId }, fd)
.$promise.then(function (response) {
$analytics.eventTrack('Updated License');
toastr.success('You have updated your license.');
$uibModalInstance.close();
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -1,7 +1,7 @@
<section class="content-header"> <section class="content-header">
<h1> <h1>
Billing Billing
<small>manage your payments</small> <small>manage your billing &amp; licensing</small>
</h1> </h1>
</section> </section>
<section class="content"> <section class="content">
@@ -26,14 +26,28 @@
<div class="box-body"> <div class="box-body">
<div class="row"> <div class="row">
<div class="col-sm-6"> <div class="col-sm-6">
<dl> <dl ng-if="selfHosted">
<dt>Name</dt>
<dd>{{plan.name || '-'}}</dd>
<dt>Expiration</dt>
<dd ng-if="loading">
Loading...
</dd>
<dd ng-if="!loading && expiration">
{{expiration | date: 'medium'}}
</dd>
<dd ng-if="!loading && !expiration">
Never expires
</dd>
</dl>
<dl ng-if="!selfHosted">
<dt>Name</dt> <dt>Name</dt>
<dd>{{plan.name || '-'}}</dd> <dd>{{plan.name || '-'}}</dd>
<dt>Total Seats</dt> <dt>Total Seats</dt>
<dd>{{plan.seats || '-'}}</dd> <dd>{{plan.seats || '-'}}</dd>
</dl> </dl>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6" ng-if="!selfHosted">
<dl> <dl>
<dt>Status</dt> <dt>Status</dt>
<dd> <dd>
@@ -41,11 +55,11 @@
<span ng-if="subscription.markedForCancel">- marked for cancellation</span> <span ng-if="subscription.markedForCancel">- marked for cancellation</span>
</dd> </dd>
<dt>Next Charge</dt> <dt>Next Charge</dt>
<dd>{{nextInvoice ? ((nextInvoice.date | date: format: mediumDate) + ', ' + (nextInvoice.amount | currency:'$')) : '-'}}</dd> <dd>{{nextInvoice ? ((nextInvoice.date | date: 'mediumDate') + ', ' + (nextInvoice.amount | currency:'$')) : '-'}}</dd>
</dl> </dl>
</div> </div>
</div> </div>
<div class="row" ng-if="!noSubscription"> <div class="row" ng-if="!selfHosted && !noSubscription">
<div class="col-md-6"> <div class="col-md-6">
<strong>Details</strong> <strong>Details</strong>
<div ng-show="loading"> <div ng-show="loading">
@@ -67,7 +81,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="box-footer"> <div class="box-footer" ng-if="!selfHosted">
<button type="button" class="btn btn-default btn-flat" ng-click="changePlan()"> <button type="button" class="btn btn-default btn-flat" ng-click="changePlan()">
Change Plan Change Plan
</button> </button>
@@ -79,6 +93,18 @@
ng-if="!noSubscription && subscription.markedForCancel"> ng-if="!noSubscription && subscription.markedForCancel">
Reinstate Plan Reinstate Plan
</button> </button>
<button type="button" class="btn btn-default btn-flat" ng-click="license()"
ng-if="!subscription.cancelled">
Download License
</button>
</div>
<div class="box-footer" ng-if="selfHosted">
<button type="button" class="btn btn-default btn-flat" ng-click="updateLicense()">
Update License
</button>
<a href="https://vault.bitwarden.com" class="btn btn-default btn-flat" target="_blank">
Manage Billing
</a>
</div> </div>
</div> </div>
<div class="box box-default"> <div class="box box-default">
@@ -93,7 +119,7 @@
You plan currently has a total of <b>{{plan.seats}}</b> seats. You plan currently has a total of <b>{{plan.seats}}</b> seats.
</div> </div>
</div> </div>
<div class="box-footer" ng-if="!noSubscription"> <div class="box-footer" ng-if="!selfHosted && !noSubscription">
<button type="button" class="btn btn-default btn-flat" ng-click="adjustSeats(true)"> <button type="button" class="btn btn-default btn-flat" ng-click="adjustSeats(true)">
Add Seats Add Seats
</button> </button>
@@ -102,7 +128,7 @@
</button> </button>
</div> </div>
</div> </div>
<div class="box box-default" ng-if="storage"> <div class="box box-default" ng-if="storage && !selfHosted">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title">Storage</h3> <h3 class="box-title">Storage</h3>
</div> </div>
@@ -128,7 +154,7 @@
</button> </button>
</div> </div>
</div> </div>
<div class="box box-default"> <div class="box box-default" ng-if="!selfHosted">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title">Payment Method</h3> <h3 class="box-title">Payment Method</h3>
</div> </div>
@@ -160,7 +186,7 @@
</button> </button>
</div> </div>
</div> </div>
<div class="box box-default"> <div class="box box-default" ng-if="!selfHosted">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title">Charges</h3> <h3 class="box-title">Charges</h3>
</div> </div>
@@ -176,7 +202,7 @@
<tbody> <tbody>
<tr ng-repeat="charge in charges"> <tr ng-repeat="charge in charges">
<td style="width: 200px"> <td style="width: 200px">
{{charge.date | date: format: mediumDate}} {{charge.date | date: 'mediumDate'}}
</td> </td>
<td style="min-width: 150px"> <td style="min-width: 150px">
{{charge.paymentSource}} {{charge.paymentSource}}

View File

@@ -70,7 +70,17 @@
putReinstate: { url: _apiUri + '/organizations/:id/reinstate', method: 'POST', params: { id: '@id' } }, putReinstate: { url: _apiUri + '/organizations/:id/reinstate', method: 'POST', params: { id: '@id' } },
postLeave: { url: _apiUri + '/organizations/:id/leave', method: 'POST', params: { id: '@id' } }, postLeave: { url: _apiUri + '/organizations/:id/leave', method: 'POST', params: { id: '@id' } },
postVerifyBank: { url: _apiUri + '/organizations/:id/verify-bank', method: 'POST', params: { id: '@id' } }, postVerifyBank: { url: _apiUri + '/organizations/:id/verify-bank', method: 'POST', params: { id: '@id' } },
del: { url: _apiUri + '/organizations/:id/delete', method: 'POST', params: { id: '@id' } } del: { url: _apiUri + '/organizations/:id/delete', method: 'POST', params: { id: '@id' } },
postLicense: {
url: _apiUri + '/organizations/license',
method: 'POST',
headers: { 'Content-Type': undefined }
},
putLicense: {
url: _apiUri + '/organizations/:id/license',
method: 'POST',
headers: { 'Content-Type': undefined }
}
}); });
_service.organizationUsers = $resource(_apiUri + '/organizations/:orgId/users/:id', {}, { _service.organizationUsers = $resource(_apiUri + '/organizations/:orgId/users/:id', {}, {

View File

@@ -2,10 +2,11 @@
.module('bit.settings') .module('bit.settings')
.controller('settingsCreateOrganizationController', function ($scope, $state, apiService, cryptoService, .controller('settingsCreateOrganizationController', function ($scope, $state, apiService, cryptoService,
toastr, $analytics, authService, stripe, constants) { toastr, $analytics, authService, stripe, constants, appSettings, validationService) {
$scope.plans = constants.plans; $scope.plans = constants.plans;
$scope.storageGb = constants.storageGb; $scope.storageGb = constants.storageGb;
$scope.paymentMethod = 'card'; $scope.paymentMethod = 'card';
$scope.selfHosted = appSettings.selfHosted;
$scope.model = { $scope.model = {
plan: 'free', plan: 'free',
@@ -52,7 +53,24 @@
} }
}; };
$scope.submit = function (model) { $scope.submit = function (model, form) {
if ($scope.selfHosted) {
var fileEl = document.getElementById('file');
var files = fileEl.files;
if (!files || !files.length) {
validationService.addError(form, 'file', 'Select a license file.', true);
return;
}
var fd = new FormData();
fd.append('license', files[0]);
fd.append('key', shareKeyCt);
$scope.submitPromise = apiService.organizations.postLicense(fd).$promise.then(function (result) {
return finalizeCreate();
});
}
else {
var shareKeyCt = cryptoService.makeShareKeyCt(); var shareKeyCt = cryptoService.makeShareKeyCt();
if (model.plan === 'free') { if (model.plan === 'free') {
@@ -97,6 +115,7 @@
throw err.message; throw err.message;
}).then(finalizeCreate); }).then(finalizeCreate);
} }
}
function finalizeCreate(result) { function finalizeCreate(result) {
$analytics.eventTrack('Created Organization'); $analytics.eventTrack('Created Organization');

View File

@@ -6,13 +6,36 @@
Organizations allow you to share parts of your vault with others as well as manage related users Organizations allow you to share parts of your vault with others as well as manage related users
for a specific entity (such as a family, small team, or large company). for a specific entity (such as a family, small team, or large company).
</p> </p>
<form name="createOrgForm" ng-submit="createOrgForm.$valid && submit(model)" api-form="submitPromise"> <form name="createOrgForm" ng-submit="createOrgForm.$valid && submit(model, createOrgForm)" api-form="submitPromise">
<div class="callout callout-danger validation-errors" ng-show="createOrgForm.$errors"> <div class="callout callout-danger validation-errors" ng-show="createOrgForm.$errors">
<h4>Errors have occurred</h4> <h4>Errors have occurred</h4>
<ul> <ul>
<li ng-repeat="e in createOrgForm.$errors">{{e}}</li> <li ng-repeat="e in createOrgForm.$errors">{{e}}</li>
</ul> </ul>
</div> </div>
<div ng-if="selfHosted">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">License</h3>
</div>
<div class="box-body">
<p>Create an organization with your license file.</p>
<div class="form-group" show-error>
<label for="file" class="sr-only">License</label>
<input type="file" id="file" name="file" accept=".json" />
<p class="help-block">
Your license file will be named something like <code>bitwarden_organization_license.json</code>
</p>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="createOrgForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="createOrgForm.$loading"></i>Submit
</button>
</div>
</div>
</div>
<div ng-if="!selfHosted">
<div class="box box-default"> <div class="box box-default">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title">General Information</h3> <h3 class="box-title">General Information</h3>
@@ -669,5 +692,6 @@
</button> </button>
</div> </div>
</div> </div>
</div>
</form> </form>
</section> </section>

View File

@@ -202,6 +202,7 @@
<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>
<script src="app/organization/organizationBillingAdjustStorageController.js"></script> <script src="app/organization/organizationBillingAdjustStorageController.js"></script>
<script src="app/organization/organizationBillingUpdateLicenseController.js"></script>
<script src="app/organization/organizationDeleteController.js"></script> <script src="app/organization/organizationDeleteController.js"></script>
<script src="app/organization/organizationBillingChangePlanController.js"></script> <script src="app/organization/organizationBillingChangePlanController.js"></script>
<script src="app/organization/organizationBillingVerifyBankController.js"></script> <script src="app/organization/organizationBillingVerifyBankController.js"></script>