From 3c2d3669c56f0d6055732453d75b1ed496e4741b Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Tue, 16 Apr 2024 15:26:30 -0400 Subject: [PATCH 01/10] Bumped web version to (#8772) --- apps/web/package.json | 2 +- package-lock.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 99828bb5439..55fe0987d72 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.4.0", + "version": "2024.4.1", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index c399536cca1..096f6653cb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -247,7 +247,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.4.0" + "version": "2024.4.1" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From d6f2965367dbf583350b3bbe19b06cc109fd5665 Mon Sep 17 00:00:00 2001 From: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com> Date: Wed, 17 Apr 2024 01:07:47 +0530 Subject: [PATCH 02/10] [PM-5015] org billing history view component migration (#8302) --- .../organization-billing-history-view.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html index 9df05d02edd..087009b2910 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html +++ b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html @@ -16,11 +16,11 @@ - {{ "loading" | i18n }} + {{ "loading" | i18n }} From f5198e86fd8a2187e3c9e528ba788edf2595fa2d Mon Sep 17 00:00:00 2001 From: KiruthigaManivannan <162679756+KiruthigaManivannan@users.noreply.github.com> Date: Wed, 17 Apr 2024 01:08:19 +0530 Subject: [PATCH 03/10] PM-5019 Migrate Adjust Payment component (#8383) * PM-5019 Migrated Adjust Payment component * PM-5019 Migrated Adjust Payment dialog component * PM-5019 Removing type any * PM-5019 Addressed review comments * PM-5019 Included deleted line space --- .../adjust-payment-dialog.component.html | 25 ++++ .../shared/adjust-payment-dialog.component.ts | 110 ++++++++++++++++++ .../shared/adjust-payment.component.html | 19 --- .../shared/adjust-payment.component.ts | 90 -------------- .../billing/shared/billing-shared.module.ts | 4 +- .../shared/payment-method.component.html | 18 +-- .../shared/payment-method.component.ts | 28 +++-- 7 files changed, 155 insertions(+), 139 deletions(-) create mode 100644 apps/web/src/app/billing/shared/adjust-payment-dialog.component.html create mode 100644 apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts delete mode 100644 apps/web/src/app/billing/shared/adjust-payment.component.html delete mode 100644 apps/web/src/app/billing/shared/adjust-payment.component.ts diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.html new file mode 100644 index 00000000000..0f92b023b15 --- /dev/null +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.html @@ -0,0 +1,25 @@ +
+ + + + + + + + + + +
diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts new file mode 100644 index 00000000000..41d0ad7e7af --- /dev/null +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts @@ -0,0 +1,110 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject, ViewChild } from "@angular/core"; +import { FormGroup } from "@angular/forms"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; +import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { DialogService } from "@bitwarden/components"; + +import { PaymentComponent } from "./payment.component"; +import { TaxInfoComponent } from "./tax-info.component"; + +export interface AdjustPaymentDialogData { + organizationId: string; + currentType: PaymentMethodType; +} + +export enum AdjustPaymentDialogResult { + Adjusted = "adjusted", + Cancelled = "cancelled", +} + +@Component({ + templateUrl: "adjust-payment-dialog.component.html", +}) +export class AdjustPaymentDialogComponent { + @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; + @ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent; + + organizationId: string; + currentType: PaymentMethodType; + paymentMethodType = PaymentMethodType; + + protected DialogResult = AdjustPaymentDialogResult; + protected formGroup = new FormGroup({}); + + constructor( + private dialogRef: DialogRef, + @Inject(DIALOG_DATA) protected data: AdjustPaymentDialogData, + private apiService: ApiService, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private logService: LogService, + private organizationApiService: OrganizationApiServiceAbstraction, + private paymentMethodWarningService: PaymentMethodWarningService, + ) { + this.organizationId = data.organizationId; + this.currentType = data.currentType; + } + + submit = async () => { + const request = new PaymentRequest(); + const response = this.paymentComponent.createPaymentToken().then((result) => { + request.paymentToken = result[0]; + request.paymentMethodType = result[1]; + request.postalCode = this.taxInfoComponent.taxInfo.postalCode; + request.country = this.taxInfoComponent.taxInfo.country; + if (this.organizationId == null) { + return this.apiService.postAccountPayment(request); + } else { + request.taxId = this.taxInfoComponent.taxInfo.taxId; + request.state = this.taxInfoComponent.taxInfo.state; + request.line1 = this.taxInfoComponent.taxInfo.line1; + request.line2 = this.taxInfoComponent.taxInfo.line2; + request.city = this.taxInfoComponent.taxInfo.city; + request.state = this.taxInfoComponent.taxInfo.state; + return this.organizationApiService.updatePayment(this.organizationId, request); + } + }); + await response; + if (this.organizationId) { + await this.paymentMethodWarningService.removeSubscriptionRisk(this.organizationId); + } + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("updatedPaymentMethod"), + ); + this.dialogRef.close(AdjustPaymentDialogResult.Adjusted); + }; + + changeCountry() { + if (this.taxInfoComponent.taxInfo.country === "US") { + this.paymentComponent.hideBank = !this.organizationId; + } else { + this.paymentComponent.hideBank = true; + if (this.paymentComponent.method === PaymentMethodType.BankAccount) { + this.paymentComponent.method = PaymentMethodType.Card; + this.paymentComponent.changeMethod(); + } + } + } +} + +/** + * Strongly typed helper to open a AdjustPaymentDialog + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param config Configuration for the dialog + */ +export function openAdjustPaymentDialog( + dialogService: DialogService, + config: DialogConfig, +) { + return dialogService.open(AdjustPaymentDialogComponent, config); +} diff --git a/apps/web/src/app/billing/shared/adjust-payment.component.html b/apps/web/src/app/billing/shared/adjust-payment.component.html deleted file mode 100644 index 724e7a44c2a..00000000000 --- a/apps/web/src/app/billing/shared/adjust-payment.component.html +++ /dev/null @@ -1,19 +0,0 @@ -
-
- -

- {{ (currentType != null ? "changePaymentMethod" : "addPaymentMethod") | i18n }} -

- - - - -
-
diff --git a/apps/web/src/app/billing/shared/adjust-payment.component.ts b/apps/web/src/app/billing/shared/adjust-payment.component.ts deleted file mode 100644 index 74523441417..00000000000 --- a/apps/web/src/app/billing/shared/adjust-payment.component.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; - -import { PaymentComponent } from "./payment.component"; -import { TaxInfoComponent } from "./tax-info.component"; - -@Component({ - selector: "app-adjust-payment", - templateUrl: "adjust-payment.component.html", -}) -export class AdjustPaymentComponent { - @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; - @ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent; - - @Input() currentType?: PaymentMethodType; - @Input() organizationId: string; - @Output() onAdjusted = new EventEmitter(); - @Output() onCanceled = new EventEmitter(); - - paymentMethodType = PaymentMethodType; - formPromise: Promise; - - constructor( - private apiService: ApiService, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private logService: LogService, - private organizationApiService: OrganizationApiServiceAbstraction, - private paymentMethodWarningService: PaymentMethodWarningService, - ) {} - - async submit() { - try { - const request = new PaymentRequest(); - this.formPromise = this.paymentComponent.createPaymentToken().then((result) => { - request.paymentToken = result[0]; - request.paymentMethodType = result[1]; - request.postalCode = this.taxInfoComponent.taxInfo.postalCode; - request.country = this.taxInfoComponent.taxInfo.country; - if (this.organizationId == null) { - return this.apiService.postAccountPayment(request); - } else { - request.taxId = this.taxInfoComponent.taxInfo.taxId; - request.state = this.taxInfoComponent.taxInfo.state; - request.line1 = this.taxInfoComponent.taxInfo.line1; - request.line2 = this.taxInfoComponent.taxInfo.line2; - request.city = this.taxInfoComponent.taxInfo.city; - request.state = this.taxInfoComponent.taxInfo.state; - return this.organizationApiService.updatePayment(this.organizationId, request); - } - }); - await this.formPromise; - if (this.organizationId) { - await this.paymentMethodWarningService.removeSubscriptionRisk(this.organizationId); - } - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("updatedPaymentMethod"), - ); - this.onAdjusted.emit(); - } catch (e) { - this.logService.error(e); - } - } - - cancel() { - this.onCanceled.emit(); - } - - changeCountry() { - if (this.taxInfoComponent.taxInfo.country === "US") { - this.paymentComponent.hideBank = !this.organizationId; - } else { - this.paymentComponent.hideBank = true; - if (this.paymentComponent.method === PaymentMethodType.BankAccount) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); - } - } - } -} diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts index 2f773870aad..65a651b73df 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -4,7 +4,7 @@ import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; import { AddCreditComponent } from "./add-credit.component"; -import { AdjustPaymentComponent } from "./adjust-payment.component"; +import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog.component"; import { AdjustStorageComponent } from "./adjust-storage.component"; import { BillingHistoryComponent } from "./billing-history.component"; import { OffboardingSurveyComponent } from "./offboarding-survey.component"; @@ -18,7 +18,7 @@ import { UpdateLicenseComponent } from "./update-license.component"; imports: [SharedModule, PaymentComponent, TaxInfoComponent, HeaderModule], declarations: [ AddCreditComponent, - AdjustPaymentComponent, + AdjustPaymentDialogComponent, AdjustStorageComponent, BillingHistoryComponent, PaymentMethodComponent, diff --git a/apps/web/src/app/billing/shared/payment-method.component.html b/apps/web/src/app/billing/shared/payment-method.component.html index cfe98178b06..5f78294fa64 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.html +++ b/apps/web/src/app/billing/shared/payment-method.component.html @@ -15,7 +15,7 @@
- +

{{ "paymentMethod" | i18n }}

@@ -102,23 +102,9 @@ {{ paymentSource.description }}

- - -

{{ "paymentChargedWithUnpaidSubscription" | i18n }}

{{ "taxInformation" | i18n }}

diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index d2b65968c34..fee97cb912a 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, FormControl, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; +import { lastValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; @@ -14,6 +15,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; +import { + AdjustPaymentDialogResult, + openAdjustPaymentDialog, +} from "./adjust-payment-dialog.component"; import { TaxInfoComponent } from "./tax-info.component"; @Component({ @@ -25,7 +30,6 @@ export class PaymentMethodComponent implements OnInit { loading = false; firstLoaded = false; - showAdjustPayment = false; showAddCredit = false; billing: BillingPaymentResponse; org: OrganizationSubscriptionResponse; @@ -120,18 +124,18 @@ export class PaymentMethodComponent implements OnInit { } } - changePayment() { - this.showAdjustPayment = true; - } - - closePayment(load: boolean) { - this.showAdjustPayment = false; - if (load) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); + changePayment = async () => { + const dialogRef = openAdjustPaymentDialog(this.dialogService, { + data: { + organizationId: this.organizationId, + currentType: this.paymentSource !== null ? this.paymentSource.type : null, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustPaymentDialogResult.Adjusted) { + await this.load(); } - } + }; async verifyBank() { if (this.loading || !this.forOrganization) { From 5d3541dd6379f422f0f740c3702367b58c8898f8 Mon Sep 17 00:00:00 2001 From: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com> Date: Wed, 17 Apr 2024 01:09:05 +0530 Subject: [PATCH 04/10] [PM-5013] migrate change plan component (#8407) * change plan component migration * change plan component migration --------- Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com> --- .../organizations/change-plan.component.html | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/billing/organizations/change-plan.component.html b/apps/web/src/app/billing/organizations/change-plan.component.html index b9a15be5ea7..a25dde4fd30 100644 --- a/apps/web/src/app/billing/organizations/change-plan.component.html +++ b/apps/web/src/app/billing/organizations/change-plan.component.html @@ -1,10 +1,18 @@ -
-
- -

{{ "changeBillingPlan" | i18n }}

-

{{ "changeBillingPlanUpgrade" | i18n }}

+
+
+ +

{{ "changeBillingPlan" | i18n }}

+

{{ "changeBillingPlanUpgrade" | i18n }}

Date: Wed, 17 Apr 2024 01:09:53 +0530 Subject: [PATCH 05/10] [PM-5021] billing history component migration (#8042) * billing history component migration * billing history component migration * billing history component migration * billing history component migration * billing history component migration --------- Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com> --- .../billing-history-view.component.html | 11 +- .../shared/billing-history.component.html | 135 +++++++++--------- 2 files changed, 76 insertions(+), 70 deletions(-) diff --git a/apps/web/src/app/billing/individual/billing-history-view.component.html b/apps/web/src/app/billing/individual/billing-history-view.component.html index 1032558f5f6..2491fc42c7f 100644 --- a/apps/web/src/app/billing/individual/billing-history-view.component.html +++ b/apps/web/src/app/billing/individual/billing-history-view.component.html @@ -1,13 +1,12 @@ -
-

+
+

{{ "billingHistory" | i18n }} -

+

- {{ "loading" | i18n }} + {{ "loading" | i18n }} diff --git a/apps/web/src/app/billing/shared/billing-history.component.html b/apps/web/src/app/billing/shared/billing-history.component.html index 56a8a990d4d..1719a59076f 100644 --- a/apps/web/src/app/billing/shared/billing-history.component.html +++ b/apps/web/src/app/billing/shared/billing-history.component.html @@ -1,65 +1,72 @@ -

{{ "invoices" | i18n }}

-

{{ "noInvoices" | i18n }}

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

{{ "transactions" | i18n }}

+

+ {{ "noTransactions" | i18n }} +

+ + +
+ + + + - - - - -
{{ i.date | date: "mediumDate" }} - +

{{ "invoices" | i18n }}

+

{{ "noInvoices" | i18n }}

+ + +
{{ i.date | date: "mediumDate" }} + + + + {{ "invoiceNumber" | i18n: i.number }} + {{ i.amount | currency: "$" }} + + + {{ "paid" | i18n }} + + + + {{ "unpaid" | i18n }} + +
{{ t.createdDate | date: "mediumDate" }} + + {{ "chargeNoun" | i18n }} + + {{ "refundNoun" | i18n }} + + + {{ t.details }} + - - - {{ "invoiceNumber" | i18n: i.number }} - {{ i.amount | currency: "$" }} - - - {{ "paid" | i18n }} - - - - {{ "unpaid" | i18n }} - -
-

{{ "transactions" | i18n }}

-

{{ "noTransactions" | i18n }}

- - - - - - - - - -
{{ t.createdDate | date: "mediumDate" }} - - {{ "chargeNoun" | i18n }} - - {{ "refundNoun" | i18n }} - - - {{ t.details }} - - {{ t.amount | currency: "$" }} -
-* {{ "chargesStatement" | i18n: "BITWARDEN" }} + {{ t.amount | currency: "$" }} + + + + + * {{ "chargesStatement" | i18n: "BITWARDEN" }} + From 9ecf384176c21fb43117a5b324e37f9d218451f0 Mon Sep 17 00:00:00 2001 From: KiruthigaManivannan <162679756+KiruthigaManivannan@users.noreply.github.com> Date: Wed, 17 Apr 2024 01:10:10 +0530 Subject: [PATCH 06/10] [PM-5020] Adjust Storage component migration (#8301) * Migrated Add Storage component * PM-5020 Addressed review comments for Adjust Storage component * PM-5020 Changes done in dialog css * PM-5020 Latest review comments addressed * PM-5020 Add storage submit action changes done * PM-5020 Moved the paragraph to top of dialog content --- .../user-subscription.component.html | 13 +- .../individual/user-subscription.component.ts | 32 ++-- ...nization-subscription-cloud.component.html | 25 +-- ...ganization-subscription-cloud.component.ts | 35 ++-- .../shared/adjust-storage.component.html | 72 ++++---- .../shared/adjust-storage.component.ts | 163 ++++++++++-------- 6 files changed, 180 insertions(+), 160 deletions(-) diff --git a/apps/web/src/app/billing/individual/user-subscription.component.html b/apps/web/src/app/billing/individual/user-subscription.component.html index 874983df840..380116e81b4 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.html +++ b/apps/web/src/app/billing/individual/user-subscription.component.html @@ -170,8 +170,8 @@
-
-
-
diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index 7d8c3a0f185..fa21317c180 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -12,6 +12,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; +import { + AdjustStorageDialogResult, + openAdjustStorageDialog, +} from "../shared/adjust-storage.component"; import { OffboardingSurveyDialogResultType, openOffboardingSurvey, @@ -24,7 +28,6 @@ export class UserSubscriptionComponent implements OnInit { loading = false; firstLoaded = false; adjustStorageAdd = true; - showAdjustStorage = false; showUpdateLicense = false; sub: SubscriptionResponse; selfHosted = false; @@ -144,19 +147,20 @@ export class UserSubscriptionComponent implements OnInit { } } - adjustStorage(add: boolean) { - this.adjustStorageAdd = add; - this.showAdjustStorage = true; - } - - closeStorage(load: boolean) { - this.showAdjustStorage = false; - if (load) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); - } - } + adjustStorage = (add: boolean) => { + return async () => { + const dialogRef = openAdjustStorageDialog(this.dialogService, { + data: { + storageGbPrice: 4, + add: add, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustStorageDialogResult.Adjusted) { + await this.load(); + } + }; + }; get subscriptionMarkedForCancel() { return ( diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index b4fac658546..16641c0d526 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -175,23 +175,24 @@
-
- -
-
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 2173d4c0ca1..9326359bd8c 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 @@ -18,6 +18,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; +import { + AdjustStorageDialogResult, + openAdjustStorageDialog, +} from "../shared/adjust-storage.component"; import { OffboardingSurveyDialogResultType, openOffboardingSurvey, @@ -36,8 +40,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy userOrg: Organization; showChangePlan = false; showDownloadLicense = false; - adjustStorageAdd = true; - showAdjustStorage = false; hasBillingSyncToken: boolean; showAdjustSecretsManager = false; showSecretsManagerSubscribe = false; @@ -361,19 +363,22 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.load(); } - adjustStorage(add: boolean) { - this.adjustStorageAdd = add; - this.showAdjustStorage = true; - } - - closeStorage(load: boolean) { - this.showAdjustStorage = false; - if (load) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); - } - } + adjustStorage = (add: boolean) => { + return async () => { + const dialogRef = openAdjustStorageDialog(this.dialogService, { + data: { + storageGbPrice: this.storageGbPrice, + add: add, + organizationId: this.organizationId, + interval: this.billingInterval, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustStorageDialogResult.Adjusted) { + await this.load(); + } + }; + }; removeSponsorship = async () => { const confirmed = await this.dialogService.openSimpleDialog({ diff --git a/apps/web/src/app/billing/shared/adjust-storage.component.html b/apps/web/src/app/billing/shared/adjust-storage.component.html index aa6daca335f..a597a3ae5ea 100644 --- a/apps/web/src/app/billing/shared/adjust-storage.component.html +++ b/apps/web/src/app/billing/shared/adjust-storage.component.html @@ -1,43 +1,35 @@ -
-
- -

{{ (add ? "addStorage" : "removeStorage") | i18n }}

-
-
- - + + + +

{{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }}

+
+ + {{ (add ? "gbStorageAdd" : "gbStorageRemove") | i18n }} + + + {{ "total" | i18n }}: + {{ formGroup.get("storageAdjustment").value || 0 }} GB × + {{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{ + interval | i18n + }} + +
-
-
- {{ "total" | i18n }}: {{ storageAdjustment || 0 }} GB × - {{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{ - interval | i18n - }} -
- - - - {{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }} - -
+ + + + + + diff --git a/apps/web/src/app/billing/shared/adjust-storage.component.ts b/apps/web/src/app/billing/shared/adjust-storage.component.ts index 25462c28295..fcdbc3437df 100644 --- a/apps/web/src/app/billing/shared/adjust-storage.component.ts +++ b/apps/web/src/app/billing/shared/adjust-storage.component.ts @@ -1,4 +1,6 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core"; +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject, ViewChild } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -8,27 +10,45 @@ import { StorageRequest } from "@bitwarden/common/models/request/storage.request import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { DialogService } from "@bitwarden/components"; import { PaymentComponent } from "./payment.component"; +export interface AdjustStorageDialogData { + storageGbPrice: number; + add: boolean; + organizationId?: string; + interval?: string; +} + +export enum AdjustStorageDialogResult { + Adjusted = "adjusted", + Cancelled = "cancelled", +} + @Component({ - selector: "app-adjust-storage", templateUrl: "adjust-storage.component.html", }) export class AdjustStorageComponent { - @Input() storageGbPrice = 0; - @Input() add = true; - @Input() organizationId: string; - @Input() interval = "year"; - @Output() onAdjusted = new EventEmitter(); - @Output() onCanceled = new EventEmitter(); + storageGbPrice: number; + add: boolean; + organizationId: string; + interval: string; @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; - storageAdjustment = 0; - formPromise: Promise; + protected DialogResult = AdjustStorageDialogResult; + protected formGroup = new FormGroup({ + storageAdjustment: new FormControl(0, [ + Validators.required, + Validators.min(0), + Validators.max(99), + ]), + }); constructor( + private dialogRef: DialogRef, + @Inject(DIALOG_DATA) protected data: AdjustStorageDialogData, private apiService: ApiService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, @@ -36,69 +56,74 @@ export class AdjustStorageComponent { private activatedRoute: ActivatedRoute, private logService: LogService, private organizationApiService: OrganizationApiServiceAbstraction, - ) {} + ) { + this.storageGbPrice = data.storageGbPrice; + this.add = data.add; + this.organizationId = data.organizationId; + this.interval = data.interval || "year"; + } - async submit() { - try { - const request = new StorageRequest(); - request.storageGbAdjustment = this.storageAdjustment; - if (!this.add) { - request.storageGbAdjustment *= -1; - } - - let paymentFailed = false; - const action = async () => { - let response: Promise; - if (this.organizationId == null) { - response = this.formPromise = this.apiService.postAccountStorage(request); - } else { - response = this.formPromise = this.organizationApiService.updateStorage( - this.organizationId, - request, - ); - } - const result = await response; - if (result != null && result.paymentIntentClientSecret != null) { - try { - await this.paymentComponent.handleStripeCardPayment( - result.paymentIntentClientSecret, - null, - ); - } catch { - paymentFailed = true; - } - } - }; - this.formPromise = action(); - await this.formPromise; - this.onAdjusted.emit(this.storageAdjustment); - if (paymentFailed) { - this.platformUtilsService.showToast( - "warning", - null, - this.i18nService.t("couldNotChargeCardPayInvoice"), - { timeout: 10000 }, - ); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["../billing"], { relativeTo: this.activatedRoute }); - } else { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), - ); - } - } catch (e) { - this.logService.error(e); + submit = async () => { + const request = new StorageRequest(); + request.storageGbAdjustment = this.formGroup.value.storageAdjustment; + if (!this.add) { + request.storageGbAdjustment *= -1; } - } - cancel() { - this.onCanceled.emit(); - } + let paymentFailed = false; + const action = async () => { + let response: Promise; + if (this.organizationId == null) { + response = this.apiService.postAccountStorage(request); + } else { + response = this.organizationApiService.updateStorage(this.organizationId, request); + } + const result = await response; + if (result != null && result.paymentIntentClientSecret != null) { + try { + await this.paymentComponent.handleStripeCardPayment( + result.paymentIntentClientSecret, + null, + ); + } catch { + paymentFailed = true; + } + } + }; + await action(); + this.dialogRef.close(AdjustStorageDialogResult.Adjusted); + if (paymentFailed) { + this.platformUtilsService.showToast( + "warning", + null, + this.i18nService.t("couldNotChargeCardPayInvoice"), + { timeout: 10000 }, + ); + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["../billing"], { relativeTo: this.activatedRoute }); + } else { + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), + ); + } + }; get adjustedStorageTotal(): number { - return this.storageGbPrice * this.storageAdjustment; + return this.storageGbPrice * this.formGroup.value.storageAdjustment; } } + +/** + * Strongly typed helper to open an AdjustStorageDialog + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param config Configuration for the dialog + */ +export function openAdjustStorageDialog( + dialogService: DialogService, + config: DialogConfig, +) { + return dialogService.open(AdjustStorageComponent, config); +} From 65f1bd2e3a258a42dc2a956e0f936452d610d268 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Tue, 16 Apr 2024 16:35:53 -0500 Subject: [PATCH 07/10] [PM-7527] Get MV3 build artifacts in main branch with clear messaging that that the build is not to be released (#8771) * [PM-7527] Get MV3 build artifacts in main branch with clear messaging that that the build is not to be released * Update .github/workflows/build-browser.yml Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com> --------- Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com> --- .github/workflows/build-browser.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 585a888ae10..4fb72a47dee 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -189,12 +189,12 @@ jobs: path: browser-source/apps/browser/dist/dist-chrome.zip if-no-files-found: error - # - name: Upload Chrome MV3 artifact - # uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - # with: - # name: dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip - # path: browser-source/apps/browser/dist/dist-chrome-mv3.zip - # if-no-files-found: error + - name: Upload Chrome MV3 artifact (DO NOT USE FOR PROD) + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip + path: browser-source/apps/browser/dist/dist-chrome-mv3.zip + if-no-files-found: error - name: Upload Firefox artifact uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 From f45eec1a4f42329efc909f54778b559f7f1ae4fc Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:31:48 +1000 Subject: [PATCH 08/10] [AC-2169] Group modal - limit admin access - members tab (#8650) * Restrict user from adding themselves to existing group --- .../manage/group-add-edit.component.html | 7 +- .../manage/group-add-edit.component.ts | 118 +++++++++++++----- apps/web/src/locales/en/messages.json | 3 + libs/common/src/abstractions/api.service.ts | 1 - libs/common/src/services/api.service.ts | 10 -- 5 files changed, 98 insertions(+), 41 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html index c6bfb94557f..3afb816e14d 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html @@ -31,7 +31,12 @@ -

{{ "editGroupMembersDesc" | i18n }}

+

+ {{ "editGroupMembersDesc" | i18n }} + + {{ "restrictedGroupAccessDesc" | i18n }} + +

= []; group: GroupView; groupForm = this.formBuilder.group({ @@ -110,6 +125,10 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { return this.params.organizationId; } + protected get editMode(): boolean { + return this.groupId != null; + } + private destroy$ = new Subject(); private get orgCollections$() { @@ -134,7 +153,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { ); } - private get orgMembers$() { + private get orgMembers$(): Observable> { return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe( map((response) => response.data.map((m) => ({ @@ -145,34 +164,55 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { listName: m.name?.length > 0 ? `${m.name} (${m.email})` : m.email, labelName: m.name || m.email, status: m.status, + userId: m.userId as UserId, })), ), ); } - private get groupDetails$() { - if (!this.editMode) { - return of(undefined); - } - - return combineLatest([ - this.groupService.get(this.organizationId, this.groupId), - this.apiService.getGroupUsers(this.organizationId, this.groupId), - ]).pipe( - map(([groupView, users]) => { - groupView.members = users; - return groupView; - }), - catchError((e: unknown) => { - if (e instanceof ErrorResponse) { - this.logService.error(e.message); - } else { - this.logService.error(e.toString()); - } + private groupDetails$: Observable = of(this.editMode).pipe( + concatMap((editMode) => { + if (!editMode) { return of(undefined); - }), - ); - } + } + + return combineLatest([ + this.groupService.get(this.organizationId, this.groupId), + this.apiService.getGroupUsers(this.organizationId, this.groupId), + ]).pipe( + map(([groupView, users]) => { + groupView.members = users; + return groupView; + }), + catchError((e: unknown) => { + if (e instanceof ErrorResponse) { + this.logService.error(e.message); + } else { + this.logService.error(e.toString()); + } + return of(undefined); + }), + ); + }), + shareReplay({ refCount: false }), + ); + + restrictGroupAccess$ = combineLatest([ + this.organizationService.get$(this.organizationId), + this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), + this.groupDetails$, + ]).pipe( + map( + ([organization, flexibleCollectionsV1Enabled, group]) => + // Feature flag conditionals + flexibleCollectionsV1Enabled && + organization.flexibleCollections && + // Business logic conditionals + !organization.allowAdminAccessToAllCollectionItems && + group !== undefined, + ), + shareReplay({ refCount: true, bufferSize: 1 }), + ); constructor( @Inject(DIALOG_DATA) private params: GroupAddEditDialogParams, @@ -188,17 +228,25 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { private changeDetectorRef: ChangeDetectorRef, private dialogService: DialogService, private organizationService: OrganizationService, + private configService: ConfigService, + private accountService: AccountService, ) { this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info; } ngOnInit() { - this.editMode = this.loading = this.groupId != null; + this.loading = true; this.title = this.i18nService.t(this.editMode ? "editGroup" : "newGroup"); - combineLatest([this.orgCollections$, this.orgMembers$, this.groupDetails$]) + combineLatest([ + this.orgCollections$, + this.orgMembers$, + this.groupDetails$, + this.restrictGroupAccess$, + this.accountService.activeAccount$, + ]) .pipe(takeUntil(this.destroy$)) - .subscribe(([collections, members, group]) => { + .subscribe(([collections, members, group, restrictGroupAccess, activeAccount]) => { this.collections = collections; this.members = members; this.group = group; @@ -224,6 +272,18 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { }); } + // If the current user is not already in the group and cannot add themselves, remove them from the list + if (restrictGroupAccess) { + const organizationUserId = this.members.find((m) => m.userId === activeAccount.id).id; + const isAlreadyInGroup = this.groupForm.value.members.some( + (m) => m.id === organizationUserId, + ); + + if (!isAlreadyInGroup) { + this.members = this.members.filter((m) => m.id !== organizationUserId); + } + } + this.loading = false; }); } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index f28bff066a9..a3fc234caba 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7905,5 +7905,8 @@ }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." } } diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 6962a442680..44a58403ede 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -297,7 +297,6 @@ export abstract class ApiService { ) => Promise; getGroupUsers: (organizationId: string, id: string) => Promise; - putGroupUsers: (organizationId: string, id: string, request: string[]) => Promise; deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise; getSync: () => Promise; diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index c7a8f3f091a..90671c4f04b 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -866,16 +866,6 @@ export class ApiService implements ApiServiceAbstraction { return r; } - async putGroupUsers(organizationId: string, id: string, request: string[]): Promise { - await this.send( - "PUT", - "/organizations/" + organizationId + "/groups/" + id + "/users", - request, - true, - false, - ); - } - deleteGroupUser(organizationId: string, id: string, organizationUserId: string): Promise { return this.send( "DELETE", From 4db383850fd9f11ec531c18ee2118de8830a21e3 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:03:48 +1000 Subject: [PATCH 09/10] [AC-2172] Member modal - limit admin access (#8343) * limit admin permissions to assign members to collections that the admin doesn't have can manage permissions for --- .../member-dialog/member-dialog.component.ts | 97 ++++++++++++++----- .../vault/core/collection-admin.service.ts | 3 + .../models/response/collection.response.ts | 11 +-- 3 files changed, 81 insertions(+), 30 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index 752122de004..771b8cc505c 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -31,6 +31,7 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v import { DialogService } from "@bitwarden/components"; import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service"; +import { CollectionAdminView } from "../../../../../vault/core/views/collection-admin.view"; import { CollectionAccessSelectionView, GroupService, @@ -206,25 +207,52 @@ export class MemberDialogComponent implements OnDestroy { collections: this.collectionAdminService.getAll(this.params.organizationId), userDetails: userDetails$, groups: groups$, + flexibleCollectionsV1Enabled: this.configService.getFeatureFlag$( + FeatureFlag.FlexibleCollectionsV1, + false, + ), }) .pipe(takeUntil(this.destroy$)) - .subscribe(({ organization, collections, userDetails, groups }) => { - this.setFormValidators(organization); + .subscribe( + ({ organization, collections, userDetails, groups, flexibleCollectionsV1Enabled }) => { + this.setFormValidators(organization); - this.collectionAccessItems = [].concat( - collections.map((c) => mapCollectionToAccessItemView(c)), - ); + // Groups tab: populate available groups + this.groupAccessItems = [].concat( + groups.map((g) => mapGroupToAccessItemView(g)), + ); - this.groupAccessItems = [].concat( - groups.map((g) => mapGroupToAccessItemView(g)), - ); + // Collections tab: Populate all available collections (including current user access where applicable) + this.collectionAccessItems = collections + .map((c) => + mapCollectionToAccessItemView( + c, + organization, + flexibleCollectionsV1Enabled, + userDetails == null + ? undefined + : c.users.find((access) => access.id === userDetails.id), + ), + ) + // But remove collections that we can't assign access to, unless the user is already assigned + .filter( + (item) => + !item.readonly || userDetails?.collections.some((access) => access.id == item.id), + ); - if (this.params.organizationUserId) { - this.loadOrganizationUser(userDetails, groups, collections); - } + if (userDetails != null) { + this.loadOrganizationUser( + userDetails, + groups, + collections, + organization, + flexibleCollectionsV1Enabled, + ); + } - this.loading = false; - }); + this.loading = false; + }, + ); } private setFormValidators(organization: Organization) { @@ -246,7 +274,9 @@ export class MemberDialogComponent implements OnDestroy { private loadOrganizationUser( userDetails: OrganizationUserAdminView, groups: GroupView[], - collections: CollectionView[], + collections: CollectionAdminView[], + organization: Organization, + flexibleCollectionsV1Enabled: boolean, ) { if (!userDetails) { throw new Error("Could not find user to edit."); @@ -295,13 +325,22 @@ export class MemberDialogComponent implements OnDestroy { }), ); + // Populate additional collection access via groups (rendered as separate rows from user access) this.collectionAccessItems = this.collectionAccessItems.concat( collectionsFromGroups.map(({ collection, accessSelection, group }) => - mapCollectionToAccessItemView(collection, accessSelection, group), + mapCollectionToAccessItemView( + collection, + organization, + flexibleCollectionsV1Enabled, + accessSelection, + group, + ), ), ); - const accessSelections = mapToAccessSelections(userDetails); + // Set current collections and groups the user has access to (excluding collections the current user doesn't have + // permissions to change - they are included as readonly via the CollectionAccessItems) + const accessSelections = mapToAccessSelections(userDetails, this.collectionAccessItems); const groupAccessSelections = mapToGroupAccessSelections(userDetails.groups); this.formGroup.removeControl("emails"); @@ -573,6 +612,8 @@ export class MemberDialogComponent implements OnDestroy { function mapCollectionToAccessItemView( collection: CollectionView, + organization: Organization, + flexibleCollectionsV1Enabled: boolean, accessSelection?: CollectionAccessSelectionView, group?: GroupView, ): AccessItemView { @@ -581,7 +622,8 @@ function mapCollectionToAccessItemView( id: group ? `${collection.id}-${group.id}` : collection.id, labelName: collection.name, listName: collection.name, - readonly: group !== undefined, + readonly: + group !== undefined || !collection.canEdit(organization, flexibleCollectionsV1Enabled), readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined, viaGroupName: group?.name, }; @@ -596,16 +638,23 @@ function mapGroupToAccessItemView(group: GroupView): AccessItemView { }; } -function mapToAccessSelections(user: OrganizationUserAdminView): AccessItemValue[] { +function mapToAccessSelections( + user: OrganizationUserAdminView, + items: AccessItemView[], +): AccessItemValue[] { if (user == undefined) { return []; } - return [].concat( - user.collections.map((selection) => ({ - id: selection.id, - type: AccessItemType.Collection, - permission: convertToPermission(selection), - })), + + return ( + user.collections + // The FormControl value only represents editable collection access - exclude readonly access selections + .filter((selection) => !items.find((item) => item.id == selection.id).readonly) + .map((selection) => ({ + id: selection.id, + type: AccessItemType.Collection, + permission: convertToPermission(selection), + })) ); } diff --git a/apps/web/src/app/vault/core/collection-admin.service.ts b/apps/web/src/app/vault/core/collection-admin.service.ts index 74f825e1acb..7f78ab214a4 100644 --- a/apps/web/src/app/vault/core/collection-admin.service.ts +++ b/apps/web/src/app/vault/core/collection-admin.service.ts @@ -124,6 +124,9 @@ export class CollectionAdminService { view.groups = c.groups; view.users = c.users; view.assigned = c.assigned; + view.readOnly = c.readOnly; + view.hidePasswords = c.hidePasswords; + view.manage = c.manage; } return view; diff --git a/libs/common/src/vault/models/response/collection.response.ts b/libs/common/src/vault/models/response/collection.response.ts index f16fe547e07..ac4781df714 100644 --- a/libs/common/src/vault/models/response/collection.response.ts +++ b/libs/common/src/vault/models/response/collection.response.ts @@ -21,6 +21,10 @@ export class CollectionDetailsResponse extends CollectionResponse { readOnly: boolean; manage: boolean; hidePasswords: boolean; + + /** + * Flag indicating the user has been explicitly assigned to this Collection + */ assigned: boolean; constructor(response: any) { @@ -35,15 +39,10 @@ export class CollectionDetailsResponse extends CollectionResponse { } } -export class CollectionAccessDetailsResponse extends CollectionResponse { +export class CollectionAccessDetailsResponse extends CollectionDetailsResponse { groups: SelectionReadOnlyResponse[] = []; users: SelectionReadOnlyResponse[] = []; - /** - * Flag indicating the user has been explicitly assigned to this Collection - */ - assigned: boolean; - constructor(response: any) { super(response); this.assigned = this.getResponseProperty("Assigned") || false; From a72b7f3d214f6fc5d2b7b09e65a55c2fc3023521 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 17 Apr 2024 14:07:26 +0100 Subject: [PATCH 10/10] [AC-1218] Add ability to delete Provider Portals (#8685) * initial commit * add changes from running prettier * resolve the linx issue * resolve the lint issue * resolving lint error * correct the redirect issue * resolve pr commit * Add a feature flag * move the new component to adminconsole * resolve some pr comments * move the endpoint from ApiService to providerApiService * move provider endpoints to the provider-api class * change the header * resolve some pr comments --- apps/cli/src/bw.ts | 5 + ...ify-recover-delete-provider.component.html | 34 ++++++ ...erify-recover-delete-provider.component.ts | 61 ++++++++++ .../organization-plans.component.ts | 4 +- apps/web/src/app/oss-routing.module.ts | 7 ++ .../src/app/shared/loose-components.module.ts | 3 + apps/web/src/locales/en/messages.json | 36 ++++++ .../providers/providers.module.ts | 2 + .../providers/settings/account.component.html | 105 ++++++++++-------- .../providers/settings/account.component.ts | 73 +++++++++++- .../providers/setup/setup.component.ts | 8 +- .../src/services/jslib-services.module.ts | 7 ++ libs/common/src/abstractions/api.service.ts | 7 -- .../provider-api.service.abstraction.ts | 15 +++ .../provider-verify-recover-delete.request.ts | 7 ++ .../services/provider/provider-api.service.ts | 47 ++++++++ libs/common/src/enums/feature-flag.enum.ts | 1 + libs/common/src/services/api.service.ts | 20 ---- 18 files changed, 359 insertions(+), 83 deletions(-) create mode 100644 apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.html create mode 100644 apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts create mode 100644 libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts create mode 100644 libs/common/src/admin-console/models/request/provider/provider-verify-recover-delete.request.ts create mode 100644 libs/common/src/admin-console/services/provider/provider-api.service.ts diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index f02d7da49c7..7fbefc10e31 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -18,11 +18,13 @@ import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; +import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; @@ -232,6 +234,7 @@ export class Main { stateEventRunnerService: StateEventRunnerService; biometricStateService: BiometricStateService; billingAccountProfileStateService: BillingAccountProfileStateService; + providerApiService: ProviderApiServiceAbstraction; constructor() { let p = null; @@ -692,6 +695,8 @@ export class Main { this.eventUploadService, this.authService, ); + + this.providerApiService = new ProviderApiService(this.apiService); } async run() { diff --git a/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.html b/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.html new file mode 100644 index 00000000000..a287a537a4d --- /dev/null +++ b/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.html @@ -0,0 +1,34 @@ +
+
+
+

{{ "deleteProvider" | i18n }}

+
+
+ {{ "deleteProviderWarning" | i18n }} +

+ {{ name }} +

+

{{ "deleteProviderRecoverConfirmDesc" | i18n }}

+
+
+ + + {{ "cancel" | i18n }} + +
+
+
+
+
+
diff --git a/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts b/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts new file mode 100644 index 00000000000..0550820cda4 --- /dev/null +++ b/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts @@ -0,0 +1,61 @@ +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; +import { ProviderVerifyRecoverDeleteRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-verify-recover-delete.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +@Component({ + selector: "app-verify-recover-delete-provider", + templateUrl: "verify-recover-delete-provider.component.html", +}) +// eslint-disable-next-line rxjs-angular/prefer-takeuntil +export class VerifyRecoverDeleteProviderComponent implements OnInit { + name: string; + formPromise: Promise; + + private providerId: string; + private token: string; + + constructor( + private router: Router, + private providerApiService: ProviderApiServiceAbstraction, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + private route: ActivatedRoute, + private logService: LogService, + ) {} + + async ngOnInit() { + const qParams = await firstValueFrom(this.route.queryParams); + if (qParams.providerId != null && qParams.token != null && qParams.name != null) { + this.providerId = qParams.providerId; + this.token = qParams.token; + this.name = qParams.name; + } else { + await this.router.navigate(["/"]); + } + } + + async submit() { + try { + const request = new ProviderVerifyRecoverDeleteRequest(this.token); + this.formPromise = this.providerApiService.providerRecoverDeleteToken( + this.providerId, + request, + ); + await this.formPromise; + this.platformUtilsService.showToast( + "success", + this.i18nService.t("providerDeleted"), + this.i18nService.t("providerDeletedDesc"), + ); + await this.router.navigate(["/"]); + } catch (e) { + this.logService.error(e); + } + } +} diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index f2fb2965221..30691ce87d5 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -15,6 +15,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/organization-create.request"; @@ -147,6 +148,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private messagingService: MessagingService, private formBuilder: FormBuilder, private organizationApiService: OrganizationApiServiceAbstraction, + private providerApiService: ProviderApiServiceAbstraction, ) { this.selfHosted = platformUtilsService.isSelfHost(); } @@ -182,7 +184,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { if (this.hasProvider) { this.formGroup.controls.businessOwned.setValue(true); this.changedOwnedBusiness(); - this.provider = await this.apiService.getProvider(this.providerId); + this.provider = await this.providerApiService.getProvider(this.providerId); const providerDefaultPlan = this.passwordManagerPlans.find( (plan) => plan.type === PlanType.TeamsAnnually, ); diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index e5c2f353c0f..066ed5db103 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -13,6 +13,7 @@ import { flagEnabled, Flags } from "../utils/flags"; import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/sponsorships/accept-family-sponsorship.component"; import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component"; +import { VerifyRecoverDeleteProviderComponent } from "./admin-console/providers/verify-recover-delete-provider.component"; import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component"; import { SponsoredFamiliesComponent } from "./admin-console/settings/sponsored-families.component"; import { AcceptOrganizationComponent } from "./auth/accept-organization.component"; @@ -156,6 +157,12 @@ const routes: Routes = [ canActivate: [UnauthGuard], data: { titleId: "deleteAccount" }, }, + { + path: "verify-recover-delete-provider", + component: VerifyRecoverDeleteProviderComponent, + canActivate: [UnauthGuard], + data: { titleId: "deleteAccount" }, + }, { path: "send/:sendId/:key", component: AccessComponent, diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 586f2079627..8f6a1eaedce 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -13,6 +13,7 @@ import { ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent } f import { UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent } from "../admin-console/organizations/tools/unsecured-websites-report.component"; import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from "../admin-console/organizations/tools/weak-passwords-report.component"; import { ProvidersComponent } from "../admin-console/providers/providers.component"; +import { VerifyRecoverDeleteProviderComponent } from "../admin-console/providers/verify-recover-delete-provider.component"; import { SponsoredFamiliesComponent } from "../admin-console/settings/sponsored-families.component"; import { SponsoringOrgRowComponent } from "../admin-console/settings/sponsoring-org-row.component"; import { AcceptOrganizationComponent } from "../auth/accept-organization.component"; @@ -184,6 +185,7 @@ import { SharedModule } from "./shared.module"; VerifyEmailComponent, VerifyEmailTokenComponent, VerifyRecoverDeleteComponent, + VerifyRecoverDeleteProviderComponent, LowKdfComponent, ], exports: [ @@ -261,6 +263,7 @@ import { SharedModule } from "./shared.module"; VerifyEmailComponent, VerifyEmailTokenComponent, VerifyRecoverDeleteComponent, + VerifyRecoverDeleteProviderComponent, LowKdfComponent, HeaderModule, DangerZoneComponent, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index a3fc234caba..c8dfa14c8ba 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7908,5 +7908,41 @@ }, "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 81cc7c29192..0d759737129 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -8,6 +8,7 @@ import { OrganizationPlansComponent, TaxInfoComponent } from "@bitwarden/web-vau import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared"; import { OssModule } from "@bitwarden/web-vault/app/oss.module"; +import { DangerZoneComponent } from "../../../../../../apps/web/src/app/auth/settings/account/danger-zone.component"; import { ManageClientOrganizationSubscriptionComponent } from "../../billing/providers/clients/manage-client-organization-subscription.component"; import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component"; @@ -40,6 +41,7 @@ import { SetupComponent } from "./setup/setup.component"; ProvidersLayoutComponent, PaymentMethodWarningsModule, TaxInfoComponent, + DangerZoneComponent, ], declarations: [ AcceptProviderComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html index ea634e5ebc3..10f6d144252 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html @@ -1,51 +1,58 @@ - -
- - {{ "loading" | i18n }} -
-
-
-
-
- - -
-
- - -
-
-
- -
+ +
+ + {{ "loading" | i18n }}
- - +
+
+
+
+ + +
+
+ + +
+
+
+ +
+
+ +
+ + + + +
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts index 079e68fe217..83038d1bfc4 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts @@ -1,13 +1,18 @@ import { Component } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; +import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-update.request"; import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { DialogService } from "@bitwarden/components"; @Component({ selector: "provider-account", @@ -23,6 +28,11 @@ export class AccountComponent { private providerId: string; + protected enableDeleteProvider$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableDeleteProvider, + false, + ); + constructor( private apiService: ApiService, private i18nService: I18nService, @@ -30,6 +40,9 @@ export class AccountComponent { private syncService: SyncService, private platformUtilsService: PlatformUtilsService, private logService: LogService, + private dialogService: DialogService, + private configService: ConfigService, + private providerApiService: ProviderApiServiceAbstraction, ) {} async ngOnInit() { @@ -38,7 +51,7 @@ export class AccountComponent { this.route.parent.parent.params.subscribe(async (params) => { this.providerId = params.providerId; try { - this.provider = await this.apiService.getProvider(this.providerId); + this.provider = await this.providerApiService.getProvider(this.providerId); } catch (e) { this.logService.error(`Handled exception: ${e}`); } @@ -53,7 +66,7 @@ export class AccountComponent { request.businessName = this.provider.businessName; request.billingEmail = this.provider.billingEmail; - this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => { + this.formPromise = this.providerApiService.putProvider(this.providerId, request).then(() => { return this.syncService.fullSync(true); }); await this.formPromise; @@ -62,4 +75,60 @@ export class AccountComponent { this.logService.error(`Handled exception: ${e}`); } } + + async deleteProvider() { + const providerClients = await this.apiService.getProviderClients(this.providerId); + if (providerClients.data != null && providerClients.data.length > 0) { + await this.dialogService.openSimpleDialog({ + title: { key: "deleteProviderName", placeholders: [this.provider.name] }, + content: { key: "deleteProviderWarningDesc", placeholders: [this.provider.name] }, + acceptButtonText: { key: "ok" }, + type: "danger", + }); + + return false; + } + + const userVerified = await this.verifyUser(); + if (!userVerified) { + return; + } + + this.formPromise = this.providerApiService.deleteProvider(this.providerId); + try { + await this.formPromise; + this.platformUtilsService.showToast( + "success", + this.i18nService.t("providerDeleted"), + this.i18nService.t("providerDeletedDesc"), + ); + } catch (e) { + this.logService.error(e); + } + this.formPromise = null; + } + + private async verifyUser(): Promise { + const confirmDescription = "deleteProviderConfirmation"; + const result = await UserVerificationDialogComponent.open(this.dialogService, { + title: "deleteProvider", + bodyText: confirmDescription, + confirmButtonOptions: { + text: "deleteProvider", + type: "danger", + }, + }); + + // Handle the result of the dialog based on user action and verification success + if (result.userAction === "cancel") { + // User cancelled the dialog + return false; + } + + // User confirmed the dialog so check verification success + if (!result.verificationSuccess) { + return false; + } + return true; + } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts index ed7b42c9593..cf9af4f68ad 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { first } from "rxjs/operators"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -50,10 +50,10 @@ export class SetupComponent implements OnInit { private i18nService: I18nService, private route: ActivatedRoute, private cryptoService: CryptoService, - private apiService: ApiService, private syncService: SyncService, private validationService: ValidationService, private configService: ConfigService, + private providerApiService: ProviderApiServiceAbstraction, ) {} ngOnInit() { @@ -80,7 +80,7 @@ export class SetupComponent implements OnInit { // Check if provider exists, redirect if it does try { - const provider = await this.apiService.getProvider(this.providerId); + const provider = await this.providerApiService.getProvider(this.providerId); if (provider.name != null) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -128,7 +128,7 @@ export class SetupComponent implements OnInit { } } - const provider = await this.apiService.postProviderSetup(this.providerId, request); + const provider = await this.providerApiService.postProviderSetup(this.providerId, request); this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup")); await this.syncService.fullSync(true); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index dbb94f6753e..ad0881a4b3e 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -38,6 +38,7 @@ import { InternalPolicyService, PolicyService as PolicyServiceAbstraction, } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; @@ -47,6 +48,7 @@ import { DefaultOrganizationManagementPreferencesService } from "@bitwarden/comm import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; +import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service"; import { @@ -1115,6 +1117,11 @@ const safeProviders: SafeProvider[] = [ useClass: LoggingErrorHandler, deps: [], }), + safeProvider({ + provide: ProviderApiServiceAbstraction, + useClass: ProviderApiService, + deps: [ApiServiceAbstraction], + }), ]; function encryptServiceFactory( diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 44a58403ede..9b3160ee19d 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -4,8 +4,6 @@ import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/re import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request"; import { ProviderAddOrganizationRequest } from "../admin-console/models/request/provider/provider-add-organization.request"; import { ProviderOrganizationCreateRequest } from "../admin-console/models/request/provider/provider-organization-create.request"; -import { ProviderSetupRequest } from "../admin-console/models/request/provider/provider-setup.request"; -import { ProviderUpdateRequest } from "../admin-console/models/request/provider/provider-update.request"; import { ProviderUserAcceptRequest } from "../admin-console/models/request/provider/provider-user-accept.request"; import { ProviderUserBulkConfirmRequest } from "../admin-console/models/request/provider/provider-user-bulk-confirm.request"; import { ProviderUserBulkRequest } from "../admin-console/models/request/provider/provider-user-bulk.request"; @@ -29,7 +27,6 @@ import { ProviderUserResponse, ProviderUserUserDetailsResponse, } from "../admin-console/models/response/provider/provider-user.response"; -import { ProviderResponse } from "../admin-console/models/response/provider/provider.response"; import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response"; import { CreateAuthRequest } from "../auth/models/request/create-auth.request"; import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request"; @@ -372,10 +369,6 @@ export abstract class ApiService { getPlans: () => Promise>; getTaxRates: () => Promise>; - postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise; - getProvider: (id: string) => Promise; - putProvider: (id: string, request: ProviderUpdateRequest) => Promise; - getProviderUsers: (providerId: string) => Promise>; getProviderUser: (providerId: string, id: string) => Promise; postProviderUserInvite: (providerId: string, request: ProviderUserInviteRequest) => Promise; diff --git a/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts new file mode 100644 index 00000000000..3c2170bf9e6 --- /dev/null +++ b/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts @@ -0,0 +1,15 @@ +import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request"; +import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request"; +import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request"; +import { ProviderResponse } from "../../models/response/provider/provider.response"; + +export class ProviderApiServiceAbstraction { + postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise; + getProvider: (id: string) => Promise; + putProvider: (id: string, request: ProviderUpdateRequest) => Promise; + providerRecoverDeleteToken: ( + organizationId: string, + request: ProviderVerifyRecoverDeleteRequest, + ) => Promise; + deleteProvider: (id: string) => Promise; +} diff --git a/libs/common/src/admin-console/models/request/provider/provider-verify-recover-delete.request.ts b/libs/common/src/admin-console/models/request/provider/provider-verify-recover-delete.request.ts new file mode 100644 index 00000000000..528d2dba785 --- /dev/null +++ b/libs/common/src/admin-console/models/request/provider/provider-verify-recover-delete.request.ts @@ -0,0 +1,7 @@ +export class ProviderVerifyRecoverDeleteRequest { + token: string; + + constructor(token: string) { + this.token = token; + } +} diff --git a/libs/common/src/admin-console/services/provider/provider-api.service.ts b/libs/common/src/admin-console/services/provider/provider-api.service.ts new file mode 100644 index 00000000000..2ee921393ff --- /dev/null +++ b/libs/common/src/admin-console/services/provider/provider-api.service.ts @@ -0,0 +1,47 @@ +import { ApiService } from "../../../abstractions/api.service"; +import { ProviderApiServiceAbstraction } from "../../abstractions/provider/provider-api.service.abstraction"; +import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request"; +import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request"; +import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request"; +import { ProviderResponse } from "../../models/response/provider/provider.response"; + +export class ProviderApiService implements ProviderApiServiceAbstraction { + constructor(private apiService: ApiService) {} + async postProviderSetup(id: string, request: ProviderSetupRequest) { + const r = await this.apiService.send( + "POST", + "/providers/" + id + "/setup", + request, + true, + true, + ); + return new ProviderResponse(r); + } + + async getProvider(id: string) { + const r = await this.apiService.send("GET", "/providers/" + id, null, true, true); + return new ProviderResponse(r); + } + + async putProvider(id: string, request: ProviderUpdateRequest) { + const r = await this.apiService.send("PUT", "/providers/" + id, request, true, true); + return new ProviderResponse(r); + } + + providerRecoverDeleteToken( + providerId: string, + request: ProviderVerifyRecoverDeleteRequest, + ): Promise { + return this.apiService.send( + "POST", + "/providers/" + providerId + "/delete-recover-token", + request, + false, + false, + ); + } + + async deleteProvider(id: string): Promise { + await this.apiService.send("DELETE", "/providers/" + id, null, true, false); + } +} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index b937e6c462d..636e9bc4cef 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -10,6 +10,7 @@ export enum FeatureFlag { EnableConsolidatedBilling = "enable-consolidated-billing", AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", UnassignedItemsBanner = "unassigned-items-banner", + EnableDeleteProvider = "AC-1218-delete-provider", } // Replace this with a type safe lookup of the feature flag values in PM-2282 diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 90671c4f04b..e8135f3d6c2 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -7,8 +7,6 @@ import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/re import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request"; import { ProviderAddOrganizationRequest } from "../admin-console/models/request/provider/provider-add-organization.request"; import { ProviderOrganizationCreateRequest } from "../admin-console/models/request/provider/provider-organization-create.request"; -import { ProviderSetupRequest } from "../admin-console/models/request/provider/provider-setup.request"; -import { ProviderUpdateRequest } from "../admin-console/models/request/provider/provider-update.request"; import { ProviderUserAcceptRequest } from "../admin-console/models/request/provider/provider-user-accept.request"; import { ProviderUserBulkConfirmRequest } from "../admin-console/models/request/provider/provider-user-bulk-confirm.request"; import { ProviderUserBulkRequest } from "../admin-console/models/request/provider/provider-user-bulk.request"; @@ -32,7 +30,6 @@ import { ProviderUserResponse, ProviderUserUserDetailsResponse, } from "../admin-console/models/response/provider/provider-user.response"; -import { ProviderResponse } from "../admin-console/models/response/provider/provider.response"; import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response"; import { TokenService } from "../auth/abstractions/token.service"; import { CreateAuthRequest } from "../auth/models/request/create-auth.request"; @@ -1151,23 +1148,6 @@ export class ApiService implements ApiServiceAbstraction { return this.send("DELETE", "/organizations/connections/" + id, null, true, false); } - // Provider APIs - - async postProviderSetup(id: string, request: ProviderSetupRequest) { - const r = await this.send("POST", "/providers/" + id + "/setup", request, true, true); - return new ProviderResponse(r); - } - - async getProvider(id: string) { - const r = await this.send("GET", "/providers/" + id, null, true, true); - return new ProviderResponse(r); - } - - async putProvider(id: string, request: ProviderUpdateRequest) { - const r = await this.send("PUT", "/providers/" + id, request, true, true); - return new ProviderResponse(r); - } - // Provider User APIs async getProviderUsers(