From 6f9c6d07aff1e801481db1038bd60dff8701b8fe Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 20 Nov 2023 10:10:47 -0500 Subject: [PATCH] [PM-4395] Block reseller org invites if they outnumber available seats (#6698) * Add Toast when reseller org invites over seat limit * Set validation error when reseller org invited members outnumber seats * Thomas' feedback --- .../member-dialog/member-dialog.component.ts | 10 ++++++++++ .../organizations/members/people.component.ts | 10 ++++++++++ apps/web/src/locales/en/messages.json | 9 +++++++++ .../src/admin-console/models/domain/organization.ts | 4 ++++ 4 files changed, 33 insertions(+) diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index 9d0dc6799d..caeb6b464f 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -54,6 +54,7 @@ export interface MemberDialogParams { allOrganizationUserEmails: string[]; usesKeyConnector: boolean; initialTab?: MemberDialogTab; + numConfirmedMembers: number; } export enum MemberDialogResult { @@ -383,6 +384,15 @@ export class MemberDialogComponent implements OnInit, OnDestroy { }); return; } + if ( + this.organization.hasReseller && + this.params.numConfirmedMembers + emails.length > this.organization.seats + ) { + this.formGroup.controls.emails.setErrors({ + tooManyEmails: { message: this.i18nService.t("seatLimitReachedContactYourProvider") }, + }); + return; + } await this.userService.invite(emails, userView); } diff --git a/apps/web/src/app/admin-console/organizations/members/people.component.ts b/apps/web/src/app/admin-console/organizations/members/people.component.ts index 1ab5f250fe..33deb5ee73 100644 --- a/apps/web/src/app/admin-console/organizations/members/people.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/people.component.ts @@ -427,6 +427,15 @@ export class PeopleComponent } async edit(user: OrganizationUserView, initialTab: MemberDialogTab = MemberDialogTab.Role) { + if (!user && this.organization.hasReseller && this.organization.seats === this.confirmedCount) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("seatLimitReached"), + this.i18nService.t("contactYourProvider") + ); + return; + } + // Invite User: Add Flow // Click on user email: Edit Flow @@ -450,6 +459,7 @@ export class PeopleComponent allOrganizationUserEmails: this.allUsers?.map((user) => user.email) ?? [], usesKeyConnector: user?.usesKeyConnector, initialTab: initialTab, + numConfirmedMembers: this.confirmedCount, }, }); diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 7330e5053c..702f94e7e4 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7398,5 +7398,14 @@ }, "unexpectedErrorSend": { "message": "An unexpected error has occurred while loading this Send. Try again later." + }, + "seatLimitReached": { + "message": "Seat limit has been reached" + }, + "contactYourProvider": { + "message": "Contact your provider to purchase additional seats." + }, + "seatLimitReachedContactYourProvider": { + "message": "Seat limit has been reached. Contact your provider to purchase additional seats." } } diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index 068ce6d45a..c7d8ee4924 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -258,6 +258,10 @@ export class Organization { return this.providerId != null || this.providerName != null; } + get hasReseller() { + return this.hasProvider && this.providerType === ProviderType.Reseller; + } + get canAccessSecretsManager() { return this.useSecretsManager && this.accessSecretsManager; }