1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-24 12:06:15 +01:00

[PM-146] Web: Upgrade flows for free 2 person orgs (#5564)

* Added a validator when adding users to a free org

* Updated based on PR feedback

Removed parameters passing in the org to member-dialog.
Removed i18n service from validator

* Moved i18n responsibility back to the validator

Also added jsdoc comments

* Updated validator to be an injectable class

* Added back in jsdocs

* Moved the validator initialization to ngOnInit

* Updated validator to take error message a a param
This commit is contained in:
cturnbull-bitwarden 2023-06-20 08:10:04 -04:00 committed by GitHub
parent 7dbc30ee05
commit d4f292108f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 160 additions and 1 deletions

View File

@ -35,6 +35,7 @@ import {
} from "../../../shared/components/access-selector"; } from "../../../shared/components/access-selector";
import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator"; import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator";
import { freeOrgSeatLimitReachedValidator } from "./validators/free-org-inv-limit-reached.validator";
export enum MemberDialogTab { export enum MemberDialogTab {
Role = 0, Role = 0,
@ -46,6 +47,7 @@ export interface MemberDialogParams {
name: string; name: string;
organizationId: string; organizationId: string;
organizationUserId: string; organizationUserId: string;
allOrganizationUserEmails: string[];
usesKeyConnector: boolean; usesKeyConnector: boolean;
initialTab?: MemberDialogTab; initialTab?: MemberDialogTab;
} }
@ -79,7 +81,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
protected groupAccessItems: AccessItemView[] = []; protected groupAccessItems: AccessItemView[] = [];
protected tabIndex: MemberDialogTab; protected tabIndex: MemberDialogTab;
protected formGroup = this.formBuilder.group({ protected formGroup = this.formBuilder.group({
emails: ["", [Validators.required, commaSeparatedEmails]], emails: ["", { updateOn: "blur" }],
type: OrganizationUserType.User, type: OrganizationUserType.User,
externalId: this.formBuilder.control({ value: "", disabled: true }), externalId: this.formBuilder.control({ value: "", disabled: true }),
accessAllCollections: false, accessAllCollections: false,
@ -167,6 +169,20 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
this.canUseCustomPermissions = organization.useCustomPermissions; this.canUseCustomPermissions = organization.useCustomPermissions;
this.canUseSecretsManager = organization.useSecretsManager && flagEnabled("secretsManager"); this.canUseSecretsManager = organization.useSecretsManager && flagEnabled("secretsManager");
const emailsControlValidators = [
Validators.required,
commaSeparatedEmails,
freeOrgSeatLimitReachedValidator(
this.organization,
this.params.allOrganizationUserEmails,
this.i18nService.t("subscriptionFreePlan", organization.seats)
),
];
const emailsControl = this.formGroup.get("emails");
emailsControl.setValidators(emailsControlValidators);
emailsControl.updateValueAndValidity();
this.collectionAccessItems = [].concat( this.collectionAccessItems = [].concat(
collections.map((c) => mapCollectionToAccessItemView(c)) collections.map((c) => mapCollectionToAccessItemView(c))
); );

View File

@ -0,0 +1,106 @@
import { AbstractControl, FormControl, ValidationErrors } from "@angular/forms";
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProductType } from "@bitwarden/common/enums";
import { freeOrgSeatLimitReachedValidator } from "./free-org-inv-limit-reached.validator";
const orgFactory = (props: Partial<Organization> = {}) =>
Object.assign(
new Organization(),
{
id: "myOrgId",
enabled: true,
type: OrganizationUserType.Admin,
},
props
);
describe("freeOrgSeatLimitReachedValidator", () => {
let organization: Organization;
let allOrganizationUserEmails: string[];
let validatorFn: (control: AbstractControl) => ValidationErrors | null;
beforeEach(() => {
allOrganizationUserEmails = ["user1@example.com"];
});
it("should return null when control value is empty", () => {
validatorFn = freeOrgSeatLimitReachedValidator(
organization,
allOrganizationUserEmails,
"You cannot invite more than 2 members without upgrading your plan."
);
const control = new FormControl("");
const result = validatorFn(control);
expect(result).toBeNull();
});
it("should return null when control value is null", () => {
validatorFn = freeOrgSeatLimitReachedValidator(
organization,
allOrganizationUserEmails,
"You cannot invite more than 2 members without upgrading your plan."
);
const control = new FormControl(null);
const result = validatorFn(control);
expect(result).toBeNull();
});
it("should return null when max seats are not exceeded on free plan", () => {
organization = orgFactory({
planProductType: ProductType.Free,
seats: 2,
});
validatorFn = freeOrgSeatLimitReachedValidator(
organization,
allOrganizationUserEmails,
"You cannot invite more than 2 members without upgrading your plan."
);
const control = new FormControl("user2@example.com");
const result = validatorFn(control);
expect(result).toBeNull();
});
it("should return validation error when max seats are exceeded on free plan", () => {
organization = orgFactory({
planProductType: ProductType.Free,
seats: 2,
});
const errorMessage = "You cannot invite more than 2 members without upgrading your plan.";
validatorFn = freeOrgSeatLimitReachedValidator(
organization,
allOrganizationUserEmails,
"You cannot invite more than 2 members without upgrading your plan."
);
const control = new FormControl("user2@example.com,user3@example.com");
const result = validatorFn(control);
expect(result).toStrictEqual({ freePlanLimitReached: { message: errorMessage } });
});
it("should return null when not on free plan", () => {
const control = new FormControl("user2@example.com,user3@example.com");
organization = orgFactory({
planProductType: ProductType.Enterprise,
seats: 100,
});
validatorFn = freeOrgSeatLimitReachedValidator(
organization,
allOrganizationUserEmails,
"You cannot invite more than 2 members without upgrading your plan."
);
const result = validatorFn(control);
expect(result).toBeNull();
});
});

View File

@ -0,0 +1,36 @@
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProductType } from "@bitwarden/common/enums";
/**
* Checks if the limit of free organization seats has been reached when adding new users
* @param organization An object representing the organization
* @param allOrganizationUserEmails An array of strings with existing user email addresses
* @param errorMessage A localized string to display if validation fails
* @returns A function that validates an `AbstractControl` and returns `ValidationErrors` or `null`
*/
export function freeOrgSeatLimitReachedValidator(
organization: Organization,
allOrganizationUserEmails: string[],
errorMessage: string
): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (control.value === "" || !control.value) {
return null;
}
const newEmailsToAdd = control.value
.split(",")
.filter(
(newEmailToAdd: string) =>
newEmailToAdd &&
!allOrganizationUserEmails.some((existingEmail) => existingEmail === newEmailToAdd)
);
return organization.planProductType === ProductType.Free &&
allOrganizationUserEmails.length + newEmailsToAdd.length > organization.seats
? { freePlanLimitReached: { message: errorMessage } }
: null;
};
}

View File

@ -396,6 +396,7 @@ export class PeopleComponent
name: this.userNamePipe.transform(user), name: this.userNamePipe.transform(user),
organizationId: this.organization.id, organizationId: this.organization.id,
organizationUserId: user != null ? user.id : null, organizationUserId: user != null ? user.id : null,
allOrganizationUserEmails: this.allUsers?.map((user) => user.email) ?? [],
usesKeyConnector: user?.usesKeyConnector, usesKeyConnector: user?.usesKeyConnector,
initialTab: initialTab, initialTab: initialTab,
}, },