From 69d601fa78f3aed5ebfcd6ac79ed163d126d42f6 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Mon, 3 Jul 2023 15:51:29 -0700 Subject: [PATCH] [AC-1418] Add secrets manager manage subscription component (#5661) * [AC-1423] Add minWidth input to bit-progress component * [AC-1423] Add ProgressModule to shared.module.ts * [AC-1423] Update cloud subscription page styles - Remove bootstrap styles - Use CL components where applicable - Use CL typography directives - Update heading levels to prepare for new SM sections * [AC-1423] Add usePasswordManager boolean to organization domain * [AC-1423] Introduce BitwardenProductType enum * [AC-1423] Update Organization subscription line items - Add product type prefix - Indent addon services like additional storage and service accounts - Show line items for free plans * [AC-1423] Simply sort function * [AC-1423] Remove header border * [AC-1423] Remove redundant condition * [AC-1423] Remove ineffective div * [AC-1423] Make "Password Manager" the default fallback for product name * Revert "[AC-1423] Add minWidth input to bit-progress component" This reverts commit 95b2223a30e45966988bfed2e08a9631fa8980b9. * [AC-1423] Remove minWidth attribute * [AC-1423] Switch to AddonProductType enum instead of boolean * Revert "[AC-1423] Switch to AddonProductType enum instead of boolean" This reverts commit 204f64b4e7f76cf43a7dbc6161332553047592df. * [AC-1423] Tweak sorting comment * [AC-1418] Add initial SecretsManagerAdjustSubscription component * [AC-1418] Add initial SM adjustment form * [AC-1418] Adjust organization-subscription-update.request.ts to support both PM and SM * [AC-1418] Rename service account fields in the options interface * [AC-1418] Add api service call to update SM subscription * [AC-1418] Cleanup form html * [AC-1418] Add missing SM plan properties * [AC-1418] Add SM subscription adjust form and logic to hide it * [AC-1418] Add better docs to options interface * [AC-1418] Fix conflicting required/optional labels for auto-scaling limits * [AC-1418] Adjust labels and appearance to better match design * [AC-1418] Use the SM plan for billing interval * [AC-1418] Hide SM billing adjustment component behind feature flag * [AC-1418] Update request model to match server * [AC-1418] Cleanup BitwardenProductType after merge Add to barrel file and update applicable imports. * [AC-1418] Revert change to update PM subscription request model * [AC-1418] Add new update SM subscription request model * [AC-1418] Add new service method to update SM subscription * [AC-1418] Use new model and service method * [AC-1418] Cleanup SM subscription UI flags * [AC-1418] Move SM adjust subscription component into SM billing module * [AC-1418] Update SM seat count minimum to 1 * [AC-1418] Add missing currency codes * [AC-1418] Simplify monthly price calculation * [AC-1418] Increase PM adjust subscription form input width * [AC-1418] Add check for null subscription --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> --- .../adjust-subscription.component.html | 4 +- ...nization-subscription-cloud.component.html | 8 + ...ganization-subscription-cloud.component.ts | 37 +++- .../secrets-manager/enroll.component.html | 0 .../sm-adjust-subscription.component.html | 92 ++++++++++ .../sm-adjust-subscription.component.ts | 168 ++++++++++++++++++ .../secrets-manager/sm-billing.module.ts | 13 +- .../settings/organization-plans.component.ts | 8 +- apps/web/src/locales/en/messages.json | 59 +++--- .../organization-api.service.abstraction.ts | 5 + .../models/response/organization.response.ts | 10 ++ .../organization/organization-api.service.ts | 14 ++ .../billing/enums/bitwarden-product-type.ts | 4 - libs/common/src/billing/enums/index.ts | 1 + ...nization-sm-subscription-update.request.ts | 40 +++++ ...rganization-subscription-update.request.ts | 22 ++- .../billing/models/response/plan.response.ts | 3 +- .../models/response/subscription.response.ts | 2 +- 18 files changed, 447 insertions(+), 43 deletions(-) create mode 100644 apps/web/src/app/billing/organizations/secrets-manager/enroll.component.html create mode 100644 apps/web/src/app/billing/organizations/secrets-manager/sm-adjust-subscription.component.html create mode 100644 apps/web/src/app/billing/organizations/secrets-manager/sm-adjust-subscription.component.ts delete mode 100644 libs/common/src/billing/enums/bitwarden-product-type.ts create mode 100644 libs/common/src/billing/models/request/organization-sm-subscription-update.request.ts diff --git a/apps/web/src/app/billing/organizations/adjust-subscription.component.html b/apps/web/src/app/billing/organizations/adjust-subscription.component.html index f7a6f616dbd..148d507a6a8 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.html +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.html @@ -1,7 +1,7 @@
-
+
{{ "maxSeatLimit" | i18n }}
+ +

{{ "secretsManager" | i18n }}

+ +

{{ "selfHostingTitle" | i18n }}

diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 667c88a5315..770645dae03 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -9,8 +9,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { PlanType } from "@bitwarden/common/billing/enums"; -import { BitwardenProductType } from "@bitwarden/common/billing/enums/bitwarden-product-type.enum"; +import { BitwardenProductType, PlanType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { BillingSubscriptionItemResponse } from "@bitwarden/common/billing/models/response/subscription.response"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -23,6 +22,7 @@ import { BillingSyncApiKeyComponent, BillingSyncApiModalData, } from "./billing-sync-api-key.component"; +import { SecretsManagerSubscriptionOptions } from "./secrets-manager/sm-adjust-subscription.component"; @Component({ selector: "app-org-subscription-cloud", @@ -38,6 +38,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy adjustStorageAdd = true; showAdjustStorage = false; hasBillingSyncToken: boolean; + showAdjustSecretsManager = false; showSecretsManagerSubscribe = false; @@ -113,15 +114,26 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.showSecretsManagerSubscribe = this.userOrg.canEditSubscription && !this.userOrg.useSecretsManager && + this.subscription != null && !this.subscription.cancelled && !this.subscriptionMarkedForCancel; - // Remove next line when the sm-ga-billing flag is deleted - this.showSecretsManagerSubscribe = - this.showSecretsManagerSubscribe && - (await this.configService.getFeatureFlagBool(FeatureFlag.SecretsManagerBilling)); + this.showAdjustSecretsManager = + this.userOrg.canEditSubscription && + this.userOrg.useSecretsManager && + this.subscription != null && + this.sub.secretsManagerPlan?.hasAdditionalSeatsOption && + !this.subscription.cancelled && + !this.subscriptionMarkedForCancel; this.loading = false; + + // Remove the remaining lines when the sm-ga-billing flag is deleted + const smBillingEnabled = await this.configService.getFeatureFlagBool( + FeatureFlag.SecretsManagerBilling + ); + this.showSecretsManagerSubscribe = this.showSecretsManagerSubscribe && smBillingEnabled; + this.showAdjustSecretsManager = this.showAdjustSecretsManager && smBillingEnabled; } get subscription() { @@ -169,6 +181,19 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy return this.sub.seats; } + get smOptions(): SecretsManagerSubscriptionOptions { + return { + seatCount: this.sub.smSeats, + seatLimit: this.sub.maxAutoscaleSmSeats, + seatPrice: this.sub.secretsManagerPlan.seatPrice, + serviceAccountLimit: this.sub.maxAutoscaleSmServiceAccounts, + serviceAccountCount: this.sub.smServiceAccounts, + interval: this.sub.secretsManagerPlan.isAnnual ? "year" : "month", + additionalServiceAccountPrice: this.sub.secretsManagerPlan.additionalPricePerServiceAccount, + baseServiceAccountCount: this.sub.secretsManagerPlan.baseServiceAccount, + }; + } + get maxAutoscaleSeats() { return this.sub.maxAutoscaleSeats; } diff --git a/apps/web/src/app/billing/organizations/secrets-manager/enroll.component.html b/apps/web/src/app/billing/organizations/secrets-manager/enroll.component.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/web/src/app/billing/organizations/secrets-manager/sm-adjust-subscription.component.html b/apps/web/src/app/billing/organizations/secrets-manager/sm-adjust-subscription.component.html new file mode 100644 index 00000000000..d3da809aad8 --- /dev/null +++ b/apps/web/src/app/billing/organizations/secrets-manager/sm-adjust-subscription.component.html @@ -0,0 +1,92 @@ + + + {{ "subscriptionSeats" | i18n }} + + + {{ "total" | i18n }}: + {{ formGroup.value.seatCount || 0 }} × {{ options.seatPrice | currency : "$" }} = + {{ seatTotal | currency : "$" }} / {{ options.interval | i18n }} + + + + {{ "limitSubscription" | i18n }} + + + {{ "limitSmSubscriptionDesc" | i18n }} + + + + {{ "maxSeatLimit" | i18n }} + + + {{ "maxSeatCost" | i18n }}: + {{ formGroup.value.seatLimit || 0 }} × {{ options.seatPrice | currency : "$" }} = + {{ maxSeatTotal | currency : "$" }} / {{ options.interval | i18n }} + + + + {{ "additionalServiceAccounts" | i18n }} + + +
+ {{ + "additionalServiceAccountsDesc" + | i18n : options.baseServiceAccountCount : (monthlyServiceAccountPrice | currency : "$") + }} +
+
+ {{ "total" | i18n }}: + {{ formGroup.value.serviceAccountCount || 0 }} × + {{ options.additionalServiceAccountPrice | currency : "$" }} = + {{ serviceAccountTotal | currency : "$" }} / {{ options.interval | i18n }} +
+
+
+ + {{ "limitServiceAccounts" | i18n }} + + + {{ "limitServiceAccountsDesc" | i18n }} + + + + {{ "serviceAccountLimit" | i18n }} + + + {{ "maxServiceAccountCost" | i18n }}: + {{ formGroup.value.serviceAccountLimit || 0 }} × + {{ options.additionalServiceAccountPrice | currency : "$" }} = + {{ maxServiceAccountTotal | currency : "$" }} / {{ options.interval | i18n }} + + + + + diff --git a/apps/web/src/app/billing/organizations/secrets-manager/sm-adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/secrets-manager/sm-adjust-subscription.component.ts new file mode 100644 index 00000000000..0b27da37239 --- /dev/null +++ b/apps/web/src/app/billing/organizations/secrets-manager/sm-adjust-subscription.component.ts @@ -0,0 +1,168 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; +import { Subject, takeUntil } from "rxjs"; + +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationSmSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-sm-subscription-update.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +export interface SecretsManagerSubscriptionOptions { + interval: "year" | "month"; + + /** + * The current number of seats the organization subscribes to. + */ + seatCount: number; + + /** + * Optional auto-scaling limit for the number of seats the organization can subscribe to. + */ + seatLimit: number; + + /** + * The price per seat for the subscription. + */ + seatPrice: number; + + /** + * The number of service accounts that are included in the base subscription. + */ + baseServiceAccountCount: number; + + /** + * The current number of additional service accounts the organization subscribes to. + */ + serviceAccountCount: number; + + /** + * Optional auto-scaling limit for the number of additional service accounts the organization can subscribe to. + */ + serviceAccountLimit: number; + + /** + * The price per additional service account for the subscription. + */ + additionalServiceAccountPrice: number; +} + +@Component({ + selector: "app-sm-adjust-subscription", + templateUrl: "sm-adjust-subscription.component.html", +}) +export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDestroy { + @Input() organizationId: string; + @Input() options: SecretsManagerSubscriptionOptions; + @Output() onAdjusted = new EventEmitter(); + + private destroy$ = new Subject(); + + formGroup = this.formBuilder.group({ + seatCount: [0, [Validators.required, Validators.min(1)]], + limitSeats: [false], + seatLimit: [null as number | null], + serviceAccountCount: [0, [Validators.required, Validators.min(0)]], + limitServiceAccounts: [false], + serviceAccountLimit: [null as number | null], + }); + + get monthlyServiceAccountPrice(): number { + return this.options.interval == "month" + ? this.options.additionalServiceAccountPrice + : this.options.additionalServiceAccountPrice / 12; + } + + get serviceAccountTotal(): number { + return Math.abs( + this.formGroup.value.serviceAccountCount * this.options.additionalServiceAccountPrice + ); + } + + get seatTotal(): number { + return Math.abs(this.formGroup.value.seatCount * this.options.seatPrice); + } + + get maxServiceAccountTotal(): number { + return Math.abs( + (this.formGroup.value.serviceAccountLimit ?? 0) * this.options.additionalServiceAccountPrice + ); + } + + get maxSeatTotal(): number { + return Math.abs((this.formGroup.value.seatLimit ?? 0) * this.options.seatPrice); + } + + constructor( + private formBuilder: FormBuilder, + private organizationApiService: OrganizationApiServiceAbstraction, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService + ) {} + + ngOnInit() { + this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => { + const seatLimitControl = this.formGroup.controls.seatLimit; + const serviceAccountLimitControl = this.formGroup.controls.serviceAccountLimit; + + if (value.limitSeats) { + seatLimitControl.setValidators([Validators.min(value.seatCount)]); + seatLimitControl.enable({ emitEvent: false }); + } else { + seatLimitControl.disable({ emitEvent: false }); + } + + if (value.limitServiceAccounts) { + serviceAccountLimitControl.setValidators([Validators.min(value.serviceAccountCount)]); + serviceAccountLimitControl.enable({ emitEvent: false }); + } else { + serviceAccountLimitControl.disable({ emitEvent: false }); + } + }); + + this.formGroup.patchValue({ + seatCount: this.options.seatCount, + seatLimit: this.options.seatLimit, + serviceAccountCount: this.options.serviceAccountCount, + serviceAccountLimit: this.options.serviceAccountLimit, + limitSeats: this.options.seatLimit != null, + limitServiceAccounts: this.options.serviceAccountLimit != null, + }); + } + + submit = async () => { + this.formGroup.markAllAsTouched(); + + if (this.formGroup.invalid) { + return; + } + + const seatAdjustment = this.formGroup.value.seatCount - this.options.seatCount; + const serviceAccountAdjustment = + this.formGroup.value.serviceAccountCount - this.options.serviceAccountCount; + + const request = new OrganizationSmSubscriptionUpdateRequest( + seatAdjustment, + serviceAccountAdjustment, + this.formGroup.value.seatLimit, + this.formGroup.value.serviceAccountLimit + ); + + await this.organizationApiService.updateSecretsManagerSubscription( + this.organizationId, + request + ); + + await this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("subscriptionUpdated") + ); + + this.onAdjusted.emit(); + }; + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/apps/web/src/app/billing/organizations/secrets-manager/sm-billing.module.ts b/apps/web/src/app/billing/organizations/secrets-manager/sm-billing.module.ts index a46286fc5a3..127a6e49fe6 100644 --- a/apps/web/src/app/billing/organizations/secrets-manager/sm-billing.module.ts +++ b/apps/web/src/app/billing/organizations/secrets-manager/sm-billing.module.ts @@ -2,12 +2,21 @@ import { NgModule } from "@angular/core"; import { SharedModule } from "../../../shared"; +import { SecretsManagerAdjustSubscriptionComponent } from "./sm-adjust-subscription.component"; import { SecretsManagerSubscribeStandaloneComponent } from "./sm-subscribe-standalone.component"; import { SecretsManagerSubscribeComponent } from "./sm-subscribe.component"; @NgModule({ imports: [SharedModule], - declarations: [SecretsManagerSubscribeComponent, SecretsManagerSubscribeStandaloneComponent], - exports: [SecretsManagerSubscribeComponent, SecretsManagerSubscribeStandaloneComponent], + declarations: [ + SecretsManagerSubscribeComponent, + SecretsManagerSubscribeStandaloneComponent, + SecretsManagerAdjustSubscriptionComponent, + ], + exports: [ + SecretsManagerSubscribeComponent, + SecretsManagerSubscribeStandaloneComponent, + SecretsManagerAdjustSubscriptionComponent, + ], }) export class SecretsManagerBillingModule {} diff --git a/apps/web/src/app/billing/settings/organization-plans.component.ts b/apps/web/src/app/billing/settings/organization-plans.component.ts index 23a38c654a9..7d9f76708ea 100644 --- a/apps/web/src/app/billing/settings/organization-plans.component.ts +++ b/apps/web/src/app/billing/settings/organization-plans.component.ts @@ -20,8 +20,7 @@ import { OrganizationCreateRequest } from "@bitwarden/common/admin-console/model import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/models/request/organization-upgrade.request"; import { ProviderOrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-organization-create.request"; -import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums"; -import { BitwardenProductType } from "@bitwarden/common/billing/enums/bitwarden-product-type"; +import { BitwardenProductType, PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { ProductType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -56,24 +55,29 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { @Input() showFree = true; @Input() showCancel = false; @Input() acceptingSponsorship = false; + @Input() get product(): ProductType { return this._product; } + set product(product: ProductType) { this._product = product; this.formGroup?.controls?.product?.setValue(product); } + private _product = ProductType.Free; @Input() get plan(): PlanType { return this._plan; } + set plan(plan: PlanType) { this._plan = plan; this.formGroup?.controls?.plan?.setValue(plan); } + private _plan = PlanType.Free; @Input() providerId?: string; @Output() onSuccess = new EventEmitter(); diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index b882f5ef889..ff24551f891 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -404,8 +404,7 @@ "viewItem": { "message": "View item" }, - "new": - { + "new": { "message": "New", "description": "for adding new items" }, @@ -3324,6 +3323,9 @@ "limitSubscriptionDesc": { "message": "Set a seat limit for your subscription. Once this limit is reached, you will not be able to invite new members." }, + "limitSmSubscriptionDesc": { + "message": "Set a seat limit for your Secrets Manger subscription. Once this limit is reached, you will not be able to invite new members." + }, "maxSeatLimit": { "message": "Seat Limit (optional)", "description": "Upper limit of seats to allow through autoscaling" @@ -5855,10 +5857,10 @@ "message": "Delete secrets", "description": "The action to delete multiple secrets from the system." }, - "hardDeleteSecret":{ + "hardDeleteSecret": { "message": "Permanently delete secret" }, - "hardDeleteSecrets":{ + "hardDeleteSecrets": { "message": "Permanently delete secrets" }, "secretProjectAssociationDescription": { @@ -5937,14 +5939,14 @@ "message": "To get started, add a new secret or import secrets.", "description": "Message to encourage the user to start adding secrets." }, - "secretsTrashNoItemsMessage":{ + "secretsTrashNoItemsMessage": { "message": "There are no secrets in the trash." }, "serviceAccountsNoItemsMessage": { "message": "Create a new service account to get started automating secret access.", "description": "Message to encourage the user to start creating service accounts." }, - "serviceAccountsNoItemsTitle": { + "serviceAccountsNoItemsTitle": { "message": "Nothing to show yet", "description": "Title to indicate that there are no service accounts to display." }, @@ -5965,7 +5967,7 @@ "description": "Action to view the details of a service account." }, "deleteServiceAccountDialogMessage": { - "message": "Deleting service account $SERVICE_ACCOUNT$ is permanent and irreversible.", + "message": "Deleting service account $SERVICE_ACCOUNT$ is permanent and irreversible.", "placeholders": { "service_account": { "content": "$1", @@ -5973,11 +5975,11 @@ } } }, - "deleteServiceAccountsDialogMessage":{ + "deleteServiceAccountsDialogMessage": { "message": "Deleting service accounts is permanent and irreversible." }, - "deleteServiceAccountsConfirmMessage":{ - "message": "Delete $COUNT$ service accounts", + "deleteServiceAccountsConfirmMessage": { + "message": "Delete $COUNT$ service accounts", "placeholders": { "count": { "content": "$1", @@ -5985,19 +5987,19 @@ } } }, - "deleteServiceAccountToast":{ + "deleteServiceAccountToast": { "message": "Service account deleted" }, - "deleteServiceAccountsToast":{ + "deleteServiceAccountsToast": { "message": "Service accounts deleted" }, "searchServiceAccounts": { "message": "Search service accounts", "description": "Placeholder text for searching service accounts." }, - "editServiceAccount":{ - "message":"Edit service account", - "description" : "Title for editing a service account." + "editServiceAccount": { + "message": "Edit service account", + "description": "Title for editing a service account." }, "addProject": { "message": "Add project", @@ -6037,8 +6039,8 @@ "hardDeleteSecretsConfirmation": { "message": "Are you sure you want to permanently delete these secrets?" }, - "hardDeletesSuccessToast":{ - "message":"Secrets permanently deleted" + "hardDeletesSuccessToast": { + "message": "Secrets permanently deleted" }, "smAccess": { "message": "Access", @@ -6052,7 +6054,7 @@ "message": "Service account name", "description": "Label for the name of a service account" }, - "serviceAccountCreated": { + "serviceAccountCreated": { "message": "Service account created", "description": "Notifies that a new service account has been created" }, @@ -6140,8 +6142,8 @@ "message": "Secret sent to trash", "description": "Notification to be displayed when a secret is successfully sent to the trash." }, - "hardDeleteSuccessToast":{ - "message":"Secret permanently deleted" + "hardDeleteSuccessToast": { + "message": "Secret permanently deleted" }, "accessTokens": { "message": "Access tokens", @@ -6844,8 +6846,8 @@ "message": "with automatic enrollment will turn on when this option is used.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The master password reset policy with automatic enrollment will turn on when this option is used.'" }, - "notFound":{ - "message": "$RESOURCE$ not found", + "notFound": { + "message": "$RESOURCE$ not found", "placeholders": { "resource": { "content": "$1", @@ -6998,6 +7000,17 @@ }, "freeOrganization": { "message": "Free Organization" + }, + "limitServiceAccounts": { + "message": "Limit service accounts (optional)" + }, + "limitServiceAccountsDesc": { + "message": "Set a limit for your service accounts. Once this limit is reached, you will not be able to create new service accounts." + }, + "serviceAccountLimit": { + "message": "Service account limit (optional)" + }, + "maxServiceAccountCost": { + "message": "Max potential service account cost" } } - diff --git a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts index c6ce62f232b..b6778cf61e6 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts @@ -3,6 +3,7 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; import { ApiKeyResponse } from "../../../auth/models/response/api-key.response"; import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response"; +import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request"; import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/request/organization-tax-info-update.request"; import { PaymentRequest } from "../../../billing/models/request/payment.request"; @@ -41,6 +42,10 @@ export class OrganizationApiServiceAbstraction { id: string, request: OrganizationSubscriptionUpdateRequest ) => Promise; + updateSecretsManagerSubscription: ( + id: string, + request: OrganizationSmSubscriptionUpdateRequest + ) => Promise; updateSeats: (id: string, request: SeatRequest) => Promise; updateStorage: (id: string, request: StorageRequest) => Promise; verifyBank: (id: string, request: VerifyBankRequest) => Promise; diff --git a/libs/common/src/admin-console/models/response/organization.response.ts b/libs/common/src/admin-console/models/response/organization.response.ts index 6bd339a64de..b248c6d0df9 100644 --- a/libs/common/src/admin-console/models/response/organization.response.ts +++ b/libs/common/src/admin-console/models/response/organization.response.ts @@ -28,6 +28,11 @@ export class OrganizationResponse extends BaseResponse { useResetPassword: boolean; useSecretsManager: boolean; hasPublicAndPrivateKeys: boolean; + usePasswordManager: boolean; + smSeats?: number; + smServiceAccounts?: number; + maxAutoscaleSmSeats?: number; + maxAutoscaleSmServiceAccounts?: number; constructor(response: any) { super(response); @@ -62,5 +67,10 @@ export class OrganizationResponse extends BaseResponse { this.useResetPassword = this.getResponseProperty("UseResetPassword"); this.useSecretsManager = this.getResponseProperty("UseSecretsManager"); this.hasPublicAndPrivateKeys = this.getResponseProperty("HasPublicAndPrivateKeys"); + this.usePasswordManager = this.getResponseProperty("UsePasswordManager"); + this.smSeats = this.getResponseProperty("SmSeats"); + this.smServiceAccounts = this.getResponseProperty("SmServiceAccounts"); + this.maxAutoscaleSmSeats = this.getResponseProperty("MaxAutoscaleSmSeats"); + this.maxAutoscaleSmServiceAccounts = this.getResponseProperty("MaxAutoscaleSmServiceAccounts"); } } diff --git a/libs/common/src/admin-console/services/organization/organization-api.service.ts b/libs/common/src/admin-console/services/organization/organization-api.service.ts index 503aeb3820f..c235b4b8eea 100644 --- a/libs/common/src/admin-console/services/organization/organization-api.service.ts +++ b/libs/common/src/admin-console/services/organization/organization-api.service.ts @@ -4,6 +4,7 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; import { ApiKeyResponse } from "../../../auth/models/response/api-key.response"; import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response"; +import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request"; import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/request/organization-tax-info-update.request"; import { PaymentRequest } from "../../../billing/models/request/payment.request"; @@ -133,6 +134,19 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction ); } + async updateSecretsManagerSubscription( + id: string, + request: OrganizationSmSubscriptionUpdateRequest + ): Promise { + return this.apiService.send( + "POST", + "/organizations/" + id + "/sm-subscription", + request, + true, + false + ); + } + async updateSeats(id: string, request: SeatRequest): Promise { const r = await this.apiService.send( "POST", diff --git a/libs/common/src/billing/enums/bitwarden-product-type.ts b/libs/common/src/billing/enums/bitwarden-product-type.ts deleted file mode 100644 index 76b0899fd9c..00000000000 --- a/libs/common/src/billing/enums/bitwarden-product-type.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum BitwardenProductType { - PasswordManager = 0, - SecretsManager = 1, -} diff --git a/libs/common/src/billing/enums/index.ts b/libs/common/src/billing/enums/index.ts index b4f96cd8fd6..70a3495a8bf 100644 --- a/libs/common/src/billing/enums/index.ts +++ b/libs/common/src/billing/enums/index.ts @@ -2,3 +2,4 @@ export * from "./payment-method-type.enum"; export * from "./plan-sponsorship-type.enum"; export * from "./plan-type.enum"; export * from "./transaction-type.enum"; +export * from "./bitwarden-product-type.enum"; diff --git a/libs/common/src/billing/models/request/organization-sm-subscription-update.request.ts b/libs/common/src/billing/models/request/organization-sm-subscription-update.request.ts new file mode 100644 index 00000000000..a69937d2311 --- /dev/null +++ b/libs/common/src/billing/models/request/organization-sm-subscription-update.request.ts @@ -0,0 +1,40 @@ +export class OrganizationSmSubscriptionUpdateRequest { + /** + * The number of seats to add or remove from the subscription. + */ + seatAdjustment: number; + + /** + * The maximum number of seats that can be auto-scaled for the subscription. + */ + maxAutoscaleSeats?: number; + + /** + * The number of additional service accounts to add or remove from the subscription. + */ + serviceAccountAdjustment: number; + + /** + * The maximum number of additional service accounts that can be auto-scaled for the subscription. + */ + maxAutoscaleServiceAccounts?: number; + + /** + * Build a subscription update request for the Secrets Manager product type. + * @param seatAdjustment - The number of seats to add or remove from the subscription. + * @param serviceAccountAdjustment - The number of additional service accounts to add or remove from the subscription. + * @param maxAutoscaleSeats - The maximum number of seats that can be auto-scaled for the subscription. + * @param maxAutoscaleServiceAccounts - The maximum number of additional service accounts that can be auto-scaled for the subscription. + */ + constructor( + seatAdjustment: number, + serviceAccountAdjustment: number, + maxAutoscaleSeats?: number, + maxAutoscaleServiceAccounts?: number + ) { + this.seatAdjustment = seatAdjustment; + this.serviceAccountAdjustment = serviceAccountAdjustment; + this.maxAutoscaleSeats = maxAutoscaleSeats; + this.maxAutoscaleServiceAccounts = maxAutoscaleServiceAccounts; + } +} diff --git a/libs/common/src/billing/models/request/organization-subscription-update.request.ts b/libs/common/src/billing/models/request/organization-subscription-update.request.ts index 9db3d4be80e..d7566806f6b 100644 --- a/libs/common/src/billing/models/request/organization-subscription-update.request.ts +++ b/libs/common/src/billing/models/request/organization-subscription-update.request.ts @@ -1,3 +1,23 @@ export class OrganizationSubscriptionUpdateRequest { - constructor(public seatAdjustment: number, public maxAutoscaleSeats?: number) {} + /** + * The number of seats to add or remove from the subscription. + * Applies to both PM and SM request types. + */ + seatAdjustment: number; + + /** + * The maximum number of seats that can be auto-scaled for the subscription. + * Applies to both PM and SM request types. + */ + maxAutoscaleSeats?: number; + + /** + * Build a subscription update request for the Password Manager product type. + * @param seatAdjustment - The number of seats to add or remove from the subscription. + * @param maxAutoscaleSeats - The maximum number of seats that can be auto-scaled for the subscription. + */ + constructor(seatAdjustment: number, maxAutoscaleSeats?: number) { + this.seatAdjustment = seatAdjustment; + this.maxAutoscaleSeats = maxAutoscaleSeats; + } } diff --git a/libs/common/src/billing/models/response/plan.response.ts b/libs/common/src/billing/models/response/plan.response.ts index e67ed35e44b..c3558a1b534 100644 --- a/libs/common/src/billing/models/response/plan.response.ts +++ b/libs/common/src/billing/models/response/plan.response.ts @@ -1,7 +1,6 @@ import { ProductType } from "../../../enums"; import { BaseResponse } from "../../../models/response/base.response"; -import { PlanType } from "../../enums"; -import { BitwardenProductType } from "../../enums/bitwarden-product-type"; +import { BitwardenProductType, PlanType } from "../../enums"; export class PlanResponse extends BaseResponse { type: PlanType; diff --git a/libs/common/src/billing/models/response/subscription.response.ts b/libs/common/src/billing/models/response/subscription.response.ts index f966a3e9bfa..ffcc9c23768 100644 --- a/libs/common/src/billing/models/response/subscription.response.ts +++ b/libs/common/src/billing/models/response/subscription.response.ts @@ -1,5 +1,5 @@ import { BaseResponse } from "../../../models/response/base.response"; -import { BitwardenProductType } from "../../enums/bitwarden-product-type.enum"; +import { BitwardenProductType } from "../../enums"; export class SubscriptionResponse extends BaseResponse { storageName: string;