mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +00:00
[PM-24550] Remove Feature Flag Code for PM-12276 (#16173)
* tests: remove feature flag use in tests * tests: remove breadcrumbingPolicyTests and add service tests * refactor: remove event log use of flag from org-layout component * refactor: remove new policy code from org-layout component * refactor: remove event log use of flag from events component * refactor: remove event log use from collection dialog component * refactor: remove event log use from vault-header component * refactor: remove event-log route logic for org-reporting * refactor: remove logic from org-settings routing * refactor: remove breadcrumbing function and from billing service * refactor: remove ConfigService from DI for billing service * refactor: remove new policy code from policy-edit component * refactor: remove new policy code from policies component * refactor: remove feature flag * fix(Admin Console): revert to use of reactive observables pattern * fix(Admin Console): remove type artifact from reversion
This commit is contained in:
@@ -55,10 +55,7 @@
|
|||||||
<bit-nav-item
|
<bit-nav-item
|
||||||
[text]="'eventLogs' | i18n"
|
[text]="'eventLogs' | i18n"
|
||||||
route="reporting/events"
|
route="reporting/events"
|
||||||
*ngIf="
|
*ngIf="organization.canAccessEventLogs || organization.isOwner"
|
||||||
(organization.canAccessEventLogs && organization.useEvents) ||
|
|
||||||
(organization.isOwner && (isBreadcrumbEventLogsEnabled$ | async))
|
|
||||||
"
|
|
||||||
></bit-nav-item>
|
></bit-nav-item>
|
||||||
<bit-nav-item
|
<bit-nav-item
|
||||||
[text]="'reports' | i18n"
|
[text]="'reports' | i18n"
|
||||||
@@ -102,7 +99,7 @@
|
|||||||
<bit-nav-item
|
<bit-nav-item
|
||||||
[text]="'policies' | i18n"
|
[text]="'policies' | i18n"
|
||||||
route="settings/policies"
|
route="settings/policies"
|
||||||
*ngIf="canShowPoliciesTab$ | async"
|
*ngIf="organization.canManagePolicies"
|
||||||
></bit-nav-item>
|
></bit-nav-item>
|
||||||
<bit-nav-item
|
<bit-nav-item
|
||||||
[text]="'twoStepLogin' | i18n"
|
[text]="'twoStepLogin' | i18n"
|
||||||
|
|||||||
@@ -68,9 +68,7 @@ export class OrganizationLayoutComponent implements OnInit {
|
|||||||
hideNewOrgButton$: Observable<boolean>;
|
hideNewOrgButton$: Observable<boolean>;
|
||||||
organizationIsUnmanaged$: Observable<boolean>;
|
organizationIsUnmanaged$: Observable<boolean>;
|
||||||
|
|
||||||
protected isBreadcrumbEventLogsEnabled$: Observable<boolean>;
|
|
||||||
protected showSponsoredFamiliesDropdown$: Observable<boolean>;
|
protected showSponsoredFamiliesDropdown$: Observable<boolean>;
|
||||||
protected canShowPoliciesTab$: Observable<boolean>;
|
|
||||||
|
|
||||||
protected paymentDetailsPageData$: Observable<{
|
protected paymentDetailsPageData$: Observable<{
|
||||||
route: string;
|
route: string;
|
||||||
@@ -94,9 +92,6 @@ export class OrganizationLayoutComponent implements OnInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.isBreadcrumbEventLogsEnabled$ = this.configService.getFeatureFlag$(
|
|
||||||
FeatureFlag.PM12276_BreadcrumbEventLogs,
|
|
||||||
);
|
|
||||||
document.body.classList.remove("layout_frontend");
|
document.body.classList.remove("layout_frontend");
|
||||||
|
|
||||||
this.organization$ = this.route.params.pipe(
|
this.organization$ = this.route.params.pipe(
|
||||||
@@ -141,18 +136,6 @@ export class OrganizationLayoutComponent implements OnInit {
|
|||||||
|
|
||||||
this.integrationPageEnabled$ = this.organization$.pipe(map((org) => org.canAccessIntegrations));
|
this.integrationPageEnabled$ = this.organization$.pipe(map((org) => org.canAccessIntegrations));
|
||||||
|
|
||||||
this.canShowPoliciesTab$ = this.organization$.pipe(
|
|
||||||
switchMap((organization) =>
|
|
||||||
this.organizationBillingService
|
|
||||||
.isBreadcrumbingPoliciesEnabled$(organization)
|
|
||||||
.pipe(
|
|
||||||
map(
|
|
||||||
(isBreadcrumbingEnabled) => isBreadcrumbingEnabled || organization.canManagePolicies,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.paymentDetailsPageData$ = this.configService
|
this.paymentDetailsPageData$ = this.configService
|
||||||
.getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout)
|
.getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout)
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@let usePlaceHolderEvents = !organization?.useEvents && (isBreadcrumbEventLogsEnabled$ | async);
|
@let usePlaceHolderEvents = !organization?.useEvents;
|
||||||
<app-header>
|
<app-header>
|
||||||
<span
|
<span
|
||||||
bitBadge
|
bitBadge
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
|||||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||||
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
||||||
import { EventSystemUser } from "@bitwarden/common/enums";
|
import { EventSystemUser } from "@bitwarden/common/enums";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
|
||||||
import { EventResponse } from "@bitwarden/common/models/response/event.response";
|
import { EventResponse } from "@bitwarden/common/models/response/event.response";
|
||||||
import { EventView } from "@bitwarden/common/models/view/event.view";
|
import { EventView } from "@bitwarden/common/models/view/event.view";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
@@ -62,10 +61,6 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
|
|||||||
private orgUsersUserIdMap = new Map<string, any>();
|
private orgUsersUserIdMap = new Map<string, any>();
|
||||||
readonly ProductTierType = ProductTierType;
|
readonly ProductTierType = ProductTierType;
|
||||||
|
|
||||||
protected isBreadcrumbEventLogsEnabled$ = this.configService.getFeatureFlag$(
|
|
||||||
FeatureFlag.PM12276_BreadcrumbEventLogs,
|
|
||||||
);
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
|||||||
@@ -1,20 +1,7 @@
|
|||||||
<app-header>
|
<app-header></app-header>
|
||||||
@let organization = organization$ | async;
|
|
||||||
@if (isBreadcrumbingEnabled$ | async) {
|
|
||||||
<button
|
|
||||||
bitBadge
|
|
||||||
class="!tw-align-middle"
|
|
||||||
(click)="changePlan(organization)"
|
|
||||||
slot="title-suffix"
|
|
||||||
type="button"
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
{{ "upgrade" | i18n }}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</app-header>
|
|
||||||
|
|
||||||
<bit-container>
|
<bit-container>
|
||||||
|
@let organization = organization$ | async;
|
||||||
@if (loading) {
|
@if (loading) {
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { firstValueFrom, lastValueFrom, map, Observable, switchMap } from "rxjs";
|
import { firstValueFrom, lastValueFrom, Observable } from "rxjs";
|
||||||
import { first } from "rxjs/operators";
|
import { first, map } from "rxjs/operators";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getOrganizationById,
|
getOrganizationById,
|
||||||
@@ -14,18 +14,11 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
|||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
import {
|
|
||||||
ChangePlanDialogResultType,
|
|
||||||
openChangePlanDialog,
|
|
||||||
} from "@bitwarden/web-vault/app/billing/organizations/change-plan-dialog.component";
|
|
||||||
import { All } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
|
||||||
|
|
||||||
import { PolicyListService } from "../../core/policy-list.service";
|
import { PolicyListService } from "../../core/policy-list.service";
|
||||||
import { BasePolicy } from "../policies";
|
import { BasePolicy } from "../policies";
|
||||||
import { CollectionDialogTabType } from "../shared/components/collection-dialog";
|
|
||||||
|
|
||||||
import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.component";
|
import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.component";
|
||||||
|
|
||||||
@@ -38,19 +31,17 @@ export class PoliciesComponent implements OnInit {
|
|||||||
loading = true;
|
loading = true;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
policies: BasePolicy[];
|
policies: BasePolicy[];
|
||||||
protected organization$: Observable<Organization>;
|
organization$: Observable<Organization>;
|
||||||
|
|
||||||
private orgPolicies: PolicyResponse[];
|
private orgPolicies: PolicyResponse[];
|
||||||
protected policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
|
protected policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
|
||||||
protected isBreadcrumbingEnabled$: Observable<boolean>;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private accountService: AccountService,
|
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
|
private accountService: AccountService,
|
||||||
private policyApiService: PolicyApiServiceAbstraction,
|
private policyApiService: PolicyApiServiceAbstraction,
|
||||||
private policyListService: PolicyListService,
|
private policyListService: PolicyListService,
|
||||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
protected configService: ConfigService,
|
protected configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
@@ -62,9 +53,11 @@ export class PoliciesComponent implements OnInit {
|
|||||||
const userId = await firstValueFrom(
|
const userId = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.organization$ = this.organizationService
|
this.organization$ = this.organizationService
|
||||||
.organizations$(userId)
|
.organizations$(userId)
|
||||||
.pipe(getOrganizationById(this.organizationId));
|
.pipe(getOrganizationById(this.organizationId));
|
||||||
|
|
||||||
this.policies = this.policyListService.getPolicies();
|
this.policies = this.policyListService.getPolicies();
|
||||||
|
|
||||||
await this.load();
|
await this.load();
|
||||||
@@ -100,11 +93,7 @@ export class PoliciesComponent implements OnInit {
|
|||||||
this.orgPolicies.forEach((op) => {
|
this.orgPolicies.forEach((op) => {
|
||||||
this.policiesEnabledMap.set(op.type, op.enabled);
|
this.policiesEnabledMap.set(op.type, op.enabled);
|
||||||
});
|
});
|
||||||
this.isBreadcrumbingEnabled$ = this.organization$.pipe(
|
|
||||||
switchMap((organization) =>
|
|
||||||
this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,34 +106,8 @@ export class PoliciesComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await lastValueFrom(dialogRef.closed);
|
const result = await lastValueFrom(dialogRef.closed);
|
||||||
switch (result) {
|
if (result === PolicyEditDialogResult.Saved) {
|
||||||
case PolicyEditDialogResult.Saved:
|
await this.load();
|
||||||
await this.load();
|
|
||||||
break;
|
|
||||||
case PolicyEditDialogResult.UpgradePlan:
|
|
||||||
await this.changePlan(await firstValueFrom(this.organization$));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly CollectionDialogTabType = CollectionDialogTabType;
|
|
||||||
protected readonly All = All;
|
|
||||||
|
|
||||||
protected async changePlan(organization: Organization) {
|
|
||||||
const reference = openChangePlanDialog(this.dialogService, {
|
|
||||||
data: {
|
|
||||||
organizationId: organization.id,
|
|
||||||
subscription: null,
|
|
||||||
productTierType: organization.productTierType,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await lastValueFrom(reference.closed);
|
|
||||||
|
|
||||||
if (result === ChangePlanDialogResultType.Closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.load();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
<bit-dialog [loading]="loading" [title]="'editPolicy' | i18n" [subtitle]="policy.name | i18n">
|
<bit-dialog [loading]="loading" [title]="'editPolicy' | i18n" [subtitle]="policy.name | i18n">
|
||||||
<ng-container bitDialogTitle>
|
|
||||||
<button
|
|
||||||
bitBadge
|
|
||||||
class="!tw-align-middle"
|
|
||||||
(click)="upgradePlan()"
|
|
||||||
*ngIf="isBreadcrumbingEnabled$ | async"
|
|
||||||
type="button"
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
{{ "planNameEnterprise" | i18n }}
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container bitDialogContent>
|
<ng-container bitDialogContent>
|
||||||
<div *ngIf="loading">
|
<div *ngIf="loading">
|
||||||
<i
|
<i
|
||||||
@@ -30,7 +18,6 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container bitDialogFooter>
|
<ng-container bitDialogFooter>
|
||||||
<button
|
<button
|
||||||
*ngIf="!(isBreadcrumbingEnabled$ | async); else breadcrumbing"
|
|
||||||
bitButton
|
bitButton
|
||||||
buttonType="primary"
|
buttonType="primary"
|
||||||
[disabled]="saveDisabled$ | async"
|
[disabled]="saveDisabled$ | async"
|
||||||
@@ -39,11 +26,6 @@
|
|||||||
>
|
>
|
||||||
{{ "save" | i18n }}
|
{{ "save" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<ng-template #breadcrumbing>
|
|
||||||
<button bitButton buttonType="primary" bitFormButton type="button" (click)="upgradePlan()">
|
|
||||||
{{ "upgrade" | i18n }}
|
|
||||||
</button>
|
|
||||||
</ng-template>
|
|
||||||
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||||
{{ "cancel" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -9,20 +9,12 @@ import {
|
|||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
import { FormBuilder } from "@angular/forms";
|
||||||
import { map, Observable, switchMap } from "rxjs";
|
import { Observable, map } from "rxjs";
|
||||||
|
|
||||||
import {
|
|
||||||
getOrganizationById,
|
|
||||||
OrganizationService,
|
|
||||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
|
||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
|
||||||
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
|
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
|
||||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
|
||||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import {
|
import {
|
||||||
DIALOG_DATA,
|
DIALOG_DATA,
|
||||||
@@ -45,7 +37,6 @@ export type PolicyEditDialogData = {
|
|||||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||||
export enum PolicyEditDialogResult {
|
export enum PolicyEditDialogResult {
|
||||||
Saved = "saved",
|
Saved = "saved",
|
||||||
UpgradePlan = "upgrade-plan",
|
|
||||||
}
|
}
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-policy-edit",
|
selector: "app-policy-edit",
|
||||||
@@ -66,22 +57,15 @@ export class PolicyEditComponent implements AfterViewInit {
|
|||||||
formGroup = this.formBuilder.group({
|
formGroup = this.formBuilder.group({
|
||||||
enabled: [this.enabled],
|
enabled: [this.enabled],
|
||||||
});
|
});
|
||||||
protected organization$: Observable<Organization>;
|
|
||||||
protected isBreadcrumbingEnabled$: Observable<boolean>;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DIALOG_DATA) protected data: PolicyEditDialogData,
|
@Inject(DIALOG_DATA) protected data: PolicyEditDialogData,
|
||||||
private accountService: AccountService,
|
|
||||||
private policyApiService: PolicyApiServiceAbstraction,
|
private policyApiService: PolicyApiServiceAbstraction,
|
||||||
private organizationService: OrganizationService,
|
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private cdr: ChangeDetectorRef,
|
private cdr: ChangeDetectorRef,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private dialogRef: DialogRef<PolicyEditDialogResult>,
|
private dialogRef: DialogRef<PolicyEditDialogResult>,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get policy(): BasePolicy {
|
get policy(): BasePolicy {
|
||||||
return this.data.policy;
|
return this.data.policy;
|
||||||
}
|
}
|
||||||
@@ -115,16 +99,6 @@ export class PolicyEditComponent implements AfterViewInit {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.organization$ = this.accountService.activeAccount$.pipe(
|
|
||||||
getUserId,
|
|
||||||
switchMap((userId) => this.organizationService.organizations$(userId)),
|
|
||||||
getOrganizationById(this.data.organizationId),
|
|
||||||
);
|
|
||||||
this.isBreadcrumbingEnabled$ = this.organization$.pipe(
|
|
||||||
switchMap((organization) =>
|
|
||||||
this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
@@ -154,8 +128,4 @@ export class PolicyEditComponent implements AfterViewInit {
|
|||||||
static open = (dialogService: DialogService, config: DialogConfig<PolicyEditDialogData>) => {
|
static open = (dialogService: DialogService, config: DialogConfig<PolicyEditDialogData>) => {
|
||||||
return dialogService.open<PolicyEditDialogResult>(PolicyEditComponent, config);
|
return dialogService.open<PolicyEditDialogResult>(PolicyEditComponent, config);
|
||||||
};
|
};
|
||||||
|
|
||||||
protected upgradePlan(): void {
|
|
||||||
this.dialogRef.close(PolicyEditDialogResult.UpgradePlan);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { inject, NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
import { CanMatchFn, RouterModule, Routes } from "@angular/router";
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
import { map } from "rxjs";
|
|
||||||
|
|
||||||
import { canAccessReportingTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { canAccessReportingTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { ExposedPasswordsReportComponent } from "../../../dirt/reports/pages/organizations/exposed-passwords-report.component";
|
import { ExposedPasswordsReportComponent } from "../../../dirt/reports/pages/organizations/exposed-passwords-report.component";
|
||||||
@@ -26,11 +23,6 @@ import { EventsComponent } from "../manage/events.component";
|
|||||||
|
|
||||||
import { ReportsHomeComponent } from "./reports-home.component";
|
import { ReportsHomeComponent } from "./reports-home.component";
|
||||||
|
|
||||||
const breadcrumbEventLogsPermission$: CanMatchFn = () =>
|
|
||||||
inject(ConfigService)
|
|
||||||
.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs)
|
|
||||||
.pipe(map((breadcrumbEventLogs) => breadcrumbEventLogs === true));
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
@@ -92,24 +84,10 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Event routing is temporarily duplicated
|
|
||||||
{
|
{
|
||||||
path: "events",
|
path: "events",
|
||||||
component: EventsComponent,
|
component: EventsComponent,
|
||||||
canMatch: [breadcrumbEventLogsPermission$], // if this matches, the flag is ON
|
canActivate: [organizationPermissionsGuard((org) => org.canAccessEventLogs || org.isOwner)],
|
||||||
canActivate: [
|
|
||||||
organizationPermissionsGuard(
|
|
||||||
(org) => (org.canAccessEventLogs && org.useEvents) || org.isOwner,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
data: {
|
|
||||||
titleId: "eventLogs",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "events",
|
|
||||||
component: EventsComponent,
|
|
||||||
canActivate: [organizationPermissionsGuard((org) => org.canAccessEventLogs)],
|
|
||||||
data: {
|
data: {
|
||||||
titleId: "eventLogs",
|
titleId: "eventLogs",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { NgModule, inject } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
import { RouterModule, Routes } from "@angular/router";
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
import { map } from "rxjs";
|
|
||||||
|
|
||||||
import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
|
||||||
|
|
||||||
import { organizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard";
|
import { organizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard";
|
||||||
import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard";
|
import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard";
|
||||||
@@ -43,14 +41,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: "policies",
|
path: "policies",
|
||||||
component: PoliciesComponent,
|
component: PoliciesComponent,
|
||||||
canActivate: [
|
canActivate: [organizationPermissionsGuard((org) => org.canManagePolicies)],
|
||||||
organizationPermissionsGuard((o: Organization) => {
|
|
||||||
const organizationBillingService = inject(OrganizationBillingServiceAbstraction);
|
|
||||||
return organizationBillingService
|
|
||||||
.isBreadcrumbingPoliciesEnabled$(o)
|
|
||||||
.pipe(map((isBreadcrumbingEnabled) => o.canManagePolicies || isBreadcrumbingEnabled));
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
data: {
|
data: {
|
||||||
titleId: "policies",
|
titleId: "policies",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import {
|
|||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { getById } from "@bitwarden/common/platform/misc";
|
import { getById } from "@bitwarden/common/platform/misc";
|
||||||
@@ -188,22 +187,16 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
await this.loadOrg(this.params.organizationId);
|
await this.loadOrg(this.params.organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBreadcrumbEventLogsEnabled = await firstValueFrom(
|
this.organizationSelected.setAsyncValidators(
|
||||||
this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs),
|
freeOrgCollectionLimitValidator(
|
||||||
|
this.organizations$,
|
||||||
|
this.collectionService
|
||||||
|
.encryptedCollections$(userId)
|
||||||
|
.pipe(map((collections) => collections ?? [])),
|
||||||
|
this.i18nService,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
this.formGroup.updateValueAndValidity();
|
||||||
if (isBreadcrumbEventLogsEnabled) {
|
|
||||||
this.organizationSelected.setAsyncValidators(
|
|
||||||
freeOrgCollectionLimitValidator(
|
|
||||||
this.organizations$,
|
|
||||||
this.collectionService
|
|
||||||
.encryptedCollections$(userId)
|
|
||||||
.pipe(map((collections) => collections ?? [])),
|
|
||||||
this.i18nService,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
this.formGroup.updateValueAndValidity();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.organizationSelected.valueChanges
|
this.organizationSelected.valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
|||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
@@ -218,28 +217,22 @@ export class VaultHeaderComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addCollection(): Promise<void> {
|
async addCollection(): Promise<void> {
|
||||||
const isBreadcrumbEventLogsEnabled = await firstValueFrom(
|
const organization = this.organizations?.find(
|
||||||
this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs),
|
(org) => org.productTierType === ProductTierType.Free,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isBreadcrumbEventLogsEnabled) {
|
if (this.organizations?.length == 1 && !!organization) {
|
||||||
const organization = this.organizations?.find(
|
const collections = await firstValueFrom(
|
||||||
(org) => org.productTierType === ProductTierType.Free,
|
this.accountService.activeAccount$.pipe(
|
||||||
);
|
getUserId,
|
||||||
|
switchMap((userId) =>
|
||||||
if (this.organizations?.length == 1 && !!organization) {
|
this.collectionAdminService.collectionAdminViews$(organization.id, userId),
|
||||||
const collections = await firstValueFrom(
|
|
||||||
this.accountService.activeAccount$.pipe(
|
|
||||||
getUserId,
|
|
||||||
switchMap((userId) =>
|
|
||||||
this.collectionAdminService.collectionAdminViews$(organization.id, userId),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
if (collections.length === organization.maxCollections) {
|
);
|
||||||
await this.showFreeOrgUpgradeDialog(organization);
|
if (collections.length === organization.maxCollections) {
|
||||||
return;
|
await this.showFreeOrgUpgradeDialog(organization);
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1332,7 +1332,6 @@ const safeProviders: SafeProvider[] = [
|
|||||||
I18nServiceAbstraction,
|
I18nServiceAbstraction,
|
||||||
OrganizationApiServiceAbstraction,
|
OrganizationApiServiceAbstraction,
|
||||||
SyncService,
|
SyncService,
|
||||||
ConfigService,
|
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
import { Observable } from "rxjs";
|
|
||||||
|
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
|
||||||
|
|
||||||
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
|
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
|
||||||
import { InitiationPath } from "../../models/request/reference-event.request";
|
import { InitiationPath } from "../../models/request/reference-event.request";
|
||||||
import { PaymentMethodType, PlanType } from "../enums";
|
import { PaymentMethodType, PlanType } from "../enums";
|
||||||
@@ -63,10 +59,4 @@ export abstract class OrganizationBillingServiceAbstraction {
|
|||||||
organizationId: string,
|
organizationId: string,
|
||||||
subscription: SubscriptionInformation,
|
subscription: SubscriptionInformation,
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if breadcrumbing policies is enabled for the organizations meeting certain criteria.
|
|
||||||
* @param organization
|
|
||||||
*/
|
|
||||||
abstract isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable<boolean>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { firstValueFrom, of } from "rxjs";
|
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationApiServiceAbstraction as OrganizationApiService } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction as OrganizationApiService } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import {
|
||||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
BillingApiServiceAbstraction,
|
||||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
SubscriptionInformation,
|
||||||
|
} from "@bitwarden/common/billing/abstractions";
|
||||||
|
import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums";
|
||||||
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
|
||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
describe("BillingAccountProfileStateService", () => {
|
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
|
||||||
|
import { EncString } from "../../key-management/crypto/models/enc-string";
|
||||||
|
import { OrgKey } from "../../types/key";
|
||||||
|
import { PaymentMethodResponse } from "../models/response/payment-method.response";
|
||||||
|
|
||||||
|
describe("OrganizationBillingService", () => {
|
||||||
let apiService: jest.Mocked<ApiService>;
|
let apiService: jest.Mocked<ApiService>;
|
||||||
let billingApiService: jest.Mocked<BillingApiServiceAbstraction>;
|
let billingApiService: jest.Mocked<BillingApiServiceAbstraction>;
|
||||||
let keyService: jest.Mocked<KeyService>;
|
let keyService: jest.Mocked<KeyService>;
|
||||||
@@ -24,7 +28,6 @@ describe("BillingAccountProfileStateService", () => {
|
|||||||
let i18nService: jest.Mocked<I18nService>;
|
let i18nService: jest.Mocked<I18nService>;
|
||||||
let organizationApiService: jest.Mocked<OrganizationApiService>;
|
let organizationApiService: jest.Mocked<OrganizationApiService>;
|
||||||
let syncService: jest.Mocked<SyncService>;
|
let syncService: jest.Mocked<SyncService>;
|
||||||
let configService: jest.Mocked<ConfigService>;
|
|
||||||
|
|
||||||
let sut: OrganizationBillingService;
|
let sut: OrganizationBillingService;
|
||||||
|
|
||||||
@@ -36,7 +39,6 @@ describe("BillingAccountProfileStateService", () => {
|
|||||||
i18nService = mock<I18nService>();
|
i18nService = mock<I18nService>();
|
||||||
organizationApiService = mock<OrganizationApiService>();
|
organizationApiService = mock<OrganizationApiService>();
|
||||||
syncService = mock<SyncService>();
|
syncService = mock<SyncService>();
|
||||||
configService = mock<ConfigService>();
|
|
||||||
|
|
||||||
sut = new OrganizationBillingService(
|
sut = new OrganizationBillingService(
|
||||||
apiService,
|
apiService,
|
||||||
@@ -46,7 +48,6 @@ describe("BillingAccountProfileStateService", () => {
|
|||||||
i18nService,
|
i18nService,
|
||||||
organizationApiService,
|
organizationApiService,
|
||||||
syncService,
|
syncService,
|
||||||
configService,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,98 +55,246 @@ describe("BillingAccountProfileStateService", () => {
|
|||||||
return jest.resetAllMocks();
|
return jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("isBreadcrumbingPoliciesEnabled", () => {
|
describe("getPaymentSource()", () => {
|
||||||
it("returns false when feature flag is disabled", async () => {
|
it("given a valid organization id, then it returns a payment source", async () => {
|
||||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
//Arrange
|
||||||
const org = {
|
const orgId = "organization-test";
|
||||||
isProviderUser: false,
|
const paymentMethodResponse = {
|
||||||
canEditSubscription: true,
|
paymentSource: { type: PaymentMethodType.Card },
|
||||||
productTierType: ProductTierType.Teams,
|
} as PaymentMethodResponse;
|
||||||
} as Organization;
|
billingApiService.getOrganizationPaymentMethod.mockResolvedValue(paymentMethodResponse);
|
||||||
|
|
||||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
//Act
|
||||||
expect(actual).toBe(false);
|
const returnedPaymentSource = await sut.getPaymentSource(orgId);
|
||||||
expect(configService.getFeatureFlag$).toHaveBeenCalledWith(
|
|
||||||
FeatureFlag.PM12276_BreadcrumbEventLogs,
|
//Assert
|
||||||
|
expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1);
|
||||||
|
expect(returnedPaymentSource).toEqual(paymentMethodResponse.paymentSource);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given an invalid organizationId, it should return undefined", async () => {
|
||||||
|
//Arrange
|
||||||
|
const orgId = "invalid-id";
|
||||||
|
billingApiService.getOrganizationPaymentMethod.mockResolvedValue(null);
|
||||||
|
|
||||||
|
//Act
|
||||||
|
const returnedPaymentSource = await sut.getPaymentSource(orgId);
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1);
|
||||||
|
expect(returnedPaymentSource).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given an API error occurs, then it throws the error", async () => {
|
||||||
|
// Arrange
|
||||||
|
const orgId = "error-org";
|
||||||
|
billingApiService.getOrganizationPaymentMethod.mockRejectedValue(new Error("API Error"));
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(sut.getPaymentSource(orgId)).rejects.toThrow("API Error");
|
||||||
|
expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("purchaseSubscription()", () => {
|
||||||
|
it("given valid subscription information, then it returns successful response", async () => {
|
||||||
|
//Arrange
|
||||||
|
const subscriptionInformation = {
|
||||||
|
organization: { name: "test-business", billingEmail: "test@test.com" },
|
||||||
|
plan: { type: PlanType.EnterpriseAnnually2023 },
|
||||||
|
payment: {
|
||||||
|
paymentMethod: ["card-token", PaymentMethodType.Card],
|
||||||
|
billing: { postalCode: "12345" },
|
||||||
|
},
|
||||||
|
} as SubscriptionInformation;
|
||||||
|
|
||||||
|
const organizationResponse = {
|
||||||
|
name: subscriptionInformation.organization.name,
|
||||||
|
billingEmail: subscriptionInformation.organization.billingEmail,
|
||||||
|
planType: subscriptionInformation.plan.type,
|
||||||
|
} as OrganizationResponse;
|
||||||
|
|
||||||
|
organizationApiService.create.mockResolvedValue(organizationResponse);
|
||||||
|
keyService.makeOrgKey.mockResolvedValue([new EncString("encrypyted-key"), {} as OrgKey]);
|
||||||
|
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypyted-key")]);
|
||||||
|
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypyted"));
|
||||||
|
|
||||||
|
//Act
|
||||||
|
const response = await sut.purchaseSubscription(subscriptionInformation);
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
expect(organizationApiService.create).toHaveBeenCalledTimes(1);
|
||||||
|
expect(response).toEqual(organizationResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given organization creation fails, then it throws an error", async () => {
|
||||||
|
// Arrange
|
||||||
|
const subscriptionInformation = {
|
||||||
|
organization: { name: "test-business", billingEmail: "test@test.com" },
|
||||||
|
plan: { type: PlanType.EnterpriseAnnually2023 },
|
||||||
|
payment: {
|
||||||
|
paymentMethod: ["card-token", PaymentMethodType.Card],
|
||||||
|
billing: { postalCode: "12345" },
|
||||||
|
},
|
||||||
|
} as SubscriptionInformation;
|
||||||
|
|
||||||
|
organizationApiService.create.mockRejectedValue(new Error("Failed to create organization"));
|
||||||
|
keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]);
|
||||||
|
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
|
||||||
|
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypyted"));
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(sut.purchaseSubscription(subscriptionInformation)).rejects.toThrow(
|
||||||
|
"Failed to create organization",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns false when organization belongs to a provider", async () => {
|
it("given key generation fails, then it throws an error", async () => {
|
||||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
// Arrange
|
||||||
const org = {
|
const subscriptionInformation = {
|
||||||
isProviderUser: true,
|
organization: { name: "test-business", billingEmail: "test@test.com" },
|
||||||
canEditSubscription: true,
|
plan: { type: PlanType.EnterpriseAnnually2023 },
|
||||||
productTierType: ProductTierType.Teams,
|
payment: {
|
||||||
} as Organization;
|
paymentMethod: ["card-token", PaymentMethodType.Card],
|
||||||
|
billing: { postalCode: "12345" },
|
||||||
|
},
|
||||||
|
} as SubscriptionInformation;
|
||||||
|
|
||||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
keyService.makeOrgKey.mockRejectedValue(new Error("Key generation failed"));
|
||||||
expect(actual).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false when cannot edit subscription", async () => {
|
// Act & Assert
|
||||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
await expect(sut.purchaseSubscription(subscriptionInformation)).rejects.toThrow(
|
||||||
const org = {
|
"Key generation failed",
|
||||||
isProviderUser: false,
|
|
||||||
canEditSubscription: false,
|
|
||||||
productTierType: ProductTierType.Teams,
|
|
||||||
} as Organization;
|
|
||||||
|
|
||||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
|
||||||
expect(actual).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
["Teams", ProductTierType.Teams],
|
|
||||||
["TeamsStarter", ProductTierType.TeamsStarter],
|
|
||||||
])("returns true when all conditions are met with %s tier", async (_, productTierType) => {
|
|
||||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
|
||||||
const org = {
|
|
||||||
isProviderUser: false,
|
|
||||||
canEditSubscription: true,
|
|
||||||
productTierType: productTierType,
|
|
||||||
} as Organization;
|
|
||||||
|
|
||||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
|
||||||
expect(actual).toBe(true);
|
|
||||||
expect(configService.getFeatureFlag$).toHaveBeenCalledWith(
|
|
||||||
FeatureFlag.PM12276_BreadcrumbEventLogs,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns false when product tier is not supported", async () => {
|
it("given an invalid plan type, then it throws an error", async () => {
|
||||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
// Arrange
|
||||||
const org = {
|
const subscriptionInformation = {
|
||||||
isProviderUser: false,
|
organization: { name: "test-business", billingEmail: "test@test.com" },
|
||||||
canEditSubscription: true,
|
plan: { type: -1 as unknown as PlanType },
|
||||||
productTierType: ProductTierType.Enterprise,
|
payment: {
|
||||||
} as Organization;
|
paymentMethod: ["card-token", PaymentMethodType.Card],
|
||||||
|
billing: { postalCode: "12345" },
|
||||||
|
},
|
||||||
|
} as SubscriptionInformation;
|
||||||
|
|
||||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
// Act & Assert
|
||||||
expect(actual).toBe(false);
|
await expect(sut.purchaseSubscription(subscriptionInformation)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("purchaseSubscriptionNoPaymentMethod()", () => {
|
||||||
|
it("given valid subscription information, then it returns successful response", async () => {
|
||||||
|
//Arrange
|
||||||
|
const subscriptionInformation = {
|
||||||
|
organization: { name: "test-business", billingEmail: "test@test.com" },
|
||||||
|
plan: { type: PlanType.EnterpriseAnnually2023 },
|
||||||
|
} as SubscriptionInformation;
|
||||||
|
|
||||||
|
const organizationResponse = {
|
||||||
|
name: subscriptionInformation.organization.name,
|
||||||
|
plan: { type: subscriptionInformation.plan.type },
|
||||||
|
planType: subscriptionInformation.plan.type,
|
||||||
|
} as OrganizationResponse;
|
||||||
|
|
||||||
|
organizationApiService.createWithoutPayment.mockResolvedValue(organizationResponse);
|
||||||
|
keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]);
|
||||||
|
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
|
||||||
|
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted"));
|
||||||
|
|
||||||
|
//Act
|
||||||
|
const response = await sut.purchaseSubscriptionNoPaymentMethod(subscriptionInformation);
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
expect(organizationApiService.createWithoutPayment).toHaveBeenCalledTimes(1);
|
||||||
|
expect(response).toEqual(organizationResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles all conditions false correctly", async () => {
|
it("given organization creation fails without payment method, then it throws an error", async () => {
|
||||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
const subscriptionInformation = {
|
||||||
const org = {
|
organization: { name: "test-business", billingEmail: "test@test.com" },
|
||||||
isProviderUser: true,
|
plan: { type: PlanType.EnterpriseAnnually2023 },
|
||||||
canEditSubscription: false,
|
} as SubscriptionInformation;
|
||||||
productTierType: ProductTierType.Free,
|
|
||||||
} as Organization;
|
|
||||||
|
|
||||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
organizationApiService.createWithoutPayment.mockRejectedValue(new Error("Creation failed"));
|
||||||
expect(actual).toBe(false);
|
keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]);
|
||||||
|
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
|
||||||
|
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted"));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.purchaseSubscriptionNoPaymentMethod(subscriptionInformation),
|
||||||
|
).rejects.toThrow("Creation failed");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("verifies feature flag is only called once", async () => {
|
it("given key generation fails, then it throws an error", async () => {
|
||||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
const subscriptionInformation = {
|
||||||
const org = {
|
organization: { name: "test-business", billingEmail: "test@test.com" },
|
||||||
isProviderUser: false,
|
plan: { type: PlanType.EnterpriseAnnually2023 },
|
||||||
canEditSubscription: true,
|
} as SubscriptionInformation;
|
||||||
productTierType: ProductTierType.Teams,
|
|
||||||
} as Organization;
|
|
||||||
|
|
||||||
await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
keyService.makeOrgKey.mockRejectedValue(new Error("Key generation failed"));
|
||||||
expect(configService.getFeatureFlag$).toHaveBeenCalledTimes(1);
|
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.purchaseSubscriptionNoPaymentMethod(subscriptionInformation),
|
||||||
|
).rejects.toThrow("Key generation failed");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("startFree()", () => {
|
||||||
|
it("given valid free plan information, then it creates a free organization", async () => {
|
||||||
|
const subscriptionInformation = {
|
||||||
|
organization: { name: "test-business", billingEmail: "test@test.com" },
|
||||||
|
plan: { type: PlanType.Free },
|
||||||
|
} as SubscriptionInformation;
|
||||||
|
|
||||||
|
const organizationResponse = {
|
||||||
|
name: subscriptionInformation.organization.name,
|
||||||
|
billingEmail: subscriptionInformation.organization.billingEmail,
|
||||||
|
planType: subscriptionInformation.plan.type,
|
||||||
|
} as OrganizationResponse;
|
||||||
|
|
||||||
|
organizationApiService.create.mockResolvedValue(organizationResponse);
|
||||||
|
keyService.makeOrgKey.mockResolvedValue([new EncString("encrypyted-key"), {} as OrgKey]);
|
||||||
|
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypyted-key")]);
|
||||||
|
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypyted"));
|
||||||
|
|
||||||
|
//Act
|
||||||
|
const response = await sut.startFree(subscriptionInformation);
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
expect(organizationApiService.create).toHaveBeenCalledTimes(1);
|
||||||
|
expect(response).toEqual(organizationResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given key generation fails, then it throws an error", async () => {
|
||||||
|
const subscriptionInformation = {
|
||||||
|
organization: { name: "test-business", billingEmail: "test@test.com" },
|
||||||
|
plan: { type: PlanType.Free },
|
||||||
|
} as SubscriptionInformation;
|
||||||
|
|
||||||
|
keyService.makeOrgKey.mockRejectedValue(new Error("Key generation failed"));
|
||||||
|
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
|
||||||
|
|
||||||
|
await expect(sut.startFree(subscriptionInformation)).rejects.toThrow("Key generation failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("given organization creation fails, then it throws an error", async () => {
|
||||||
|
// Arrange
|
||||||
|
const subscriptionInformation = {
|
||||||
|
organization: { name: "test-business", billingEmail: "test@test.com" },
|
||||||
|
plan: { type: PlanType.Free },
|
||||||
|
} as SubscriptionInformation;
|
||||||
|
|
||||||
|
organizationApiService.create.mockRejectedValue(new Error("Failed to create organization"));
|
||||||
|
keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]);
|
||||||
|
keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]);
|
||||||
|
encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypyted"));
|
||||||
|
// Act & Assert
|
||||||
|
await expect(sut.startFree(subscriptionInformation)).rejects.toThrow(
|
||||||
|
"Failed to create organization",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Observable, of, switchMap } from "rxjs";
|
|
||||||
|
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
|
||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
@@ -27,7 +22,7 @@ import {
|
|||||||
PlanInformation,
|
PlanInformation,
|
||||||
SubscriptionInformation,
|
SubscriptionInformation,
|
||||||
} from "../abstractions";
|
} from "../abstractions";
|
||||||
import { PlanType, ProductTierType } from "../enums";
|
import { PlanType } from "../enums";
|
||||||
import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request";
|
import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request";
|
||||||
import { PaymentSourceResponse } from "../models/response/payment-source.response";
|
import { PaymentSourceResponse } from "../models/response/payment-source.response";
|
||||||
|
|
||||||
@@ -47,12 +42,11 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
|
|||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private organizationApiService: OrganizationApiService,
|
private organizationApiService: OrganizationApiService,
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
private configService: ConfigService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getPaymentSource(organizationId: string): Promise<PaymentSourceResponse> {
|
async getPaymentSource(organizationId: string): Promise<PaymentSourceResponse> {
|
||||||
const paymentMethod = await this.billingApiService.getOrganizationPaymentMethod(organizationId);
|
const paymentMethod = await this.billingApiService.getOrganizationPaymentMethod(organizationId);
|
||||||
return paymentMethod.paymentSource;
|
return paymentMethod?.paymentSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
async purchaseSubscription(subscription: SubscriptionInformation): Promise<OrganizationResponse> {
|
async purchaseSubscription(subscription: SubscriptionInformation): Promise<OrganizationResponse> {
|
||||||
@@ -229,29 +223,4 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
|
|||||||
this.setPaymentInformation(request, subscription.payment);
|
this.setPaymentInformation(request, subscription.payment);
|
||||||
await this.billingApiService.restartSubscription(organizationId, request);
|
await this.billingApiService.restartSubscription(organizationId, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable<boolean> {
|
|
||||||
if (organization === null || organization === undefined) {
|
|
||||||
return of(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs).pipe(
|
|
||||||
switchMap((featureFlagEnabled) => {
|
|
||||||
if (!featureFlagEnabled) {
|
|
||||||
return of(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organization.isProviderUser || !organization.canEditSubscription) {
|
|
||||||
return of(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const supportedProducts = [ProductTierType.Teams, ProductTierType.TeamsStarter];
|
|
||||||
const isSupportedProduct = supportedProducts.some(
|
|
||||||
(product) => product === organization.productTierType,
|
|
||||||
);
|
|
||||||
|
|
||||||
return of(isSupportedProduct);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export enum FeatureFlag {
|
|||||||
|
|
||||||
/* Billing */
|
/* Billing */
|
||||||
TrialPaymentOptional = "PM-8163-trial-payment",
|
TrialPaymentOptional = "PM-8163-trial-payment",
|
||||||
PM12276_BreadcrumbEventLogs = "pm-12276-breadcrumbing-for-business-features",
|
|
||||||
PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships",
|
PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships",
|
||||||
UseOrganizationWarningsService = "use-organization-warnings-service",
|
UseOrganizationWarningsService = "use-organization-warnings-service",
|
||||||
PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout",
|
PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout",
|
||||||
@@ -95,7 +94,6 @@ export const DefaultFeatureFlagValue = {
|
|||||||
|
|
||||||
/* Billing */
|
/* Billing */
|
||||||
[FeatureFlag.TrialPaymentOptional]: FALSE,
|
[FeatureFlag.TrialPaymentOptional]: FALSE,
|
||||||
[FeatureFlag.PM12276_BreadcrumbEventLogs]: FALSE,
|
|
||||||
[FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE,
|
[FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE,
|
||||||
[FeatureFlag.UseOrganizationWarningsService]: FALSE,
|
[FeatureFlag.UseOrganizationWarningsService]: FALSE,
|
||||||
[FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout]: FALSE,
|
[FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout]: FALSE,
|
||||||
|
|||||||
Reference in New Issue
Block a user