From e3f31ac741049b27ae8401d31f1b9589b82f5cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Fri, 14 Apr 2023 11:14:18 +0100 Subject: [PATCH] [AC-1081] Merge feature/billing-obfuscation (#5172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [AC-431] Add new organization invite process (#4841) * [AC-431] Added properties 'key' and 'keys' to OrganizationUserAcceptRequest * [AC-431] On organization accept added check for 'initOrganization' flag and send encrypt keys if true * [AC-431] Reverted changes on AcceptOrganizationComponent and OrganizationUserAcceptRequest * [AC-431] Created OrganizationUserAcceptInitRequest * [AC-431] Added method postOrganizationUserAcceptInit to OrganizationUserService * [AC-431] Created AcceptInitOrganizationComponent and added routing config. Added 'inviteInitAcceptedDesc' to messages * [AC-431] Remove blank line * [AC-431] Remove requirement for logging in again * [AC-431] Removed accept-init-organization.component.html * Update libs/common/src/abstractions/organization-user/organization-user.service.ts Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * [AC-431] Sending collection name when initializing an org * [AC-431] Deleted component accept-init-organization and incorporated logic into accept-organization * Update libs/common/src/abstractions/organization-user/organization-user.service.ts Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * [AC-431] Returning promise chains * [AC-431] Moved ReAuth check to org accept only * [AC-431] Fixed import issues --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * [AC-434] Hide billing screen for reseller clients (#4955) * [AC-434] Retrieving ProviderType for each Org * [AC-434] Hide subscription details if user cannot manage billing * [AC-434] Renamed providerType to provider-type * [AC-434] Reverted change that showed Billing History and Payment Methods tabs * [AC-434] Hiding Secrets Manager enroll * [AC-434] Renamed Billing access variables to be more readable * Apply suggestions from code review Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * [AC-434] Reduce duplication in permission code * [AC-434] npm prettier * [AC-434] Changed selfhost subscription permission * [AC-434] Added canEditSubscription check for change plan buttons * [AC-434] Removed message displaying provider name in subscription * [AC-434] canEditSubscription logic depends on canViewSubscription * [AC-434] Hiding next charge value for users without billing edit permission * [AC-434] Changed canViewSubscription and canEditSubscription to be clearer * [AC-434] Altered BillingSubscriptionItemResponse.amount and BillingSubscriptionUpcomingInvoiceResponse.amount to nullable * [AC-434] Reverted change on BillingSubscriptionItemResponse.amount --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * Updated IsPaidOrgGuard reference from org.CanManageBilling to canEditSubscription --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> --- .../organizations/guards/is-paid-org.guard.ts | 2 +- .../manage/collections.component.ts | 6 +- .../organizations/members/people.component.ts | 6 +- .../settings/account.component.html | 4 +- .../settings/account.component.ts | 6 +- .../organization-billing-routing.module.ts | 4 +- .../organization-billing-tab.component.ts | 7 +- ...nization-subscription-cloud.component.html | 214 +++++++++--------- ...ganization-subscription-cloud.component.ts | 2 +- ...ation-subscription-selfhost.component.html | 2 +- ...ization-subscription-selfhost.component.ts | 2 +- .../vault-header/vault-header.component.ts | 6 +- .../src/auth/accept-organization.component.ts | 88 +++++-- apps/web/src/locales/en/messages.json | 3 + .../organization-user.service.ts | 15 ++ .../organization-user/requests/index.ts | 1 + .../organization-user-accept-init.request.ts | 8 + .../models/data/organization.data.ts | 4 +- .../models/domain/organization.ts | 26 ++- .../response/profile-organization.response.ts | 4 +- .../models/response/subscription.response.ts | 2 +- libs/common/src/enums/index.ts | 1 + libs/common/src/enums/provider-type.enum.ts | 4 + ...rganization-user.service.implementation.ts | 15 ++ 24 files changed, 280 insertions(+), 152 deletions(-) create mode 100644 libs/common/src/abstractions/organization-user/requests/organization-user-accept-init.request.ts create mode 100644 libs/common/src/enums/provider-type.enum.ts diff --git a/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.ts b/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.ts index a74e152f88..79ad01eea9 100644 --- a/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.ts +++ b/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.ts @@ -27,7 +27,7 @@ export class IsPaidOrgGuard implements CanActivate { if (org.isFreeOrg) { // Users without billing permission can't access billing - if (!org.canManageBilling) { + if (!org.canEditSubscription) { await this.platformUtilsService.showDialog( this.i18nService.t("notAvailableForFreeOrganization"), this.i18nService.t("upgradeOrganization"), diff --git a/apps/web/src/app/admin-console/organizations/manage/collections.component.ts b/apps/web/src/app/admin-console/organizations/manage/collections.component.ts index 5c18c1e316..8026ee4a37 100644 --- a/apps/web/src/app/admin-console/organizations/manage/collections.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/collections.component.ts @@ -151,7 +151,7 @@ export class CollectionsComponent implements OnInit { const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = { title: this.i18nService.t("upgradeOrganization"), content: this.i18nService.t( - this.organization.canManageBilling + this.organization.canEditSubscription ? "freeOrgMaxCollectionReachedManageBilling" : "freeOrgMaxCollectionReachedNoManageBilling", this.organization.maxCollections @@ -159,7 +159,7 @@ export class CollectionsComponent implements OnInit { type: SimpleDialogType.PRIMARY, }; - if (this.organization.canManageBilling) { + if (this.organization.canEditSubscription) { orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade"); } else { orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok"); @@ -173,7 +173,7 @@ export class CollectionsComponent implements OnInit { return; } - if (result == SimpleDialogCloseType.ACCEPT && this.organization.canManageBilling) { + if (result == SimpleDialogCloseType.ACCEPT && this.organization.canEditSubscription) { this.router.navigate( ["/organizations", this.organization.id, "billing", "subscription"], { queryParams: { upgrade: true } } 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 1e1ea6f94d..cc5cc4db3a 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 @@ -347,7 +347,7 @@ export class PeopleComponent const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = { title: this.i18nService.t("upgradeOrganization"), content: this.i18nService.t( - this.organization.canManageBilling + this.organization.canEditSubscription ? "freeOrgInvLimitReachedManageBilling" : "freeOrgInvLimitReachedNoManageBilling", this.organization.seats @@ -355,7 +355,7 @@ export class PeopleComponent type: SimpleDialogType.PRIMARY, }; - if (this.organization.canManageBilling) { + if (this.organization.canEditSubscription) { orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade"); } else { orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok"); @@ -369,7 +369,7 @@ export class PeopleComponent return; } - if (result == SimpleDialogCloseType.ACCEPT && this.organization.canManageBilling) { + if (result == SimpleDialogCloseType.ACCEPT && this.organization.canEditSubscription) { this.router.navigate(["/organizations", this.organization.id, "billing", "subscription"], { queryParams: { upgrade: true }, }); diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.html b/apps/web/src/app/admin-console/organizations/settings/account.component.html index 1d11412bfb..096ae89015 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.html +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.html @@ -37,7 +37,7 @@ type="text" name="BillingEmail" [(ngModel)]="org.billingEmail" - [disabled]="selfHosted || !canManageBilling" + [disabled]="selfHosted || !canEditSubscription" />
@@ -48,7 +48,7 @@ type="text" name="BusinessName" [(ngModel)]="org.businessName" - [disabled]="selfHosted || !canManageBilling" + [disabled]="selfHosted || !canEditSubscription" />
diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index 75dae9015a..0072e1f3ae 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -33,7 +33,7 @@ export class AccountComponent { rotateApiKeyModalRef: ViewContainerRef; selfHosted = false; - canManageBilling = true; + canEditSubscription = true; loading = true; canUseApi = false; org: OrganizationResponse; @@ -60,7 +60,9 @@ export class AccountComponent { // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { this.organizationId = params.organizationId; - this.canManageBilling = this.organizationService.get(this.organizationId).canManageBilling; + this.canEditSubscription = this.organizationService.get( + this.organizationId + ).canEditSubscription; try { this.org = await this.organizationApiService.get(this.organizationId); this.canUseApi = this.org.useApi; diff --git a/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts b/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts index 5ba8ab4415..eae0fdfdec 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts @@ -34,7 +34,7 @@ const routes: Routes = [ canActivate: [OrganizationPermissionsGuard], data: { titleId: "paymentMethod", - organizationPermissions: (org: Organization) => org.canManageBilling, + organizationPermissions: (org: Organization) => org.canEditPaymentMethods, }, }, { @@ -43,7 +43,7 @@ const routes: Routes = [ canActivate: [OrganizationPermissionsGuard], data: { titleId: "billingHistory", - organizationPermissions: (org: Organization) => org.canManageBilling, + organizationPermissions: (org: Organization) => org.canViewBillingHistory, }, }, ], diff --git a/apps/web/src/app/billing/organizations/organization-billing-tab.component.ts b/apps/web/src/app/billing/organizations/organization-billing-tab.component.ts index 8acf90254e..11609dc5e7 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-tab.component.ts +++ b/apps/web/src/app/billing/organizations/organization-billing-tab.component.ts @@ -21,7 +21,12 @@ export class OrganizationBillingTabComponent implements OnInit { ngOnInit() { this.showPaymentAndHistory$ = this.route.params.pipe( switchMap((params) => this.organizationService.get$(params.organizationId)), - map((org) => !this.platformUtilsService.isSelfHost() && org.canManageBilling) + map( + (org) => + !this.platformUtilsService.isSelfHost() && + org.canViewBillingHistory && + org.canEditPaymentMethods + ) ); } } diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index c92b09c7e3..c28bdd4ddc 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -17,7 +17,7 @@ @@ -64,30 +64,24 @@ -
- {{ "details" | i18n }} - - - - - - - -
- {{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @ - {{ i.amount | currency : "$" }} - {{ i.quantity * i.amount | currency : "$" }} /{{ i.interval | i18n }}
-
- -
-
-
{{ "provider" | i18n }}
-
{{ "yourProviderIs" | i18n : userOrg.providerName }}
-
+ +
+ {{ "details" | i18n }} + + + + + + + +
+ {{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @ + {{ i.amount | currency : "$" }} + {{ i.quantity * i.amount | currency : "$" }} /{{ i.interval | i18n }}
- + -

{{ "storage" | i18n }}

-

{{ "subscriptionStorage" | i18n : sub.maxStorageGb || 0 : sub.storageName || "0 MB" }}

-
-
+

{{ "manageSubscription" | i18n }}

+

{{ subscriptionDesc }}

+ - {{ storagePercentage / 100 | percent }} -
-
- -
-
- - + +
+ + +

{{ "storage" | i18n }}

+

{{ "subscriptionStorage" | i18n : sub.maxStorageGb || 0 : sub.storageName || "0 MB" }}

+
+
+ {{ storagePercentage / 100 | percent }}
-
+ +
+
+ + +
+ +
+

{{ "selfHostingTitle" | i18n }}

@@ -214,20 +212,22 @@ (onCanceled)="closeDownloadLicense()" >
-

{{ "additionalOptions" | i18n }}

-

- {{ "additionalOptionsDesc" | i18n }} -

-
- -
+ +

{{ "additionalOptions" | i18n }}

+

+ {{ "additionalOptionsDesc" | i18n }} +

+
+ +
+
diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 9cc8d4cfc4..3ee9d99b2a 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -77,7 +77,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy } this.loading = true; this.userOrg = this.organizationService.get(this.organizationId); - if (this.userOrg.canManageBilling) { + if (this.userOrg.canViewSubscription) { this.sub = await this.organizationApiService.getSubscription(this.organizationId); } diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html index a7ba79a79d..08fb12e76e 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html @@ -18,7 +18,7 @@
diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts index 1956e7361f..a6afb20aad 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts @@ -101,7 +101,7 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest } this.loading = true; this.userOrg = this.organizationService.get(this.organizationId); - if (this.userOrg.canManageBilling) { + if (this.userOrg.canViewSubscription) { this.sub = await this.organizationApiService.getSubscription(this.organizationId); } diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index 95349c85d1..385b198638 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -111,7 +111,7 @@ export class VaultHeaderComponent { const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = { title: this.i18nService.t("upgradeOrganization"), content: this.i18nService.t( - this.organization.canManageBilling + this.organization.canEditSubscription ? "freeOrgMaxCollectionReachedManageBilling" : "freeOrgMaxCollectionReachedNoManageBilling", this.organization.maxCollections @@ -119,7 +119,7 @@ export class VaultHeaderComponent { type: SimpleDialogType.PRIMARY, }; - if (this.organization.canManageBilling) { + if (this.organization.canEditSubscription) { orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade"); } else { orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok"); @@ -133,7 +133,7 @@ export class VaultHeaderComponent { return; } - if (result == SimpleDialogCloseType.ACCEPT && this.organization.canManageBilling) { + if (result == SimpleDialogCloseType.ACCEPT && this.organization.canEditSubscription) { this.router.navigate(["/organizations", this.organization.id, "billing", "subscription"], { queryParams: { upgrade: true }, }); diff --git a/apps/web/src/auth/accept-organization.component.ts b/apps/web/src/auth/accept-organization.component.ts index 561c7567a0..64f1f8a22f 100644 --- a/apps/web/src/auth/accept-organization.component.ts +++ b/apps/web/src/auth/accept-organization.component.ts @@ -6,13 +6,17 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service"; -import { OrganizationUserAcceptRequest } from "@bitwarden/common/abstractions/organization-user/requests"; +import { + OrganizationUserAcceptInitRequest, + OrganizationUserAcceptRequest, +} from "@bitwarden/common/abstractions/organization-user/requests"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { Utils } from "@bitwarden/common/misc/utils"; import { BaseAcceptComponent } from "../app/common/base.accept.component"; @@ -44,32 +48,33 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent { } async authedHandler(qParams: Params): Promise { - const needsReAuth = (await this.stateService.getOrganizationInvitation()) != null; - if (!needsReAuth) { - // Accepting an org invite requires authentication from a logged out state - this.messagingService.send("logout", { redirect: false }); - await this.prepareOrganizationInvitation(qParams); - return; + const initOrganization = + qParams.initOrganization != null && qParams.initOrganization.toLocaleLowerCase() === "true"; + if (initOrganization) { + this.actionPromise = this.acceptInitOrganizationFlow(qParams); + } else { + const needsReAuth = (await this.stateService.getOrganizationInvitation()) == null; + if (needsReAuth) { + // Accepting an org invite requires authentication from a logged out state + this.messagingService.send("logout", { redirect: false }); + await this.prepareOrganizationInvitation(qParams); + return; + } + + // User has already logged in and passed the Master Password policy check + this.actionPromise = this.acceptFlow(qParams); } - // User has already logged in and passed the Master Password policy check - this.actionPromise = this.prepareAcceptRequest(qParams).then(async (request) => { - await this.organizationUserService.postOrganizationUserAccept( - qParams.organizationId, - qParams.organizationUserId, - request - ); - }); - - await this.stateService.setOrganizationInvitation(null); await this.actionPromise; + await this.stateService.setOrganizationInvitation(null); this.platformUtilService.showToast( "success", this.i18nService.t("inviteAccepted"), - this.i18nService.t("inviteAcceptedDesc"), + initOrganization + ? this.i18nService.t("inviteInitAcceptedDesc") + : this.i18nService.t("inviteAcceptedDesc"), { timeout: 10000 } ); - this.router.navigate(["/vault"]); } @@ -77,6 +82,51 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent { await this.prepareOrganizationInvitation(qParams); } + private async acceptInitOrganizationFlow(qParams: Params): Promise { + return this.prepareAcceptInitRequest(qParams).then((request) => + this.organizationUserService.postOrganizationUserAcceptInit( + qParams.organizationId, + qParams.organizationUserId, + request + ) + ); + } + + private async acceptFlow(qParams: Params): Promise { + return this.prepareAcceptRequest(qParams).then((request) => + this.organizationUserService.postOrganizationUserAccept( + qParams.organizationId, + qParams.organizationUserId, + request + ) + ); + } + + private async prepareAcceptInitRequest( + qParams: Params + ): Promise { + const request = new OrganizationUserAcceptInitRequest(); + request.token = qParams.token; + + const [encryptedOrgShareKey, orgShareKey] = await this.cryptoService.makeShareKey(); + const [orgPublicKey, encryptedOrgPrivateKey] = await this.cryptoService.makeKeyPair( + orgShareKey + ); + const collection = await this.cryptoService.encrypt( + this.i18nService.t("defaultCollection"), + orgShareKey + ); + + request.key = encryptedOrgShareKey.encryptedString; + request.keys = new OrganizationKeysRequest( + orgPublicKey, + encryptedOrgPrivateKey.encryptedString + ); + request.collectionName = collection.encryptedString; + + return request; + } + private async prepareAcceptRequest(qParams: Params): Promise { const request = new OrganizationUserAcceptRequest(); request.token = qParams.token; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index ade4303a36..dd9fbcbe79 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -3126,6 +3126,9 @@ "inviteAcceptedDesc": { "message": "You can access this organization once an administrator confirms your membership. We'll send you an email when that happens." }, + "inviteInitAcceptedDesc": { + "message": "You can now access this organization." + }, "inviteAcceptFailed": { "message": "Unable to accept invitation. Ask an organization admin to send a new invitation." }, diff --git a/libs/common/src/abstractions/organization-user/organization-user.service.ts b/libs/common/src/abstractions/organization-user/organization-user.service.ts index cfb7f0145c..ef65d34b11 100644 --- a/libs/common/src/abstractions/organization-user/organization-user.service.ts +++ b/libs/common/src/abstractions/organization-user/organization-user.service.ts @@ -1,6 +1,7 @@ import { ListResponse } from "../../models/response/list.response"; import { + OrganizationUserAcceptInitRequest, OrganizationUserAcceptRequest, OrganizationUserBulkConfirmRequest, OrganizationUserConfirmRequest, @@ -94,6 +95,20 @@ export abstract class OrganizationUserService { ids: string[] ): Promise>; + /** + * Accept an invitation to initialize and join an organization created via the Admin Portal **only**. + * This is only used once for the initial Owner, because it also creates the organization's encryption keys. + * This should not be used for organizations created via the Web client. + * @param organizationId - Identifier for the organization to accept + * @param id - Organization user identifier + * @param request - Request details for accepting the invitation + */ + abstract postOrganizationUserAcceptInit( + organizationId: string, + id: string, + request: OrganizationUserAcceptInitRequest + ): Promise; + /** * Accept an organization user invitation * @param organizationId - Identifier for the organization to accept diff --git a/libs/common/src/abstractions/organization-user/requests/index.ts b/libs/common/src/abstractions/organization-user/requests/index.ts index 8f45e585ee..6c9cec2b4b 100644 --- a/libs/common/src/abstractions/organization-user/requests/index.ts +++ b/libs/common/src/abstractions/organization-user/requests/index.ts @@ -1,3 +1,4 @@ +export * from "./organization-user-accept-init.request"; export * from "./organization-user-accept.request"; export * from "./organization-user-bulk-confirm.request"; export * from "./organization-user-confirm.request"; diff --git a/libs/common/src/abstractions/organization-user/requests/organization-user-accept-init.request.ts b/libs/common/src/abstractions/organization-user/requests/organization-user-accept-init.request.ts new file mode 100644 index 0000000000..d47cb711d2 --- /dev/null +++ b/libs/common/src/abstractions/organization-user/requests/organization-user-accept-init.request.ts @@ -0,0 +1,8 @@ +import { OrganizationKeysRequest } from "../../../admin-console/models/request/organization-keys.request"; + +export class OrganizationUserAcceptInitRequest { + token: string; + key: string; + keys: OrganizationKeysRequest; + collectionName: string; +} diff --git a/libs/common/src/admin-console/models/data/organization.data.ts b/libs/common/src/admin-console/models/data/organization.data.ts index 2886445fc5..cfaaff37c7 100644 --- a/libs/common/src/admin-console/models/data/organization.data.ts +++ b/libs/common/src/admin-console/models/data/organization.data.ts @@ -1,4 +1,4 @@ -import { ProductType } from "../../../enums"; +import { ProductType, ProviderType } from "../../../enums"; import { OrganizationUserStatusType, OrganizationUserType } from "../../enums"; import { PermissionsApi } from "../api/permissions.api"; import { ProfileOrganizationResponse } from "../response/profile-organization.response"; @@ -36,6 +36,7 @@ export class OrganizationData { hasPublicAndPrivateKeys: boolean; providerId: string; providerName: string; + providerType?: ProviderType; isProviderUser: boolean; familySponsorshipFriendlyName: string; familySponsorshipAvailable: boolean; @@ -80,6 +81,7 @@ export class OrganizationData { this.hasPublicAndPrivateKeys = response.hasPublicAndPrivateKeys; this.providerId = response.providerId; this.providerName = response.providerName; + this.providerType = response.providerType; this.familySponsorshipFriendlyName = response.familySponsorshipFriendlyName; this.familySponsorshipAvailable = response.familySponsorshipAvailable; this.planProductType = response.planProductType; diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index c9c95891a7..b29a9fa7c8 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -1,6 +1,6 @@ import { Jsonify } from "type-fest"; -import { ProductType } from "../../../enums"; +import { ProductType, ProviderType } from "../../../enums"; import { OrganizationUserStatusType, OrganizationUserType } from "../../enums"; import { PermissionsApi } from "../api/permissions.api"; import { OrganizationData } from "../data/organization.data"; @@ -38,6 +38,7 @@ export class Organization { hasPublicAndPrivateKeys: boolean; providerId: string; providerName: string; + providerType?: ProviderType; isProviderUser: boolean; familySponsorshipFriendlyName: string; familySponsorshipAvailable: boolean; @@ -86,6 +87,7 @@ export class Organization { this.hasPublicAndPrivateKeys = obj.hasPublicAndPrivateKeys; this.providerId = obj.providerId; this.providerName = obj.providerName; + this.providerType = obj.providerType; this.isProviderUser = obj.isProviderUser; this.familySponsorshipFriendlyName = obj.familySponsorshipFriendlyName; this.familySponsorshipAvailable = obj.familySponsorshipAvailable; @@ -197,8 +199,26 @@ export class Organization { return this.canManagePolicies; } - get canManageBilling() { - return this.isOwner && (this.isProviderUser || !this.hasProvider); + get canViewSubscription() { + if (this.canEditSubscription) { + return true; + } + + return this.hasProvider && this.providerType === ProviderType.Msp + ? this.isProviderUser + : this.isOwner; + } + + get canEditSubscription() { + return this.hasProvider ? this.isProviderUser : this.isOwner; + } + + get canEditPaymentMethods() { + return this.canEditSubscription; + } + + get canViewBillingHistory() { + return this.canEditSubscription; } get hasProvider() { diff --git a/libs/common/src/admin-console/models/response/profile-organization.response.ts b/libs/common/src/admin-console/models/response/profile-organization.response.ts index e94eef0814..18bf4d45e8 100644 --- a/libs/common/src/admin-console/models/response/profile-organization.response.ts +++ b/libs/common/src/admin-console/models/response/profile-organization.response.ts @@ -1,4 +1,4 @@ -import { ProductType } from "../../../enums"; +import { ProductType, ProviderType } from "../../../enums"; import { BaseResponse } from "../../../models/response/base.response"; import { OrganizationUserStatusType, OrganizationUserType } from "../../enums"; import { PermissionsApi } from "../api/permissions.api"; @@ -37,6 +37,7 @@ export class ProfileOrganizationResponse extends BaseResponse { userId: string; providerId: string; providerName: string; + providerType?: ProviderType; familySponsorshipFriendlyName: string; familySponsorshipAvailable: boolean; planProductType: ProductType; @@ -82,6 +83,7 @@ export class ProfileOrganizationResponse extends BaseResponse { this.userId = this.getResponseProperty("UserId"); this.providerId = this.getResponseProperty("ProviderId"); this.providerName = this.getResponseProperty("ProviderName"); + this.providerType = this.getResponseProperty("ProviderType"); this.familySponsorshipFriendlyName = this.getResponseProperty("FamilySponsorshipFriendlyName"); this.familySponsorshipAvailable = this.getResponseProperty("FamilySponsorshipAvailable"); this.planProductType = this.getResponseProperty("PlanProductType"); diff --git a/libs/common/src/billing/models/response/subscription.response.ts b/libs/common/src/billing/models/response/subscription.response.ts index 35f93cc265..8230d98417 100644 --- a/libs/common/src/billing/models/response/subscription.response.ts +++ b/libs/common/src/billing/models/response/subscription.response.ts @@ -75,7 +75,7 @@ export class BillingSubscriptionItemResponse extends BaseResponse { export class BillingSubscriptionUpcomingInvoiceResponse extends BaseResponse { date: string; - amount: number; + amount?: number; constructor(response: any) { super(response); diff --git a/libs/common/src/enums/index.ts b/libs/common/src/enums/index.ts index d51e50ca0f..87a688b856 100644 --- a/libs/common/src/enums/index.ts +++ b/libs/common/src/enums/index.ts @@ -16,6 +16,7 @@ export * from "./log-level-type.enum"; export * from "./native-messaging-version.enum"; export * from "./notification-type.enum"; export * from "./product-type.enum"; +export * from "./provider-type.enum"; export * from "./secure-note-type.enum"; export * from "./state-version.enum"; export * from "./storage-location.enum"; diff --git a/libs/common/src/enums/provider-type.enum.ts b/libs/common/src/enums/provider-type.enum.ts new file mode 100644 index 0000000000..5f81c338f0 --- /dev/null +++ b/libs/common/src/enums/provider-type.enum.ts @@ -0,0 +1,4 @@ +export enum ProviderType { + Msp = 0, + Reseller = 1, +} diff --git a/libs/common/src/services/organization-user/organization-user.service.implementation.ts b/libs/common/src/services/organization-user/organization-user.service.implementation.ts index 37b9da0be5..58cf4edd92 100644 --- a/libs/common/src/services/organization-user/organization-user.service.implementation.ts +++ b/libs/common/src/services/organization-user/organization-user.service.implementation.ts @@ -1,6 +1,7 @@ import { ApiService } from "../../abstractions/api.service"; import { OrganizationUserService } from "../../abstractions/organization-user/organization-user.service"; import { + OrganizationUserAcceptInitRequest, OrganizationUserAcceptRequest, OrganizationUserBulkConfirmRequest, OrganizationUserConfirmRequest, @@ -135,6 +136,20 @@ export class OrganizationUserServiceImplementation implements OrganizationUserSe return new ListResponse(r, OrganizationUserBulkResponse); } + postOrganizationUserAcceptInit( + organizationId: string, + id: string, + request: OrganizationUserAcceptInitRequest + ): Promise { + return this.apiService.send( + "POST", + "/organizations/" + organizationId + "/users/" + id + "/accept-init", + request, + true, + false + ); + } + postOrganizationUserAccept( organizationId: string, id: string,