1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-02 18:17:46 +01:00

[AC-1081] Merge feature/billing-obfuscation (#5172)

* [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>
This commit is contained in:
Rui Tomé 2023-04-14 11:14:18 +01:00 committed by GitHub
parent b3d4d9898e
commit e3f31ac741
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 280 additions and 152 deletions

View File

@ -27,7 +27,7 @@ export class IsPaidOrgGuard implements CanActivate {
if (org.isFreeOrg) { if (org.isFreeOrg) {
// Users without billing permission can't access billing // Users without billing permission can't access billing
if (!org.canManageBilling) { if (!org.canEditSubscription) {
await this.platformUtilsService.showDialog( await this.platformUtilsService.showDialog(
this.i18nService.t("notAvailableForFreeOrganization"), this.i18nService.t("notAvailableForFreeOrganization"),
this.i18nService.t("upgradeOrganization"), this.i18nService.t("upgradeOrganization"),

View File

@ -151,7 +151,7 @@ export class CollectionsComponent implements OnInit {
const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = { const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = {
title: this.i18nService.t("upgradeOrganization"), title: this.i18nService.t("upgradeOrganization"),
content: this.i18nService.t( content: this.i18nService.t(
this.organization.canManageBilling this.organization.canEditSubscription
? "freeOrgMaxCollectionReachedManageBilling" ? "freeOrgMaxCollectionReachedManageBilling"
: "freeOrgMaxCollectionReachedNoManageBilling", : "freeOrgMaxCollectionReachedNoManageBilling",
this.organization.maxCollections this.organization.maxCollections
@ -159,7 +159,7 @@ export class CollectionsComponent implements OnInit {
type: SimpleDialogType.PRIMARY, type: SimpleDialogType.PRIMARY,
}; };
if (this.organization.canManageBilling) { if (this.organization.canEditSubscription) {
orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade"); orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade");
} else { } else {
orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok"); orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok");
@ -173,7 +173,7 @@ export class CollectionsComponent implements OnInit {
return; return;
} }
if (result == SimpleDialogCloseType.ACCEPT && this.organization.canManageBilling) { if (result == SimpleDialogCloseType.ACCEPT && this.organization.canEditSubscription) {
this.router.navigate( this.router.navigate(
["/organizations", this.organization.id, "billing", "subscription"], ["/organizations", this.organization.id, "billing", "subscription"],
{ queryParams: { upgrade: true } } { queryParams: { upgrade: true } }

View File

@ -347,7 +347,7 @@ export class PeopleComponent
const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = { const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = {
title: this.i18nService.t("upgradeOrganization"), title: this.i18nService.t("upgradeOrganization"),
content: this.i18nService.t( content: this.i18nService.t(
this.organization.canManageBilling this.organization.canEditSubscription
? "freeOrgInvLimitReachedManageBilling" ? "freeOrgInvLimitReachedManageBilling"
: "freeOrgInvLimitReachedNoManageBilling", : "freeOrgInvLimitReachedNoManageBilling",
this.organization.seats this.organization.seats
@ -355,7 +355,7 @@ export class PeopleComponent
type: SimpleDialogType.PRIMARY, type: SimpleDialogType.PRIMARY,
}; };
if (this.organization.canManageBilling) { if (this.organization.canEditSubscription) {
orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade"); orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade");
} else { } else {
orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok"); orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok");
@ -369,7 +369,7 @@ export class PeopleComponent
return; return;
} }
if (result == SimpleDialogCloseType.ACCEPT && this.organization.canManageBilling) { if (result == SimpleDialogCloseType.ACCEPT && this.organization.canEditSubscription) {
this.router.navigate(["/organizations", this.organization.id, "billing", "subscription"], { this.router.navigate(["/organizations", this.organization.id, "billing", "subscription"], {
queryParams: { upgrade: true }, queryParams: { upgrade: true },
}); });

View File

@ -37,7 +37,7 @@
type="text" type="text"
name="BillingEmail" name="BillingEmail"
[(ngModel)]="org.billingEmail" [(ngModel)]="org.billingEmail"
[disabled]="selfHosted || !canManageBilling" [disabled]="selfHosted || !canEditSubscription"
/> />
</div> </div>
<div class="form-group"> <div class="form-group">
@ -48,7 +48,7 @@
type="text" type="text"
name="BusinessName" name="BusinessName"
[(ngModel)]="org.businessName" [(ngModel)]="org.businessName"
[disabled]="selfHosted || !canManageBilling" [disabled]="selfHosted || !canEditSubscription"
/> />
</div> </div>
</div> </div>

View File

@ -33,7 +33,7 @@ export class AccountComponent {
rotateApiKeyModalRef: ViewContainerRef; rotateApiKeyModalRef: ViewContainerRef;
selfHosted = false; selfHosted = false;
canManageBilling = true; canEditSubscription = true;
loading = true; loading = true;
canUseApi = false; canUseApi = false;
org: OrganizationResponse; org: OrganizationResponse;
@ -60,7 +60,9 @@ export class AccountComponent {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.parent.params.subscribe(async (params) => { this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId; this.organizationId = params.organizationId;
this.canManageBilling = this.organizationService.get(this.organizationId).canManageBilling; this.canEditSubscription = this.organizationService.get(
this.organizationId
).canEditSubscription;
try { try {
this.org = await this.organizationApiService.get(this.organizationId); this.org = await this.organizationApiService.get(this.organizationId);
this.canUseApi = this.org.useApi; this.canUseApi = this.org.useApi;

View File

@ -34,7 +34,7 @@ const routes: Routes = [
canActivate: [OrganizationPermissionsGuard], canActivate: [OrganizationPermissionsGuard],
data: { data: {
titleId: "paymentMethod", titleId: "paymentMethod",
organizationPermissions: (org: Organization) => org.canManageBilling, organizationPermissions: (org: Organization) => org.canEditPaymentMethods,
}, },
}, },
{ {
@ -43,7 +43,7 @@ const routes: Routes = [
canActivate: [OrganizationPermissionsGuard], canActivate: [OrganizationPermissionsGuard],
data: { data: {
titleId: "billingHistory", titleId: "billingHistory",
organizationPermissions: (org: Organization) => org.canManageBilling, organizationPermissions: (org: Organization) => org.canViewBillingHistory,
}, },
}, },
], ],

View File

@ -21,7 +21,12 @@ export class OrganizationBillingTabComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.showPaymentAndHistory$ = this.route.params.pipe( this.showPaymentAndHistory$ = this.route.params.pipe(
switchMap((params) => this.organizationService.get$(params.organizationId)), switchMap((params) => this.organizationService.get$(params.organizationId)),
map((org) => !this.platformUtilsService.isSelfHost() && org.canManageBilling) map(
(org) =>
!this.platformUtilsService.isSelfHost() &&
org.canViewBillingHistory &&
org.canEditPaymentMethods
)
); );
} }
} }

View File

@ -17,7 +17,7 @@
</ng-container> </ng-container>
<app-org-subscription-hidden <app-org-subscription-hidden
*ngIf="firstLoaded && !userOrg.canManageBilling" *ngIf="firstLoaded && !userOrg.canViewSubscription"
[providerName]="userOrg.providerName" [providerName]="userOrg.providerName"
></app-org-subscription-hidden> ></app-org-subscription-hidden>
@ -64,30 +64,24 @@
</ng-container> </ng-container>
</dl> </dl>
</div> </div>
<div class="col-8" *ngIf="subscription"> <ng-container *ngIf="userOrg.canEditSubscription">
<strong class="d-block mb-1">{{ "details" | i18n }}</strong> <div class="col-8" *ngIf="subscription">
<table class="table"> <strong class="d-block mb-1">{{ "details" | i18n }}</strong>
<tbody> <table class="table">
<tr *ngFor="let i of subscription.items"> <tbody>
<td> <tr *ngFor="let i of subscription.items">
{{ i.name }} {{ i.quantity > 1 ? "&times;" + i.quantity : "" }} @ <td>
{{ i.amount | currency : "$" }} {{ i.name }} {{ i.quantity > 1 ? "&times;" + i.quantity : "" }} @
</td> {{ i.amount | currency : "$" }}
<td>{{ i.quantity * i.amount | currency : "$" }} /{{ i.interval | i18n }}</td> </td>
</tr> <td>{{ i.quantity * i.amount | currency : "$" }} /{{ i.interval | i18n }}</td>
</tbody> </tr>
</table> </tbody>
</div> </table>
<ng-container *ngIf="userOrg?.providerId != null">
<div class="col-sm">
<dl>
<dt>{{ "provider" | i18n }}</dt>
<dd>{{ "yourProviderIs" | i18n : userOrg.providerName }}</dd>
</dl>
</div> </div>
</ng-container> </ng-container>
</div> </div>
<ng-container> <ng-container *ngIf="userOrg.canEditSubscription">
<button <button
bitButton bitButton
buttonType="secondary" buttonType="secondary"
@ -105,80 +99,84 @@
></app-change-plan> ></app-change-plan>
</ng-container> </ng-container>
<sm-enroll <ng-container *ngIf="userOrg.canEditSubscription">
*ngIf="isAdmin" <sm-enroll
[enabled]="sub?.useSecretsManager" *ngIf="isAdmin"
[organizationId]="organizationId" [enabled]="sub?.useSecretsManager"
></sm-enroll> [organizationId]="organizationId"
></sm-enroll>
<h2 class="spaced-header">{{ "manageSubscription" | i18n }}</h2>
<p class="mb-4">{{ subscriptionDesc }}</p>
<ng-container
*ngIf="
subscription && canAdjustSeats && !subscription.cancelled && !subscriptionMarkedForCancel
"
>
<div class="mt-3">
<app-adjust-subscription
[seatPrice]="seatPrice"
[organizationId]="organizationId"
[interval]="billingInterval"
[currentSeatCount]="seats"
[maxAutoscaleSeats]="maxAutoscaleSeats"
(onAdjusted)="subscriptionAdjusted()"
>
</app-adjust-subscription>
</div>
</ng-container> </ng-container>
<button
bitButton <ng-container *ngIf="userOrg.canEditSubscription">
buttonType="danger" <h2 class="spaced-header">{{ "manageSubscription" | i18n }}</h2>
type="button" <p class="mb-4">{{ subscriptionDesc }}</p>
[bitAction]="removeSponsorship" <ng-container
*ngIf="isSponsoredSubscription" *ngIf="
> subscription && canAdjustSeats && !subscription.cancelled && !subscriptionMarkedForCancel
{{ "removeSponsorship" | i18n }} "
</button>
<h2 class="spaced-header">{{ "storage" | i18n }}</h2>
<p>{{ "subscriptionStorage" | i18n : sub.maxStorageGb || 0 : sub.storageName || "0 MB" }}</p>
<div class="progress">
<div
class="progress-bar bg-success"
role="progressbar"
[ngStyle]="{ width: storageProgressWidth + '%' }"
[attr.aria-valuenow]="storagePercentage"
aria-valuemin="0"
aria-valuemax="100"
> >
{{ storagePercentage / 100 | percent }} <div class="mt-3">
</div> <app-adjust-subscription
</div> [seatPrice]="seatPrice"
<ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel"> [organizationId]="organizationId"
<div class="mt-3"> [interval]="billingInterval"
<div class="d-flex" *ngIf="!showAdjustStorage"> [currentSeatCount]="seats"
<button bitButton buttonType="secondary" type="button" (click)="adjustStorage(true)"> [maxAutoscaleSeats]="maxAutoscaleSeats"
{{ "addStorage" | i18n }} (onAdjusted)="subscriptionAdjusted()"
</button>
<button
bitButton
buttonType="secondary"
type="button"
class="ml-1"
(click)="adjustStorage(false)"
> >
{{ "removeStorage" | i18n }} </app-adjust-subscription>
</button> </div>
</ng-container>
<button
bitButton
buttonType="danger"
type="button"
[bitAction]="removeSponsorship"
*ngIf="isSponsoredSubscription"
>
{{ "removeSponsorship" | i18n }}
</button>
<h2 class="spaced-header">{{ "storage" | i18n }}</h2>
<p>{{ "subscriptionStorage" | i18n : sub.maxStorageGb || 0 : sub.storageName || "0 MB" }}</p>
<div class="progress">
<div
class="progress-bar bg-success"
role="progressbar"
[ngStyle]="{ width: storageProgressWidth + '%' }"
[attr.aria-valuenow]="storagePercentage"
aria-valuemin="0"
aria-valuemax="100"
>
{{ storagePercentage / 100 | percent }}
</div> </div>
<app-adjust-storage
[storageGbPrice]="storageGbPrice"
[add]="adjustStorageAdd"
[organizationId]="organizationId"
[interval]="billingInterval"
(onAdjusted)="closeStorage(true)"
(onCanceled)="closeStorage(false)"
*ngIf="showAdjustStorage"
></app-adjust-storage>
</div> </div>
<ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
<div class="mt-3">
<div class="d-flex" *ngIf="!showAdjustStorage">
<button bitButton buttonType="secondary" type="button" (click)="adjustStorage(true)">
{{ "addStorage" | i18n }}
</button>
<button
bitButton
buttonType="secondary"
type="button"
class="ml-1"
(click)="adjustStorage(false)"
>
{{ "removeStorage" | i18n }}
</button>
</div>
<app-adjust-storage
[storageGbPrice]="storageGbPrice"
[add]="adjustStorageAdd"
[organizationId]="organizationId"
[interval]="billingInterval"
(onAdjusted)="closeStorage(true)"
(onCanceled)="closeStorage(false)"
*ngIf="showAdjustStorage"
></app-adjust-storage>
</div>
</ng-container>
</ng-container> </ng-container>
<h2 class="spaced-header">{{ "selfHostingTitle" | i18n }}</h2> <h2 class="spaced-header">{{ "selfHostingTitle" | i18n }}</h2>
@ -214,20 +212,22 @@
(onCanceled)="closeDownloadLicense()" (onCanceled)="closeDownloadLicense()"
></app-download-license> ></app-download-license>
</div> </div>
<h2 class="spaced-header">{{ "additionalOptions" | i18n }}</h2> <ng-container *ngIf="userOrg.canEditSubscription">
<p class="mb-4"> <h2 class="spaced-header">{{ "additionalOptions" | i18n }}</h2>
{{ "additionalOptionsDesc" | i18n }} <p class="mb-4">
</p> {{ "additionalOptionsDesc" | i18n }}
<div class="d-flex"> </p>
<button <div class="d-flex">
bitButton <button
buttonType="danger" bitButton
[bitAction]="cancel" buttonType="danger"
type="button" [bitAction]="cancel"
class="ml-1" type="button"
*ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel" class="ml-1"
> *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel"
{{ "cancelSubscription" | i18n }} >
</button> {{ "cancelSubscription" | i18n }}
</div> </button>
</div>
</ng-container>
</ng-container> </ng-container>

View File

@ -77,7 +77,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
} }
this.loading = true; this.loading = true;
this.userOrg = this.organizationService.get(this.organizationId); this.userOrg = this.organizationService.get(this.organizationId);
if (this.userOrg.canManageBilling) { if (this.userOrg.canViewSubscription) {
this.sub = await this.organizationApiService.getSubscription(this.organizationId); this.sub = await this.organizationApiService.getSubscription(this.organizationId);
} }

View File

@ -18,7 +18,7 @@
</ng-container> </ng-container>
<app-org-subscription-hidden <app-org-subscription-hidden
*ngIf="firstLoaded && !userOrg.canManageBilling" *ngIf="firstLoaded && !userOrg.canViewSubscription"
[providerName]="userOrg.providerName" [providerName]="userOrg.providerName"
></app-org-subscription-hidden> ></app-org-subscription-hidden>

View File

@ -101,7 +101,7 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest
} }
this.loading = true; this.loading = true;
this.userOrg = this.organizationService.get(this.organizationId); this.userOrg = this.organizationService.get(this.organizationId);
if (this.userOrg.canManageBilling) { if (this.userOrg.canViewSubscription) {
this.sub = await this.organizationApiService.getSubscription(this.organizationId); this.sub = await this.organizationApiService.getSubscription(this.organizationId);
} }

View File

@ -111,7 +111,7 @@ export class VaultHeaderComponent {
const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = { const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = {
title: this.i18nService.t("upgradeOrganization"), title: this.i18nService.t("upgradeOrganization"),
content: this.i18nService.t( content: this.i18nService.t(
this.organization.canManageBilling this.organization.canEditSubscription
? "freeOrgMaxCollectionReachedManageBilling" ? "freeOrgMaxCollectionReachedManageBilling"
: "freeOrgMaxCollectionReachedNoManageBilling", : "freeOrgMaxCollectionReachedNoManageBilling",
this.organization.maxCollections this.organization.maxCollections
@ -119,7 +119,7 @@ export class VaultHeaderComponent {
type: SimpleDialogType.PRIMARY, type: SimpleDialogType.PRIMARY,
}; };
if (this.organization.canManageBilling) { if (this.organization.canEditSubscription) {
orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade"); orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade");
} else { } else {
orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok"); orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok");
@ -133,7 +133,7 @@ export class VaultHeaderComponent {
return; return;
} }
if (result == SimpleDialogCloseType.ACCEPT && this.organization.canManageBilling) { if (result == SimpleDialogCloseType.ACCEPT && this.organization.canEditSubscription) {
this.router.navigate(["/organizations", this.organization.id, "billing", "subscription"], { this.router.navigate(["/organizations", this.organization.id, "billing", "subscription"], {
queryParams: { upgrade: true }, queryParams: { upgrade: true },
}); });

View File

@ -6,13 +6,17 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.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 { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; 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 { 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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; 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 { Utils } from "@bitwarden/common/misc/utils";
import { BaseAcceptComponent } from "../app/common/base.accept.component"; import { BaseAcceptComponent } from "../app/common/base.accept.component";
@ -44,32 +48,33 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent {
} }
async authedHandler(qParams: Params): Promise<void> { async authedHandler(qParams: Params): Promise<void> {
const needsReAuth = (await this.stateService.getOrganizationInvitation()) != null; const initOrganization =
if (!needsReAuth) { qParams.initOrganization != null && qParams.initOrganization.toLocaleLowerCase() === "true";
// Accepting an org invite requires authentication from a logged out state if (initOrganization) {
this.messagingService.send("logout", { redirect: false }); this.actionPromise = this.acceptInitOrganizationFlow(qParams);
await this.prepareOrganizationInvitation(qParams); } else {
return; 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.actionPromise;
await this.stateService.setOrganizationInvitation(null);
this.platformUtilService.showToast( this.platformUtilService.showToast(
"success", "success",
this.i18nService.t("inviteAccepted"), this.i18nService.t("inviteAccepted"),
this.i18nService.t("inviteAcceptedDesc"), initOrganization
? this.i18nService.t("inviteInitAcceptedDesc")
: this.i18nService.t("inviteAcceptedDesc"),
{ timeout: 10000 } { timeout: 10000 }
); );
this.router.navigate(["/vault"]); this.router.navigate(["/vault"]);
} }
@ -77,6 +82,51 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent {
await this.prepareOrganizationInvitation(qParams); await this.prepareOrganizationInvitation(qParams);
} }
private async acceptInitOrganizationFlow(qParams: Params): Promise<any> {
return this.prepareAcceptInitRequest(qParams).then((request) =>
this.organizationUserService.postOrganizationUserAcceptInit(
qParams.organizationId,
qParams.organizationUserId,
request
)
);
}
private async acceptFlow(qParams: Params): Promise<any> {
return this.prepareAcceptRequest(qParams).then((request) =>
this.organizationUserService.postOrganizationUserAccept(
qParams.organizationId,
qParams.organizationUserId,
request
)
);
}
private async prepareAcceptInitRequest(
qParams: Params
): Promise<OrganizationUserAcceptInitRequest> {
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<OrganizationUserAcceptRequest> { private async prepareAcceptRequest(qParams: Params): Promise<OrganizationUserAcceptRequest> {
const request = new OrganizationUserAcceptRequest(); const request = new OrganizationUserAcceptRequest();
request.token = qParams.token; request.token = qParams.token;

View File

@ -3126,6 +3126,9 @@
"inviteAcceptedDesc": { "inviteAcceptedDesc": {
"message": "You can access this organization once an administrator confirms your membership. We'll send you an email when that happens." "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": { "inviteAcceptFailed": {
"message": "Unable to accept invitation. Ask an organization admin to send a new invitation." "message": "Unable to accept invitation. Ask an organization admin to send a new invitation."
}, },

View File

@ -1,6 +1,7 @@
import { ListResponse } from "../../models/response/list.response"; import { ListResponse } from "../../models/response/list.response";
import { import {
OrganizationUserAcceptInitRequest,
OrganizationUserAcceptRequest, OrganizationUserAcceptRequest,
OrganizationUserBulkConfirmRequest, OrganizationUserBulkConfirmRequest,
OrganizationUserConfirmRequest, OrganizationUserConfirmRequest,
@ -94,6 +95,20 @@ export abstract class OrganizationUserService {
ids: string[] ids: string[]
): Promise<ListResponse<OrganizationUserBulkResponse>>; ): Promise<ListResponse<OrganizationUserBulkResponse>>;
/**
* 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<void>;
/** /**
* Accept an organization user invitation * Accept an organization user invitation
* @param organizationId - Identifier for the organization to accept * @param organizationId - Identifier for the organization to accept

View File

@ -1,3 +1,4 @@
export * from "./organization-user-accept-init.request";
export * from "./organization-user-accept.request"; export * from "./organization-user-accept.request";
export * from "./organization-user-bulk-confirm.request"; export * from "./organization-user-bulk-confirm.request";
export * from "./organization-user-confirm.request"; export * from "./organization-user-confirm.request";

View File

@ -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;
}

View File

@ -1,4 +1,4 @@
import { ProductType } from "../../../enums"; import { ProductType, ProviderType } from "../../../enums";
import { OrganizationUserStatusType, OrganizationUserType } from "../../enums"; import { OrganizationUserStatusType, OrganizationUserType } from "../../enums";
import { PermissionsApi } from "../api/permissions.api"; import { PermissionsApi } from "../api/permissions.api";
import { ProfileOrganizationResponse } from "../response/profile-organization.response"; import { ProfileOrganizationResponse } from "../response/profile-organization.response";
@ -36,6 +36,7 @@ export class OrganizationData {
hasPublicAndPrivateKeys: boolean; hasPublicAndPrivateKeys: boolean;
providerId: string; providerId: string;
providerName: string; providerName: string;
providerType?: ProviderType;
isProviderUser: boolean; isProviderUser: boolean;
familySponsorshipFriendlyName: string; familySponsorshipFriendlyName: string;
familySponsorshipAvailable: boolean; familySponsorshipAvailable: boolean;
@ -80,6 +81,7 @@ export class OrganizationData {
this.hasPublicAndPrivateKeys = response.hasPublicAndPrivateKeys; this.hasPublicAndPrivateKeys = response.hasPublicAndPrivateKeys;
this.providerId = response.providerId; this.providerId = response.providerId;
this.providerName = response.providerName; this.providerName = response.providerName;
this.providerType = response.providerType;
this.familySponsorshipFriendlyName = response.familySponsorshipFriendlyName; this.familySponsorshipFriendlyName = response.familySponsorshipFriendlyName;
this.familySponsorshipAvailable = response.familySponsorshipAvailable; this.familySponsorshipAvailable = response.familySponsorshipAvailable;
this.planProductType = response.planProductType; this.planProductType = response.planProductType;

View File

@ -1,6 +1,6 @@
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { ProductType } from "../../../enums"; import { ProductType, ProviderType } from "../../../enums";
import { OrganizationUserStatusType, OrganizationUserType } from "../../enums"; import { OrganizationUserStatusType, OrganizationUserType } from "../../enums";
import { PermissionsApi } from "../api/permissions.api"; import { PermissionsApi } from "../api/permissions.api";
import { OrganizationData } from "../data/organization.data"; import { OrganizationData } from "../data/organization.data";
@ -38,6 +38,7 @@ export class Organization {
hasPublicAndPrivateKeys: boolean; hasPublicAndPrivateKeys: boolean;
providerId: string; providerId: string;
providerName: string; providerName: string;
providerType?: ProviderType;
isProviderUser: boolean; isProviderUser: boolean;
familySponsorshipFriendlyName: string; familySponsorshipFriendlyName: string;
familySponsorshipAvailable: boolean; familySponsorshipAvailable: boolean;
@ -86,6 +87,7 @@ export class Organization {
this.hasPublicAndPrivateKeys = obj.hasPublicAndPrivateKeys; this.hasPublicAndPrivateKeys = obj.hasPublicAndPrivateKeys;
this.providerId = obj.providerId; this.providerId = obj.providerId;
this.providerName = obj.providerName; this.providerName = obj.providerName;
this.providerType = obj.providerType;
this.isProviderUser = obj.isProviderUser; this.isProviderUser = obj.isProviderUser;
this.familySponsorshipFriendlyName = obj.familySponsorshipFriendlyName; this.familySponsorshipFriendlyName = obj.familySponsorshipFriendlyName;
this.familySponsorshipAvailable = obj.familySponsorshipAvailable; this.familySponsorshipAvailable = obj.familySponsorshipAvailable;
@ -197,8 +199,26 @@ export class Organization {
return this.canManagePolicies; return this.canManagePolicies;
} }
get canManageBilling() { get canViewSubscription() {
return this.isOwner && (this.isProviderUser || !this.hasProvider); 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() { get hasProvider() {

View File

@ -1,4 +1,4 @@
import { ProductType } from "../../../enums"; import { ProductType, ProviderType } from "../../../enums";
import { BaseResponse } from "../../../models/response/base.response"; import { BaseResponse } from "../../../models/response/base.response";
import { OrganizationUserStatusType, OrganizationUserType } from "../../enums"; import { OrganizationUserStatusType, OrganizationUserType } from "../../enums";
import { PermissionsApi } from "../api/permissions.api"; import { PermissionsApi } from "../api/permissions.api";
@ -37,6 +37,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
userId: string; userId: string;
providerId: string; providerId: string;
providerName: string; providerName: string;
providerType?: ProviderType;
familySponsorshipFriendlyName: string; familySponsorshipFriendlyName: string;
familySponsorshipAvailable: boolean; familySponsorshipAvailable: boolean;
planProductType: ProductType; planProductType: ProductType;
@ -82,6 +83,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
this.userId = this.getResponseProperty("UserId"); this.userId = this.getResponseProperty("UserId");
this.providerId = this.getResponseProperty("ProviderId"); this.providerId = this.getResponseProperty("ProviderId");
this.providerName = this.getResponseProperty("ProviderName"); this.providerName = this.getResponseProperty("ProviderName");
this.providerType = this.getResponseProperty("ProviderType");
this.familySponsorshipFriendlyName = this.getResponseProperty("FamilySponsorshipFriendlyName"); this.familySponsorshipFriendlyName = this.getResponseProperty("FamilySponsorshipFriendlyName");
this.familySponsorshipAvailable = this.getResponseProperty("FamilySponsorshipAvailable"); this.familySponsorshipAvailable = this.getResponseProperty("FamilySponsorshipAvailable");
this.planProductType = this.getResponseProperty("PlanProductType"); this.planProductType = this.getResponseProperty("PlanProductType");

View File

@ -75,7 +75,7 @@ export class BillingSubscriptionItemResponse extends BaseResponse {
export class BillingSubscriptionUpcomingInvoiceResponse extends BaseResponse { export class BillingSubscriptionUpcomingInvoiceResponse extends BaseResponse {
date: string; date: string;
amount: number; amount?: number;
constructor(response: any) { constructor(response: any) {
super(response); super(response);

View File

@ -16,6 +16,7 @@ export * from "./log-level-type.enum";
export * from "./native-messaging-version.enum"; export * from "./native-messaging-version.enum";
export * from "./notification-type.enum"; export * from "./notification-type.enum";
export * from "./product-type.enum"; export * from "./product-type.enum";
export * from "./provider-type.enum";
export * from "./secure-note-type.enum"; export * from "./secure-note-type.enum";
export * from "./state-version.enum"; export * from "./state-version.enum";
export * from "./storage-location.enum"; export * from "./storage-location.enum";

View File

@ -0,0 +1,4 @@
export enum ProviderType {
Msp = 0,
Reseller = 1,
}

View File

@ -1,6 +1,7 @@
import { ApiService } from "../../abstractions/api.service"; import { ApiService } from "../../abstractions/api.service";
import { OrganizationUserService } from "../../abstractions/organization-user/organization-user.service"; import { OrganizationUserService } from "../../abstractions/organization-user/organization-user.service";
import { import {
OrganizationUserAcceptInitRequest,
OrganizationUserAcceptRequest, OrganizationUserAcceptRequest,
OrganizationUserBulkConfirmRequest, OrganizationUserBulkConfirmRequest,
OrganizationUserConfirmRequest, OrganizationUserConfirmRequest,
@ -135,6 +136,20 @@ export class OrganizationUserServiceImplementation implements OrganizationUserSe
return new ListResponse(r, OrganizationUserBulkResponse); return new ListResponse(r, OrganizationUserBulkResponse);
} }
postOrganizationUserAcceptInit(
organizationId: string,
id: string,
request: OrganizationUserAcceptInitRequest
): Promise<void> {
return this.apiService.send(
"POST",
"/organizations/" + organizationId + "/users/" + id + "/accept-init",
request,
true,
false
);
}
postOrganizationUserAccept( postOrganizationUserAccept(
organizationId: string, organizationId: string,
id: string, id: string,