From 995fc96a5d0f2d71020633a25015c7ef8a9de380 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 14 Aug 2017 22:06:51 -0400 Subject: [PATCH] create and mange org through licensing --- .../organizationBillingController.js | 76 +- ...anizationBillingUpdateLicenseController.js | 30 + .../views/organizationBilling.html | 48 +- src/app/services/apiService.js | 12 +- .../settingsCreateOrganizationController.js | 95 +- .../views/settingsCreateOrganization.html | 1272 +++++++++-------- src/index.html | 1 + 7 files changed, 859 insertions(+), 675 deletions(-) create mode 100644 src/app/organization/organizationBillingUpdateLicenseController.js diff --git a/src/app/organization/organizationBillingController.js b/src/app/organization/organizationBillingController.js index 2eb06dd07c..55194a489d 100644 --- a/src/app/organization/organizationBillingController.js +++ b/src/app/organization/organizationBillingController.js @@ -1,18 +1,26 @@ angular .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.paymentSource = null; $scope.plan = null; $scope.subscription = null; $scope.loading = true; + var license = null; + $scope.expiration = null; $scope.$on('$viewContentLoaded', function () { load(); }); $scope.changePayment = function () { + if ($scope.selfHosted) { + return; + } + var modal = $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsBillingChangePayment.html', @@ -30,6 +38,10 @@ }; $scope.changePlan = function () { + if ($scope.selfHosted) { + return; + } + var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationBillingChangePlan.html', @@ -47,6 +59,10 @@ }; $scope.adjustSeats = function (add) { + if ($scope.selfHosted) { + return; + } + var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationBillingAdjustSeats.html', @@ -64,6 +80,10 @@ }; $scope.adjustStorage = function (add) { + if ($scope.selfHosted) { + return; + } + var modal = $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsBillingAdjustStorage.html', @@ -81,6 +101,10 @@ }; $scope.verifyBank = function () { + if ($scope.selfHosted) { + return; + } + var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationBillingVerifyBank.html', @@ -93,6 +117,10 @@ }; $scope.cancel = function () { + if ($scope.selfHosted) { + return; + } + if (!confirm('Are you sure you want to cancel? All users will lose access to the organization ' + 'at the end of this billing cycle.')) { return; @@ -107,6 +135,10 @@ }; $scope.reinstate = function () { + if ($scope.selfHosted) { + return; + } + if (!confirm('Are you sure you want to remove the cancellation request and reinstate this organization?')) { 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() { apiService.organizations.getBilling({ id: $state.params.orgId }, function (org) { $scope.loading = false; $scope.noSubscription = org.PlanType === 0; var i = 0; + $scope.expiration = org.Expiration; + license = org.License; $scope.plan = { name: org.Plan, diff --git a/src/app/organization/organizationBillingUpdateLicenseController.js b/src/app/organization/organizationBillingUpdateLicenseController.js new file mode 100644 index 0000000000..0b8d3ba2a7 --- /dev/null +++ b/src/app/organization/organizationBillingUpdateLicenseController.js @@ -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'); + }; + }); diff --git a/src/app/organization/views/organizationBilling.html b/src/app/organization/views/organizationBilling.html index 81f7803a84..70a972c093 100644 --- a/src/app/organization/views/organizationBilling.html +++ b/src/app/organization/views/organizationBilling.html @@ -1,7 +1,7 @@ 

Billing - manage your payments + manage your billing & licensing

@@ -26,14 +26,28 @@
-
+
+
Name
+
{{plan.name || '-'}}
+
Expiration
+
+ Loading... +
+
+ {{expiration | date: 'medium'}} +
+
+ Never expires +
+
+
Name
{{plan.name || '-'}}
Total Seats
{{plan.seats || '-'}}
-
+
Status
@@ -41,11 +55,11 @@ - marked for cancellation
Next Charge
-
{{nextInvoice ? ((nextInvoice.date | date: format: mediumDate) + ', ' + (nextInvoice.amount | currency:'$')) : '-'}}
+
{{nextInvoice ? ((nextInvoice.date | date: 'mediumDate') + ', ' + (nextInvoice.amount | currency:'$')) : '-'}}
-
+
Details
@@ -67,7 +81,7 @@
-
@@ -93,7 +119,7 @@ You plan currently has a total of {{plan.seats}} seats.
- -
+

Storage

@@ -128,7 +154,7 @@
-
+

Payment Method

@@ -160,7 +186,7 @@
-
+

Charges

@@ -176,7 +202,7 @@ - {{charge.date | date: format: mediumDate}} + {{charge.date | date: 'mediumDate'}} {{charge.paymentSource}} diff --git a/src/app/services/apiService.js b/src/app/services/apiService.js index c77bf1974a..aa09661f42 100644 --- a/src/app/services/apiService.js +++ b/src/app/services/apiService.js @@ -70,7 +70,17 @@ putReinstate: { url: _apiUri + '/organizations/:id/reinstate', 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' } }, - 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', {}, { diff --git a/src/app/settings/settingsCreateOrganizationController.js b/src/app/settings/settingsCreateOrganizationController.js index 42d4c5c4ce..b22d79173e 100644 --- a/src/app/settings/settingsCreateOrganizationController.js +++ b/src/app/settings/settingsCreateOrganizationController.js @@ -2,10 +2,11 @@ .module('bit.settings') .controller('settingsCreateOrganizationController', function ($scope, $state, apiService, cryptoService, - toastr, $analytics, authService, stripe, constants) { + toastr, $analytics, authService, stripe, constants, appSettings, validationService) { $scope.plans = constants.plans; $scope.storageGb = constants.storageGb; $scope.paymentMethod = 'card'; + $scope.selfHosted = appSettings.selfHosted; $scope.model = { plan: 'free', @@ -52,50 +53,68 @@ } }; - $scope.submit = function (model) { - var shareKeyCt = cryptoService.makeShareKeyCt(); - - if (model.plan === 'free') { - var freeRequest = { - name: model.name, - planType: model.plan, - key: shareKeyCt, - billingEmail: model.billingEmail - }; - - $scope.submitPromise = apiService.organizations.post(freeRequest).$promise.then(finalizeCreate); - } - else { - var stripeReq = null; - if ($scope.paymentMethod === 'card') { - stripeReq = stripe.card.createToken(model.card); - } - else if ($scope.paymentMethod === 'bank') { - model.bank.currency = 'USD'; - model.bank.country = 'US'; - stripeReq = stripe.bankAccount.createToken(model.bank); - } - else { + $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; } - $scope.submitPromise = stripeReq.then(function (response) { - var paidRequest = { + 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(); + + if (model.plan === 'free') { + var freeRequest = { name: model.name, - planType: model.interval === 'month' ? $scope.plans[model.plan].monthPlanType : - $scope.plans[model.plan].annualPlanType, + planType: model.plan, key: shareKeyCt, - paymentToken: response.id, - additionalSeats: model.additionalSeats, - additionalStorageGb: model.additionalStorageGb, - billingEmail: model.billingEmail, - businessName: model.ownedBusiness ? model.businessName : null + billingEmail: model.billingEmail }; - return apiService.organizations.post(paidRequest).$promise; - }, function (err) { - throw err.message; - }).then(finalizeCreate); + $scope.submitPromise = apiService.organizations.post(freeRequest).$promise.then(finalizeCreate); + } + else { + var stripeReq = null; + if ($scope.paymentMethod === 'card') { + stripeReq = stripe.card.createToken(model.card); + } + else if ($scope.paymentMethod === 'bank') { + model.bank.currency = 'USD'; + model.bank.country = 'US'; + stripeReq = stripe.bankAccount.createToken(model.bank); + } + else { + return; + } + + $scope.submitPromise = stripeReq.then(function (response) { + var paidRequest = { + name: model.name, + planType: model.interval === 'month' ? $scope.plans[model.plan].monthPlanType : + $scope.plans[model.plan].annualPlanType, + key: shareKeyCt, + paymentToken: response.id, + additionalSeats: model.additionalSeats, + additionalStorageGb: model.additionalStorageGb, + billingEmail: model.billingEmail, + businessName: model.ownedBusiness ? model.businessName : null + }; + + return apiService.organizations.post(paidRequest).$promise; + }, function (err) { + throw err.message; + }).then(finalizeCreate); + } } function finalizeCreate(result) { diff --git a/src/app/settings/views/settingsCreateOrganization.html b/src/app/settings/views/settingsCreateOrganization.html index 242558cf32..3aaa2d4165 100644 --- a/src/app/settings/views/settingsCreateOrganization.html +++ b/src/app/settings/views/settingsCreateOrganization.html @@ -6,667 +6,691 @@ 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).

-
+

Errors have occurred

  • {{e}}
-
-
-

General Information

-
-
-
-
-
- - -
-
-
-
- - -
+
+
+
+

License

+
+
+

Create an organization with your license file.

+
+ + +

+ Your license file will be named something like bitwarden_organization_license.json +

-
- -
-
-
-
- - -
-
+
-
-
-

Choose Your Plan

-
-
-
- +
+
+
+

General Information

-
- -
-
- -
-
- -
-
- -
-
-
-

Additional Users (Seats)

-
-
-

- Your plan comes with {{plans[model.plan].baseSeats}} users (seats). You can add additional users - - (up to {{plans[model.plan].maxAdditionalSeats}} more) - - for {{plans[model.plan].seatPrice | currency:'$'}} per user /month. -

-
-
-
- - +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
+
+
+ + +
-
-
-
-

Users (Seats)

-
-
-

- How many user seats do you need? - You can also add additional seats later if needed. -

-
-
-
- - -
+
+
+

Choose Your Plan

+
+
+
+ +
+
+ +
+
+ +
+
+
+
-
-
-
-

Additional Storage

-
-
-
+
+
+

Additional Users (Seats)

+
+

- Your plan comes with 1 GB of encrypted file storage. You can add additional - storage for {{storageGb.price | currency:"$":2}} per GB /month. + Your plan comes with {{plans[model.plan].baseSeats}} users (seats). You can add additional users + + (up to {{plans[model.plan].maxAdditionalSeats}} more) + + for {{plans[model.plan].seatPrice | currency:'$'}} per user /month.

- - +
+ + +
-
-
-
-

Billing Summary

-
-
-
- +
+
+

Users (Seats)

-
- +
+

+ How many user seats do you need? + You can also add additional seats later if needed. +

+
+
+
+ + +
+
+
- -
-
-
-

Payment Information

-
-
- - -
-
-
-

You must verify your bank account

+
+
+

Additional Storage

+
+
+

- Payment with a bank account is only available to customers in the United States. - You will be required to verify your bank account. We will make two micro-deposits within the next - 1-2 business days. Enter these amounts in the organization's billing area to verify the bank account. - Failure to verify the bank account will result in a missed payment and your organization being - disabled. + Your plan comes with 1 GB of encrypted file storage. You can add additional + storage for {{storageGb.price | currency:"$":2}} per GB /month.

-
-
-
-
- - -
-
-
-
- - -
-
-
-
-
-
- - -
-
-
-
- - -
-
-
-
-
-
-
-
- - -
-
-
- -
    -
  • -
  • -
  • -
  • -
  • -
  • -
-
-
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
-
-
- - -
-
-
-
- - +
+
+ +
- diff --git a/src/index.html b/src/index.html index 65aa92d66b..7c7be586d1 100644 --- a/src/index.html +++ b/src/index.html @@ -202,6 +202,7 @@ +