mirror of
https://github.com/bitwarden/browser.git
synced 2024-08-26 23:09:46 +02:00
Added create-client-organization.component (#8767)
This commit is contained in:
parent
cbaf3462c1
commit
1e0ad09757
|
@ -7958,5 +7958,26 @@
|
||||||
},
|
},
|
||||||
"errorAssigningTargetFolder": {
|
"errorAssigningTargetFolder": {
|
||||||
"message": "Error assigning target folder."
|
"message": "Error assigning target folder."
|
||||||
|
},
|
||||||
|
"createNewClientToManageAsProvider": {
|
||||||
|
"message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle."
|
||||||
|
},
|
||||||
|
"selectAPlan": {
|
||||||
|
"message": "Select a plan"
|
||||||
|
},
|
||||||
|
"thirtyFivePercentDiscount": {
|
||||||
|
"message": "35% Discount"
|
||||||
|
},
|
||||||
|
"monthPerMember": {
|
||||||
|
"message": "month per member"
|
||||||
|
},
|
||||||
|
"seats": {
|
||||||
|
"message": "Seats"
|
||||||
|
},
|
||||||
|
"addOrganization": {
|
||||||
|
"message": "Add organization"
|
||||||
|
},
|
||||||
|
"createdNewClient": {
|
||||||
|
"message": "Successfully created new client"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,7 @@ const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: "manage-client-organizations",
|
path: "manage-client-organizations",
|
||||||
component: ManageClientOrganizationsComponent,
|
component: ManageClientOrganizationsComponent,
|
||||||
data: { titleId: "manage-client-organizations" },
|
data: { titleId: "clients" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "manage",
|
path: "manage",
|
||||||
|
|
|
@ -4,13 +4,16 @@ import { FormsModule } from "@angular/forms";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { SearchModule } from "@bitwarden/components";
|
import { SearchModule } from "@bitwarden/components";
|
||||||
|
import { DangerZoneComponent } from "@bitwarden/web-vault/app/auth/settings/account/danger-zone.component";
|
||||||
import { OrganizationPlansComponent, TaxInfoComponent } from "@bitwarden/web-vault/app/billing";
|
import { OrganizationPlansComponent, TaxInfoComponent } from "@bitwarden/web-vault/app/billing";
|
||||||
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
|
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
|
||||||
import { OssModule } from "@bitwarden/web-vault/app/oss.module";
|
import { OssModule } from "@bitwarden/web-vault/app/oss.module";
|
||||||
|
|
||||||
import { DangerZoneComponent } from "../../../../../../apps/web/src/app/auth/settings/account/danger-zone.component";
|
import {
|
||||||
import { ManageClientOrganizationSubscriptionComponent } from "../../billing/providers/clients/manage-client-organization-subscription.component";
|
CreateClientOrganizationComponent,
|
||||||
import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component";
|
ManageClientOrganizationSubscriptionComponent,
|
||||||
|
ManageClientOrganizationsComponent,
|
||||||
|
} from "../../billing/providers/clients";
|
||||||
|
|
||||||
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
||||||
import { ClientsComponent } from "./clients/clients.component";
|
import { ClientsComponent } from "./clients/clients.component";
|
||||||
|
@ -56,6 +59,7 @@ import { SetupComponent } from "./setup/setup.component";
|
||||||
SetupComponent,
|
SetupComponent,
|
||||||
SetupProviderComponent,
|
SetupProviderComponent,
|
||||||
UserAddEditComponent,
|
UserAddEditComponent,
|
||||||
|
CreateClientOrganizationComponent,
|
||||||
ManageClientOrganizationsComponent,
|
ManageClientOrganizationsComponent,
|
||||||
ManageClientOrganizationSubscriptionComponent,
|
ManageClientOrganizationSubscriptionComponent,
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
||||||
import { ProviderAddOrganizationRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-add-organization.request";
|
import { ProviderAddOrganizationRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-add-organization.request";
|
||||||
|
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
||||||
|
import { PlanType } from "@bitwarden/common/billing/enums";
|
||||||
|
import { CreateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/create-client-organization.request";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { OrgKey } from "@bitwarden/common/types/key";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -11,6 +18,9 @@ export class WebProviderService {
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private encryptService: EncryptService,
|
||||||
|
private billingApiService: BillingApiServiceAbstraction,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async addOrganizationToProvider(providerId: string, organizationId: string) {
|
async addOrganizationToProvider(providerId: string, organizationId: string) {
|
||||||
|
@ -28,6 +38,46 @@ export class WebProviderService {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createClientOrganization(
|
||||||
|
providerId: string,
|
||||||
|
name: string,
|
||||||
|
ownerEmail: string,
|
||||||
|
planType: PlanType,
|
||||||
|
seats: number,
|
||||||
|
): Promise<void> {
|
||||||
|
const organizationKey = (await this.cryptoService.makeOrgKey<OrgKey>())[1];
|
||||||
|
|
||||||
|
const [publicKey, encryptedPrivateKey] = await this.cryptoService.makeKeyPair(organizationKey);
|
||||||
|
|
||||||
|
const encryptedCollectionName = await this.encryptService.encrypt(
|
||||||
|
this.i18nService.t("defaultCollection"),
|
||||||
|
organizationKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
const providerKey = await this.cryptoService.getProviderKey(providerId);
|
||||||
|
|
||||||
|
const encryptedProviderKey = await this.encryptService.encrypt(
|
||||||
|
organizationKey.key,
|
||||||
|
providerKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
const request = new CreateClientOrganizationRequest();
|
||||||
|
request.name = name;
|
||||||
|
request.ownerEmail = ownerEmail;
|
||||||
|
request.planType = planType;
|
||||||
|
request.seats = seats;
|
||||||
|
|
||||||
|
request.key = encryptedProviderKey.encryptedString;
|
||||||
|
request.keyPair = new OrganizationKeysRequest(publicKey, encryptedPrivateKey.encryptedString);
|
||||||
|
request.collectionName = encryptedCollectionName.encryptedString;
|
||||||
|
|
||||||
|
await this.billingApiService.createClientOrganization(providerId, request);
|
||||||
|
|
||||||
|
await this.apiService.refreshIdentityToken();
|
||||||
|
|
||||||
|
await this.syncService.fullSync(true);
|
||||||
|
}
|
||||||
|
|
||||||
async detachOrganization(providerId: string, organizationId: string): Promise<any> {
|
async detachOrganization(providerId: string, organizationId: string): Promise<any> {
|
||||||
await this.apiService.deleteProviderOrganization(providerId, organizationId);
|
await this.apiService.deleteProviderOrganization(providerId, organizationId);
|
||||||
await this.syncService.fullSync(true);
|
await this.syncService.fullSync(true);
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
|
<bit-dialog dialogSize="large">
|
||||||
|
<span bitDialogTitle class="tw-font-semibold">
|
||||||
|
{{ "newClientOrganization" | i18n }}
|
||||||
|
</span>
|
||||||
|
<div bitDialogContent>
|
||||||
|
<p>{{ "createNewClientToManageAsProvider" | i18n }}</p>
|
||||||
|
<div class="tw-mb-3">
|
||||||
|
<span class="tw-text-lg tw-pr-1">{{ "selectAPlan" | i18n }}</span>
|
||||||
|
<span bitBadge variant="success">{{ "thirtyFivePercentDiscount" | i18n }}</span>
|
||||||
|
</div>
|
||||||
|
<ng-container>
|
||||||
|
<div class="tw-grid tw-grid-flow-col tw-grid-cols-2 tw-gap-4 tw-mb-4">
|
||||||
|
<div
|
||||||
|
*ngFor="let planCard of planCards"
|
||||||
|
[ngClass]="getPlanCardContainerClasses(planCard.selected)"
|
||||||
|
(click)="selectPlan(planCard.name)"
|
||||||
|
>
|
||||||
|
<div class="tw-relative">
|
||||||
|
<div
|
||||||
|
*ngIf="planCard.selected"
|
||||||
|
class="tw-bg-primary-600 tw-text-center !tw-text-contrast tw-text-sm tw-font-bold tw-py-1 group-hover:tw-bg-primary-700"
|
||||||
|
>
|
||||||
|
{{ "selected" | i18n }}
|
||||||
|
</div>
|
||||||
|
<div class="tw-p-5" [ngClass]="{ 'tw-pt-12': !planCard.selected }">
|
||||||
|
<h3 class="tw-text-2xl tw-font-bold tw-uppercase">{{ planCard.name }}</h3>
|
||||||
|
<span class="tw-text-2xl tw-font-semibold">{{
|
||||||
|
planCard.cost | currency: "$"
|
||||||
|
}}</span>
|
||||||
|
<span class="tw-text-sm tw-font-bold">/{{ "monthPerMember" | i18n }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<div class="tw-grid tw-grid-flow-col tw-grid-cols-2 tw-gap-4">
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>
|
||||||
|
{{ "organizationName" | i18n }}
|
||||||
|
</bit-label>
|
||||||
|
<input type="text" bitInput formControlName="organizationName" />
|
||||||
|
</bit-form-field>
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>
|
||||||
|
{{ "clientOwnerEmail" | i18n }}
|
||||||
|
</bit-label>
|
||||||
|
<input type="text" bitInput formControlName="clientOwnerEmail" />
|
||||||
|
</bit-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="tw-grid tw-grid-flow-col tw-grid-cols-2 tw-gap-4">
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>
|
||||||
|
{{ "seats" | i18n }}
|
||||||
|
</bit-label>
|
||||||
|
<input type="text" bitInput formControlName="seats" />
|
||||||
|
</bit-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-container bitDialogFooter>
|
||||||
|
<button bitButton bitFormButton buttonType="primary" type="submit">
|
||||||
|
{{ "addOrganization" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button bitButton buttonType="secondary" type="button" [bitDialogClose]="ResultType.Closed">
|
||||||
|
{{ "close" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</bit-dialog>
|
||||||
|
</form>
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
|
import { Component, Inject, OnInit } from "@angular/core";
|
||||||
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
|
|
||||||
|
import { PlanType } from "@bitwarden/common/billing/enums";
|
||||||
|
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service";
|
||||||
|
|
||||||
|
type CreateClientOrganizationParams = {
|
||||||
|
providerId: string;
|
||||||
|
plans: PlanResponse[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum CreateClientOrganizationResultType {
|
||||||
|
Closed = "closed",
|
||||||
|
Submitted = "submitted",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const openCreateClientOrganizationDialog = (
|
||||||
|
dialogService: DialogService,
|
||||||
|
dialogConfig: DialogConfig<CreateClientOrganizationParams>,
|
||||||
|
) =>
|
||||||
|
dialogService.open<CreateClientOrganizationResultType, CreateClientOrganizationParams>(
|
||||||
|
CreateClientOrganizationComponent,
|
||||||
|
dialogConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
type PlanCard = {
|
||||||
|
name: string;
|
||||||
|
cost: number;
|
||||||
|
type: PlanType;
|
||||||
|
selected: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-create-client-organization",
|
||||||
|
templateUrl: "./create-client-organization.component.html",
|
||||||
|
})
|
||||||
|
export class CreateClientOrganizationComponent implements OnInit {
|
||||||
|
protected ResultType = CreateClientOrganizationResultType;
|
||||||
|
protected formGroup = this.formBuilder.group({
|
||||||
|
clientOwnerEmail: ["", [Validators.required, Validators.email]],
|
||||||
|
organizationName: ["", Validators.required],
|
||||||
|
seats: [null, [Validators.required, Validators.min(1)]],
|
||||||
|
});
|
||||||
|
protected planCards: PlanCard[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DIALOG_DATA) private dialogParams: CreateClientOrganizationParams,
|
||||||
|
private dialogRef: DialogRef<CreateClientOrganizationResultType>,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private webProviderService: WebProviderService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
protected getPlanCardContainerClasses(selected: boolean) {
|
||||||
|
switch (selected) {
|
||||||
|
case true: {
|
||||||
|
return [
|
||||||
|
"tw-group",
|
||||||
|
"tw-cursor-pointer",
|
||||||
|
"tw-block",
|
||||||
|
"tw-rounded",
|
||||||
|
"tw-border",
|
||||||
|
"tw-border-solid",
|
||||||
|
"tw-border-primary-600",
|
||||||
|
"hover:tw-border-primary-700",
|
||||||
|
"focus:tw-border-2",
|
||||||
|
"focus:tw-border-primary-700",
|
||||||
|
"focus:tw-rounded-lg",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
case false: {
|
||||||
|
return [
|
||||||
|
"tw-cursor-pointer",
|
||||||
|
"tw-block",
|
||||||
|
"tw-rounded",
|
||||||
|
"tw-border",
|
||||||
|
"tw-border-solid",
|
||||||
|
"tw-border-secondary-300",
|
||||||
|
"hover:tw-border-text-main",
|
||||||
|
"focus:tw-border-2",
|
||||||
|
"focus:tw-border-primary-700",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
const teamsPlan = this.dialogParams.plans.find((plan) => plan.type === PlanType.TeamsMonthly);
|
||||||
|
const enterprisePlan = this.dialogParams.plans.find(
|
||||||
|
(plan) => plan.type === PlanType.EnterpriseMonthly,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.planCards = [
|
||||||
|
{
|
||||||
|
name: this.i18nService.t("planNameTeams"),
|
||||||
|
cost: teamsPlan.PasswordManager.seatPrice * 0.65, // 35% off for MSPs,
|
||||||
|
type: teamsPlan.type,
|
||||||
|
selected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: this.i18nService.t("planNameEnterprise"),
|
||||||
|
cost: enterprisePlan.PasswordManager.seatPrice * 0.65, // 35% off for MSPs,
|
||||||
|
type: enterprisePlan.type,
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected selectPlan(name: string) {
|
||||||
|
this.planCards.find((planCard) => planCard.name === name).selected = true;
|
||||||
|
this.planCards.find((planCard) => planCard.name !== name).selected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
submit = async () => {
|
||||||
|
this.formGroup.markAllAsTouched();
|
||||||
|
|
||||||
|
if (this.formGroup.invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedPlanCard = this.planCards.find((planCard) => planCard.selected);
|
||||||
|
|
||||||
|
await this.webProviderService.createClientOrganization(
|
||||||
|
this.dialogParams.providerId,
|
||||||
|
this.formGroup.value.organizationName,
|
||||||
|
this.formGroup.value.clientOwnerEmail,
|
||||||
|
selectedPlanCard.type,
|
||||||
|
this.formGroup.value.seats,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("createdNewClient"));
|
||||||
|
|
||||||
|
this.dialogRef.close(this.ResultType.Submitted);
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./create-client-organization.component";
|
||||||
|
export * from "./manage-client-organizations.component";
|
||||||
|
export * from "./manage-client-organization-subscription.component";
|
|
@ -3,7 +3,7 @@ import { Component, Inject, OnInit } from "@angular/core";
|
||||||
|
|
||||||
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
||||||
import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
||||||
import { ProviderSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/provider-subscription-update.request";
|
import { UpdateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/update-client-organization.request";
|
||||||
import { Plans } from "@bitwarden/common/billing/models/response/provider-subscription-response";
|
import { Plans } from "@bitwarden/common/billing/models/response/provider-subscription-response";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
@ -45,7 +45,7 @@ export class ManageClientOrganizationSubscriptionComponent implements OnInit {
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
try {
|
try {
|
||||||
const response = await this.billingApiService.getProviderClientSubscriptions(this.providerId);
|
const response = await this.billingApiService.getProviderSubscription(this.providerId);
|
||||||
this.AdditionalSeatPurchased = this.getPurchasedSeatsByPlan(this.planName, response.plans);
|
this.AdditionalSeatPurchased = this.getPurchasedSeatsByPlan(this.planName, response.plans);
|
||||||
const seatMinimum = this.getProviderSeatMinimumByPlan(this.planName, response.plans);
|
const seatMinimum = this.getProviderSeatMinimumByPlan(this.planName, response.plans);
|
||||||
const assignedByPlan = this.getAssignedByPlan(this.planName, response.plans);
|
const assignedByPlan = this.getAssignedByPlan(this.planName, response.plans);
|
||||||
|
@ -69,10 +69,10 @@ export class ManageClientOrganizationSubscriptionComponent implements OnInit {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = new ProviderSubscriptionUpdateRequest();
|
const request = new UpdateClientOrganizationRequest();
|
||||||
request.assignedSeats = assignedSeats;
|
request.assignedSeats = assignedSeats;
|
||||||
|
|
||||||
await this.billingApiService.putProviderClientSubscriptions(
|
await this.billingApiService.updateClientOrganization(
|
||||||
this.providerId,
|
this.providerId,
|
||||||
this.providerOrganizationId,
|
this.providerOrganizationId,
|
||||||
request,
|
request,
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
<app-header>
|
<app-header>
|
||||||
<bit-search [placeholder]="'search' | i18n" [(ngModel)]="searchText"></bit-search>
|
<bit-search [placeholder]="'search' | i18n" [(ngModel)]="searchText"></bit-search>
|
||||||
<a bitButton routerLink="create" *ngIf="manageOrganizations" buttonType="primary">
|
<a
|
||||||
|
type="button"
|
||||||
|
bitButton
|
||||||
|
*ngIf="manageOrganizations"
|
||||||
|
buttonType="primary"
|
||||||
|
(click)="createClientOrganization()"
|
||||||
|
>
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
{{ "addNewOrganization" | i18n }}
|
{{ "addNewOrganization" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { SelectionModel } from "@angular/cdk/collections";
|
import { SelectionModel } from "@angular/cdk/collections";
|
||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs";
|
import { BehaviorSubject, firstValueFrom, from, lastValueFrom, Subject } from "rxjs";
|
||||||
import { first, switchMap, takeUntil } from "rxjs/operators";
|
import { first, switchMap, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
@ -9,6 +9,8 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||||
import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
||||||
|
import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
||||||
|
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
|
@ -16,6 +18,10 @@ import { DialogService, TableDataSource } from "@bitwarden/components";
|
||||||
|
|
||||||
import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service";
|
import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CreateClientOrganizationResultType,
|
||||||
|
openCreateClientOrganizationDialog,
|
||||||
|
} from "./create-client-organization.component";
|
||||||
import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-organization-subscription.component";
|
import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-organization-subscription.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -52,6 +58,7 @@ export class ManageClientOrganizationsComponent implements OnInit, OnDestroy {
|
||||||
private pagedClientsCount = 0;
|
private pagedClientsCount = 0;
|
||||||
selection = new SelectionModel<string>(true, []);
|
selection = new SelectionModel<string>(true, []);
|
||||||
protected dataSource = new TableDataSource<ProviderOrganizationOrganizationDetailsResponse>();
|
protected dataSource = new TableDataSource<ProviderOrganizationOrganizationDetailsResponse>();
|
||||||
|
protected plans: PlanResponse[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
@ -63,6 +70,7 @@ export class ManageClientOrganizationsComponent implements OnInit, OnDestroy {
|
||||||
private validationService: ValidationService,
|
private validationService: ValidationService,
|
||||||
private webProviderService: WebProviderService,
|
private webProviderService: WebProviderService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
|
private billingApiService: BillingApiService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
@ -94,12 +102,16 @@ export class ManageClientOrganizationsComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
const response = await this.apiService.getProviderClients(this.providerId);
|
const clientsResponse = await this.apiService.getProviderClients(this.providerId);
|
||||||
this.clients = response.data != null && response.data.length > 0 ? response.data : [];
|
this.clients =
|
||||||
|
clientsResponse.data != null && clientsResponse.data.length > 0 ? clientsResponse.data : [];
|
||||||
this.dataSource.data = this.clients;
|
this.dataSource.data = this.clients;
|
||||||
this.manageOrganizations =
|
this.manageOrganizations =
|
||||||
(await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin;
|
(await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin;
|
||||||
|
|
||||||
|
const plansResponse = await this.billingApiService.getPlans();
|
||||||
|
this.plans = plansResponse.data;
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,4 +189,21 @@ export class ManageClientOrganizationsComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
this.actionPromise = null;
|
this.actionPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createClientOrganization = async () => {
|
||||||
|
const reference = openCreateClientOrganizationDialog(this.dialogService, {
|
||||||
|
data: {
|
||||||
|
providerId: this.providerId,
|
||||||
|
plans: this.plans,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await lastValueFrom(reference.closed);
|
||||||
|
|
||||||
|
if (result === CreateClientOrganizationResultType.Closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.load();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1051,10 +1051,12 @@ const safeProviders: SafeProvider[] = [
|
||||||
provide: OrganizationBillingServiceAbstraction,
|
provide: OrganizationBillingServiceAbstraction,
|
||||||
useClass: OrganizationBillingService,
|
useClass: OrganizationBillingService,
|
||||||
deps: [
|
deps: [
|
||||||
|
ApiServiceAbstraction,
|
||||||
CryptoServiceAbstraction,
|
CryptoServiceAbstraction,
|
||||||
EncryptService,
|
EncryptService,
|
||||||
I18nServiceAbstraction,
|
I18nServiceAbstraction,
|
||||||
OrganizationApiServiceAbstraction,
|
OrganizationApiServiceAbstraction,
|
||||||
|
SyncServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
||||||
import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response";
|
import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response";
|
||||||
import { ProviderSubscriptionUpdateRequest } from "../models/request/provider-subscription-update.request";
|
import { PlanResponse } from "../../billing/models/response/plan.response";
|
||||||
|
import { ListResponse } from "../../models/response/list.response";
|
||||||
|
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
|
||||||
|
import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request";
|
||||||
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
|
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
|
||||||
|
|
||||||
export abstract class BillingApiServiceAbstraction {
|
export abstract class BillingApiServiceAbstraction {
|
||||||
|
@ -9,11 +12,16 @@ export abstract class BillingApiServiceAbstraction {
|
||||||
request: SubscriptionCancellationRequest,
|
request: SubscriptionCancellationRequest,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise<void>;
|
cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise<void>;
|
||||||
|
createClientOrganization: (
|
||||||
|
providerId: string,
|
||||||
|
request: CreateClientOrganizationRequest,
|
||||||
|
) => Promise<void>;
|
||||||
getBillingStatus: (id: string) => Promise<OrganizationBillingStatusResponse>;
|
getBillingStatus: (id: string) => Promise<OrganizationBillingStatusResponse>;
|
||||||
getProviderClientSubscriptions: (providerId: string) => Promise<ProviderSubscriptionResponse>;
|
getPlans: () => Promise<ListResponse<PlanResponse>>;
|
||||||
putProviderClientSubscriptions: (
|
getProviderSubscription: (providerId: string) => Promise<ProviderSubscriptionResponse>;
|
||||||
|
updateClientOrganization: (
|
||||||
providerId: string,
|
providerId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
request: ProviderSubscriptionUpdateRequest,
|
request: UpdateClientOrganizationRequest,
|
||||||
) => Promise<any>;
|
) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { OrganizationKeysRequest } from "../../../admin-console/models/request/organization-keys.request";
|
||||||
|
import { PlanType } from "../../../billing/enums";
|
||||||
|
|
||||||
|
export class CreateClientOrganizationRequest {
|
||||||
|
name: string;
|
||||||
|
ownerEmail: string;
|
||||||
|
planType: PlanType;
|
||||||
|
seats: number;
|
||||||
|
key: string;
|
||||||
|
keyPair: OrganizationKeysRequest;
|
||||||
|
collectionName: string;
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
export class ProviderSubscriptionUpdateRequest {
|
|
||||||
assignedSeats: number;
|
|
||||||
}
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export class UpdateClientOrganizationRequest {
|
||||||
|
assignedSeats: number;
|
||||||
|
}
|
|
@ -2,7 +2,10 @@ import { ApiService } from "../../abstractions/api.service";
|
||||||
import { BillingApiServiceAbstraction } from "../../billing/abstractions/billilng-api.service.abstraction";
|
import { BillingApiServiceAbstraction } from "../../billing/abstractions/billilng-api.service.abstraction";
|
||||||
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
||||||
import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response";
|
import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response";
|
||||||
import { ProviderSubscriptionUpdateRequest } from "../models/request/provider-subscription-update.request";
|
import { PlanResponse } from "../../billing/models/response/plan.response";
|
||||||
|
import { ListResponse } from "../../models/response/list.response";
|
||||||
|
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
|
||||||
|
import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request";
|
||||||
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
|
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
|
||||||
|
|
||||||
export class BillingApiService implements BillingApiServiceAbstraction {
|
export class BillingApiService implements BillingApiServiceAbstraction {
|
||||||
|
@ -25,6 +28,19 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||||
return this.apiService.send("POST", "/accounts/cancel", request, true, false);
|
return this.apiService.send("POST", "/accounts/cancel", request, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createClientOrganization(
|
||||||
|
providerId: string,
|
||||||
|
request: CreateClientOrganizationRequest,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.apiService.send(
|
||||||
|
"POST",
|
||||||
|
"/providers/" + providerId + "/clients",
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getBillingStatus(id: string): Promise<OrganizationBillingStatusResponse> {
|
async getBillingStatus(id: string): Promise<OrganizationBillingStatusResponse> {
|
||||||
const r = await this.apiService.send(
|
const r = await this.apiService.send(
|
||||||
"GET",
|
"GET",
|
||||||
|
@ -37,7 +53,12 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||||
return new OrganizationBillingStatusResponse(r);
|
return new OrganizationBillingStatusResponse(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProviderClientSubscriptions(providerId: string): Promise<ProviderSubscriptionResponse> {
|
async getPlans(): Promise<ListResponse<PlanResponse>> {
|
||||||
|
const r = await this.apiService.send("GET", "/plans", null, false, true);
|
||||||
|
return new ListResponse(r, PlanResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProviderSubscription(providerId: string): Promise<ProviderSubscriptionResponse> {
|
||||||
const r = await this.apiService.send(
|
const r = await this.apiService.send(
|
||||||
"GET",
|
"GET",
|
||||||
"/providers/" + providerId + "/billing/subscription",
|
"/providers/" + providerId + "/billing/subscription",
|
||||||
|
@ -48,14 +69,14 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||||
return new ProviderSubscriptionResponse(r);
|
return new ProviderSubscriptionResponse(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
async putProviderClientSubscriptions(
|
async updateClientOrganization(
|
||||||
providerId: string,
|
providerId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
request: ProviderSubscriptionUpdateRequest,
|
request: UpdateClientOrganizationRequest,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return await this.apiService.send(
|
return await this.apiService.send(
|
||||||
"PUT",
|
"PUT",
|
||||||
"/providers/" + providerId + "/organizations/" + organizationId,
|
"/providers/" + providerId + "/clients/" + organizationId,
|
||||||
request,
|
request,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
|
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
|
||||||
import { OrganizationKeysRequest } from "../../admin-console/models/request/organization-keys.request";
|
import { OrganizationKeysRequest } from "../../admin-console/models/request/organization-keys.request";
|
||||||
|
@ -7,6 +8,7 @@ import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||||
import { EncString } from "../../platform/models/domain/enc-string";
|
import { EncString } from "../../platform/models/domain/enc-string";
|
||||||
import { OrgKey } from "../../types/key";
|
import { OrgKey } from "../../types/key";
|
||||||
|
import { SyncService } from "../../vault/abstractions/sync/sync.service.abstraction";
|
||||||
import {
|
import {
|
||||||
OrganizationBillingServiceAbstraction,
|
OrganizationBillingServiceAbstraction,
|
||||||
OrganizationInformation,
|
OrganizationInformation,
|
||||||
|
@ -25,10 +27,12 @@ interface OrganizationKeys {
|
||||||
|
|
||||||
export class OrganizationBillingService implements OrganizationBillingServiceAbstraction {
|
export class OrganizationBillingService implements OrganizationBillingServiceAbstraction {
|
||||||
constructor(
|
constructor(
|
||||||
|
private apiService: ApiService,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private organizationApiService: OrganizationApiService,
|
private organizationApiService: OrganizationApiService,
|
||||||
|
private syncService: SyncService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async purchaseSubscription(subscription: SubscriptionInformation): Promise<OrganizationResponse> {
|
async purchaseSubscription(subscription: SubscriptionInformation): Promise<OrganizationResponse> {
|
||||||
|
@ -44,7 +48,13 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
|
||||||
|
|
||||||
this.setPaymentInformation(request, subscription.payment);
|
this.setPaymentInformation(request, subscription.payment);
|
||||||
|
|
||||||
return await this.organizationApiService.create(request);
|
const response = await this.organizationApiService.create(request);
|
||||||
|
|
||||||
|
await this.apiService.refreshIdentityToken();
|
||||||
|
|
||||||
|
await this.syncService.fullSync(true);
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async startFree(subscription: SubscriptionInformation): Promise<OrganizationResponse> {
|
async startFree(subscription: SubscriptionInformation): Promise<OrganizationResponse> {
|
||||||
|
@ -58,7 +68,13 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
|
||||||
|
|
||||||
this.setPlanInformation(request, subscription.plan);
|
this.setPlanInformation(request, subscription.plan);
|
||||||
|
|
||||||
return await this.organizationApiService.create(request);
|
const response = await this.organizationApiService.create(request);
|
||||||
|
|
||||||
|
await this.apiService.refreshIdentityToken();
|
||||||
|
|
||||||
|
await this.syncService.fullSync(true);
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async makeOrganizationKeys(): Promise<OrganizationKeys> {
|
private async makeOrganizationKeys(): Promise<OrganizationKeys> {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user