diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts index 1a28973af9a5..3ec8470fcd4e 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts @@ -4,9 +4,9 @@ import { Injectable } from "@angular/core"; import { BehaviorSubject, firstValueFrom, map } from "rxjs"; import { - OrganizationUserApiService, - OrganizationUserAcceptRequest, OrganizationUserAcceptInitRequest, + OrganizationUserAcceptRequest, + OrganizationUserApiService, } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; @@ -105,9 +105,6 @@ export class AcceptOrganizationInviteService { invite: OrganizationInvite, activeUserId: UserId, ): Promise { - const request = new OrganizationUserAcceptInitRequest(); - request.token = invite.token; - const [encryptedOrgKey, orgKey] = await this.keyService.makeOrgKey(activeUserId); const [orgPublicKey, encryptedOrgPrivateKey] = await this.keyService.makeKeyPair(orgKey); const collection = await this.encryptService.encryptString( @@ -115,14 +112,12 @@ export class AcceptOrganizationInviteService { orgKey, ); - request.key = encryptedOrgKey.encryptedString; - request.keys = new OrganizationKeysRequest( - orgPublicKey, - encryptedOrgPrivateKey.encryptedString, + return new OrganizationUserAcceptInitRequest( + invite.token, + encryptedOrgKey.encryptedString, + new OrganizationKeysRequest(orgPublicKey, encryptedOrgPrivateKey.encryptedString), + collection.encryptedString, ); - request.collectionName = collection.encryptedString; - - return request; } private async accept(invite: OrganizationInvite): Promise { diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 536be3038ea2..89ab300879e7 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -1016,16 +1016,17 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { orgKey: SymmetricCryptoKey; activeUserId: UserId; }): Promise { - const request = new OrganizationCreateRequest(); - request.key = encryptionData.key; - request.collectionName = encryptionData.collectionCt; + const request = new OrganizationCreateRequest( + encryptionData.key, + new OrganizationKeysRequest( + encryptionData.orgKeys[0], + encryptionData.orgKeys[1].encryptedString as string, + ), + encryptionData.collectionCt, + ); request.name = this.formGroup.controls.name.value ?? ""; request.billingEmail = this.formGroup.controls.billingEmail.value ?? ""; request.initiationPath = "New organization creation in-product"; - request.keys = new OrganizationKeysRequest( - encryptionData.orgKeys[0], - encryptionData.orgKeys[1].encryptedString as string, - ); if (this.selectedPlan()!.type === PlanType.Free) { request.planType = PlanType.Free; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts index e1eea78d26a0..aef75bf65361 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts @@ -81,15 +81,15 @@ export class WebProviderService { providerKey, ); - const request = new CreateProviderOrganizationRequest(); - 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; + const request = new CreateProviderOrganizationRequest( + name, + ownerEmail, + planType, + seats, + encryptedProviderKey.encryptedString, + new OrganizationKeysRequest(publicKey, encryptedPrivateKey.encryptedString), + encryptedCollectionName.encryptedString, + ); await this.providerApiService.createProviderOrganization(providerId, request); diff --git a/libs/admin-console/src/common/organization-user/models/requests/organization-user-accept-init.request.spec.ts b/libs/admin-console/src/common/organization-user/models/requests/organization-user-accept-init.request.spec.ts new file mode 100644 index 000000000000..51ce5b7fa110 --- /dev/null +++ b/libs/admin-console/src/common/organization-user/models/requests/organization-user-accept-init.request.spec.ts @@ -0,0 +1,60 @@ +import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; + +import { OrganizationUserAcceptInitRequest } from "./organization-user-accept-init.request"; + +describe("OrganizationUserAcceptInitRequest", () => { + const validToken = "invite-token"; + const validKey = "encrypted-org-key"; + const validKeys = new OrganizationKeysRequest("public-key", "encrypted-private-key"); + const validCollectionName = "encrypted-collection-name"; + + it("should create a request with all required parameters", () => { + const request = new OrganizationUserAcceptInitRequest( + validToken, + validKey, + validKeys, + validCollectionName, + ); + + expect(request.token).toBe(validToken); + expect(request.key).toBe(validKey); + expect(request.keys).toBe(validKeys); + expect(request.collectionName).toBe(validCollectionName); + }); + + it("should throw when token is empty", () => { + expect( + () => new OrganizationUserAcceptInitRequest("", validKey, validKeys, validCollectionName), + ).toThrow("Token is required"); + }); + + it("should throw when key is empty", () => { + expect( + () => new OrganizationUserAcceptInitRequest(validToken, "", validKeys, validCollectionName), + ).toThrow("Organization key is required"); + }); + + it("should throw when keys is null", () => { + expect( + () => new OrganizationUserAcceptInitRequest(validToken, validKey, null!, validCollectionName), + ).toThrow("Organization keys are required"); + }); + + it("should throw when keys is undefined", () => { + expect( + () => + new OrganizationUserAcceptInitRequest( + validToken, + validKey, + undefined!, + validCollectionName, + ), + ).toThrow("Organization keys are required"); + }); + + it("should throw when collectionName is empty", () => { + expect( + () => new OrganizationUserAcceptInitRequest(validToken, validKey, validKeys, ""), + ).toThrow("Collection name is required"); + }); +}); diff --git a/libs/admin-console/src/common/organization-user/models/requests/organization-user-accept-init.request.ts b/libs/admin-console/src/common/organization-user/models/requests/organization-user-accept-init.request.ts index 9aec866c603a..5eb1d60376dd 100644 --- a/libs/admin-console/src/common/organization-user/models/requests/organization-user-accept-init.request.ts +++ b/libs/admin-console/src/common/organization-user/models/requests/organization-user-accept-init.request.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; export class OrganizationUserAcceptInitRequest { @@ -7,4 +5,23 @@ export class OrganizationUserAcceptInitRequest { key: string; keys: OrganizationKeysRequest; collectionName: string; + + constructor(token: string, key: string, keys: OrganizationKeysRequest, collectionName: string) { + if (!token) { + throw new Error("Token is required"); + } + if (!key) { + throw new Error("Organization key is required"); + } + if (!keys) { + throw new Error("Organization keys are required"); + } + if (!collectionName) { + throw new Error("Collection name is required"); + } + this.token = token; + this.key = key; + this.keys = keys; + this.collectionName = collectionName; + } } diff --git a/libs/common/src/admin-console/models/request/create-provider-organization.request.spec.ts b/libs/common/src/admin-console/models/request/create-provider-organization.request.spec.ts new file mode 100644 index 000000000000..d299689af7ce --- /dev/null +++ b/libs/common/src/admin-console/models/request/create-provider-organization.request.spec.ts @@ -0,0 +1,82 @@ +import { PlanType } from "../../../billing/enums"; + +import { CreateProviderOrganizationRequest } from "./create-provider-organization.request"; +import { OrganizationKeysRequest } from "./organization-keys.request"; + +describe("CreateProviderOrganizationRequest", () => { + const validName = "Test Org"; + const validOwnerEmail = "owner@example.com"; + const validPlanType = PlanType.TeamsAnnually; + const validSeats = 10; + const validKey = "encrypted-org-key"; + const validKeyPair = new OrganizationKeysRequest("public-key", "encrypted-private-key"); + const validCollectionName = "encrypted-collection-name"; + + const createRequest = ( + overrides: Partial<{ + name: string; + ownerEmail: string; + planType: PlanType; + seats: number; + key: string; + keyPair: OrganizationKeysRequest; + collectionName: string; + }> = {}, + ) => { + return new CreateProviderOrganizationRequest( + "name" in overrides ? overrides.name! : validName, + "ownerEmail" in overrides ? overrides.ownerEmail! : validOwnerEmail, + "planType" in overrides ? overrides.planType! : validPlanType, + "seats" in overrides ? overrides.seats! : validSeats, + "key" in overrides ? overrides.key! : validKey, + "keyPair" in overrides ? overrides.keyPair! : validKeyPair, + "collectionName" in overrides ? overrides.collectionName! : validCollectionName, + ); + }; + + it("should create a request with all required parameters", () => { + const request = createRequest(); + + expect(request.name).toBe(validName); + expect(request.ownerEmail).toBe(validOwnerEmail); + expect(request.planType).toBe(validPlanType); + expect(request.seats).toBe(validSeats); + expect(request.key).toBe(validKey); + expect(request.keyPair).toBe(validKeyPair); + expect(request.collectionName).toBe(validCollectionName); + }); + + it("should throw when name is empty", () => { + expect(() => createRequest({ name: "" })).toThrow("Name is required"); + }); + + it("should throw when ownerEmail is empty", () => { + expect(() => createRequest({ ownerEmail: "" })).toThrow("Owner email is required"); + }); + + it("should throw when planType is null", () => { + expect(() => createRequest({ planType: null! })).toThrow("Plan type is required"); + }); + + it("should throw when seats is null", () => { + expect(() => createRequest({ seats: null! })).toThrow("Seats is required"); + }); + + it("should throw when key is empty", () => { + expect(() => createRequest({ key: "" })).toThrow("Organization key is required"); + }); + + it("should throw when keyPair is null", () => { + expect(() => createRequest({ keyPair: null! })).toThrow("Organization key pair is required"); + }); + + it("should throw when keyPair is undefined", () => { + expect(() => createRequest({ keyPair: undefined })).toThrow( + "Organization key pair is required", + ); + }); + + it("should throw when collectionName is empty", () => { + expect(() => createRequest({ collectionName: "" })).toThrow("Collection name is required"); + }); +}); diff --git a/libs/common/src/admin-console/models/request/create-provider-organization.request.ts b/libs/common/src/admin-console/models/request/create-provider-organization.request.ts index ccb437b922d8..9187a08a83dc 100644 --- a/libs/common/src/admin-console/models/request/create-provider-organization.request.ts +++ b/libs/common/src/admin-console/models/request/create-provider-organization.request.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { PlanType } from "../../../billing/enums"; import { OrganizationKeysRequest } from "./organization-keys.request"; @@ -12,4 +10,43 @@ export class CreateProviderOrganizationRequest { key: string; keyPair: OrganizationKeysRequest; collectionName: string; + + constructor( + name: string, + ownerEmail: string, + planType: PlanType, + seats: number, + key: string, + keyPair: OrganizationKeysRequest, + collectionName: string, + ) { + if (!name) { + throw new Error("Name is required"); + } + if (!ownerEmail) { + throw new Error("Owner email is required"); + } + if (planType == null) { + throw new Error("Plan type is required"); + } + if (seats == null) { + throw new Error("Seats is required"); + } + if (!key) { + throw new Error("Organization key is required"); + } + if (!keyPair) { + throw new Error("Organization key pair is required"); + } + if (!collectionName) { + throw new Error("Collection name is required"); + } + this.name = name; + this.ownerEmail = ownerEmail; + this.planType = planType; + this.seats = seats; + this.key = key; + this.keyPair = keyPair; + this.collectionName = collectionName; + } } diff --git a/libs/common/src/admin-console/models/request/organization-create.request.spec.ts b/libs/common/src/admin-console/models/request/organization-create.request.spec.ts new file mode 100644 index 000000000000..8bdcf7cde131 --- /dev/null +++ b/libs/common/src/admin-console/models/request/organization-create.request.spec.ts @@ -0,0 +1,37 @@ +import { OrganizationCreateRequest } from "./organization-create.request"; +import { OrganizationKeysRequest } from "./organization-keys.request"; + +describe("OrganizationCreateRequest", () => { + const validKey = "encrypted-org-key"; + const validKeys = new OrganizationKeysRequest("public-key", "encrypted-private-key"); + const validCollectionName = "encrypted-collection-name"; + + it("should create a request with valid key parameters", () => { + const request = new OrganizationCreateRequest(validKey, validKeys, validCollectionName); + + expect(request.key).toBe(validKey); + expect(request.keys).toBe(validKeys); + expect(request.collectionName).toBe(validCollectionName); + }); + + it("should inherit validation from parent class", () => { + expect(() => new OrganizationCreateRequest("", validKeys, validCollectionName)).toThrow( + "Organization key is required", + ); + + expect(() => new OrganizationCreateRequest(validKey, null!, validCollectionName)).toThrow( + "Organization keys are required", + ); + + expect(() => new OrganizationCreateRequest(validKey, validKeys, "")).toThrow( + "Collection name is required", + ); + }); + + it("should allow setting payment fields after construction", () => { + const request = new OrganizationCreateRequest(validKey, validKeys, validCollectionName); + request.paymentToken = "token"; + + expect(request.paymentToken).toBe("token"); + }); +}); diff --git a/libs/common/src/admin-console/models/request/organization-create.request.ts b/libs/common/src/admin-console/models/request/organization-create.request.ts index 5d8614a55c49..2873f42e01f5 100644 --- a/libs/common/src/admin-console/models/request/organization-create.request.ts +++ b/libs/common/src/admin-console/models/request/organization-create.request.ts @@ -1,11 +1,9 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { PaymentMethodType } from "../../../billing/enums"; import { OrganizationNoPaymentMethodCreateRequest } from "../../../billing/models/request/organization-no-payment-method-create-request"; export class OrganizationCreateRequest extends OrganizationNoPaymentMethodCreateRequest { - paymentMethodType: PaymentMethodType; - paymentToken: string; + paymentMethodType!: PaymentMethodType; + paymentToken: string = ""; skipTrial?: boolean; coupons?: string[]; } diff --git a/libs/common/src/billing/models/request/organization-no-payment-method-create-request.spec.ts b/libs/common/src/billing/models/request/organization-no-payment-method-create-request.spec.ts new file mode 100644 index 000000000000..65d84a04eb84 --- /dev/null +++ b/libs/common/src/billing/models/request/organization-no-payment-method-create-request.spec.ts @@ -0,0 +1,45 @@ +import { OrganizationKeysRequest } from "../../../admin-console/models/request/organization-keys.request"; + +import { OrganizationNoPaymentMethodCreateRequest } from "./organization-no-payment-method-create-request"; + +describe("OrganizationNoPaymentMethodCreateRequest", () => { + const validKey = "encrypted-org-key"; + const validKeys = new OrganizationKeysRequest("public-key", "encrypted-private-key"); + const validCollectionName = "encrypted-collection-name"; + + it("should create a request with valid key parameters", () => { + const request = new OrganizationNoPaymentMethodCreateRequest( + validKey, + validKeys, + validCollectionName, + ); + + expect(request.key).toBe(validKey); + expect(request.keys).toBe(validKeys); + expect(request.collectionName).toBe(validCollectionName); + }); + + it("should throw when key is empty", () => { + expect( + () => new OrganizationNoPaymentMethodCreateRequest("", validKeys, validCollectionName), + ).toThrow("Organization key is required"); + }); + + it("should throw when keys is null", () => { + expect( + () => new OrganizationNoPaymentMethodCreateRequest(validKey, null!, validCollectionName), + ).toThrow("Organization keys are required"); + }); + + it("should throw when keys is undefined", () => { + expect( + () => new OrganizationNoPaymentMethodCreateRequest(validKey, undefined!, validCollectionName), + ).toThrow("Organization keys are required"); + }); + + it("should throw when collectionName is empty", () => { + expect(() => new OrganizationNoPaymentMethodCreateRequest(validKey, validKeys, "")).toThrow( + "Collection name is required", + ); + }); +}); diff --git a/libs/common/src/billing/models/request/organization-no-payment-method-create-request.ts b/libs/common/src/billing/models/request/organization-no-payment-method-create-request.ts index cfa9981d0dba..0447472ff434 100644 --- a/libs/common/src/billing/models/request/organization-no-payment-method-create-request.ts +++ b/libs/common/src/billing/models/request/organization-no-payment-method-create-request.ts @@ -1,31 +1,44 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { OrganizationKeysRequest } from "../../../admin-console/models/request/organization-keys.request"; import { InitiationPath } from "../../../models/request/reference-event.request"; import { PlanType } from "../../enums"; export class OrganizationNoPaymentMethodCreateRequest { - name: string; - businessName: string; - billingEmail: string; - planType: PlanType; + name: string = ""; + businessName: string = ""; + billingEmail: string = ""; + planType!: PlanType; key: string; keys: OrganizationKeysRequest; - additionalSeats: number; - maxAutoscaleSeats: number; - additionalStorageGb: number; - premiumAccessAddon: boolean; + additionalSeats: number = 0; + maxAutoscaleSeats: number = 0; + additionalStorageGb: number = 0; + premiumAccessAddon: boolean = false; collectionName: string; - taxIdNumber: string; - billingAddressLine1: string; - billingAddressLine2: string; - billingAddressCity: string; - billingAddressState: string; - billingAddressPostalCode: string; - billingAddressCountry: string; - useSecretsManager: boolean; - additionalSmSeats: number; - additionalServiceAccounts: number; - isFromSecretsManagerTrial: boolean; - initiationPath: InitiationPath; + taxIdNumber: string = ""; + billingAddressLine1: string = ""; + billingAddressLine2: string = ""; + billingAddressCity: string = ""; + billingAddressState: string = ""; + billingAddressPostalCode: string = ""; + billingAddressCountry: string = ""; + useSecretsManager: boolean = false; + additionalSmSeats: number = 0; + additionalServiceAccounts: number = 0; + isFromSecretsManagerTrial: boolean = false; + initiationPath!: InitiationPath; + + constructor(key: string, keys: OrganizationKeysRequest, collectionName: string) { + if (!key) { + throw new Error("Organization key is required"); + } + if (!keys) { + throw new Error("Organization keys are required"); + } + if (!collectionName) { + throw new Error("Collection name is required"); + } + this.key = key; + this.keys = keys; + this.collectionName = collectionName; + } } diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index 34afb4512389..fb5f92487226 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -48,11 +48,10 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs subscription: SubscriptionInformation, activeUserId: UserId, ): Promise { - const request = new OrganizationCreateRequest(); - const organizationKeys = await this.makeOrganizationKeys(activeUserId); + const { key, keysRequest, collectionName } = this.makeOrganizationKeysRequest(organizationKeys); - this.setOrganizationKeys(request, organizationKeys); + const request = new OrganizationCreateRequest(key, keysRequest, collectionName); this.setOrganizationInformation(request, subscription.organization); @@ -77,11 +76,10 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs subscription: SubscriptionInformation, activeUserId: UserId, ): Promise { - const request = new OrganizationNoPaymentMethodCreateRequest(); - const organizationKeys = await this.makeOrganizationKeys(activeUserId); + const { key, keysRequest, collectionName } = this.makeOrganizationKeysRequest(organizationKeys); - this.setOrganizationKeys(request, organizationKeys); + const request = new OrganizationNoPaymentMethodCreateRequest(key, keysRequest, collectionName); this.setOrganizationInformation(request, subscription.organization); @@ -100,11 +98,10 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs subscription: SubscriptionInformation, activeUserId: UserId, ): Promise { - const request = new OrganizationCreateRequest(); - const organizationKeys = await this.makeOrganizationKeys(activeUserId); + const { key, keysRequest, collectionName } = this.makeOrganizationKeysRequest(organizationKeys); - this.setOrganizationKeys(request, organizationKeys); + const request = new OrganizationCreateRequest(key, keysRequest, collectionName); this.setOrganizationInformation(request, subscription.organization); @@ -158,16 +155,19 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs request.initiationPath = information.initiationPath; } - private setOrganizationKeys( - request: OrganizationCreateRequest | OrganizationNoPaymentMethodCreateRequest, - keys: OrganizationKeys, - ): void { - request.key = keys.encryptedKey.encryptedString; - request.keys = new OrganizationKeysRequest( - keys.publicKey, - keys.encryptedPrivateKey.encryptedString, - ); - request.collectionName = keys.encryptedCollectionName.encryptedString; + private makeOrganizationKeysRequest(keys: OrganizationKeys): { + key: string; + keysRequest: OrganizationKeysRequest; + collectionName: string; + } { + return { + key: keys.encryptedKey.encryptedString, + keysRequest: new OrganizationKeysRequest( + keys.publicKey, + keys.encryptedPrivateKey.encryptedString, + ), + collectionName: keys.encryptedCollectionName.encryptedString, + }; } private setPaymentInformation( @@ -223,9 +223,10 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs subscription: SubscriptionInformation, activeUserId: UserId, ): Promise { - const request = new OrganizationCreateRequest(); const organizationKeys = await this.makeOrganizationKeys(activeUserId); - this.setOrganizationKeys(request, organizationKeys); + const { key, keysRequest, collectionName } = this.makeOrganizationKeysRequest(organizationKeys); + + const request = new OrganizationCreateRequest(key, keysRequest, collectionName); this.setOrganizationInformation(request, subscription.organization); this.setPlanInformation(request, subscription.plan); this.setPaymentInformation(request, subscription.payment);