mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[PM-24412] Make billing api service call in members component non blocking (#16103)
* refactor organization to signal, unblock loading due to api call * continue refactor WIP * clean up * refactor billingMetadata signal to observble * deffer billing call * refactor billingMetadata * cleanup, add comment * qa bug: add missing param
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormControl } from "@angular/forms";
|
||||
@@ -14,6 +12,7 @@ import {
|
||||
ProviderUserStatusType,
|
||||
ProviderUserType,
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -66,20 +65,20 @@ export abstract class BaseMembersComponent<UserView extends UserViewTypes> {
|
||||
|
||||
protected abstract dataSource: PeopleTableDataSource<UserView>;
|
||||
|
||||
firstLoaded: boolean;
|
||||
firstLoaded: boolean = false;
|
||||
|
||||
/**
|
||||
* The currently selected status filter, or null to show all active users.
|
||||
* The currently selected status filter, or undefined to show all active users.
|
||||
*/
|
||||
status: StatusType | null;
|
||||
status?: StatusType;
|
||||
|
||||
/**
|
||||
* The currently executing promise - used to avoid multiple user actions executing at once.
|
||||
*/
|
||||
actionPromise: Promise<void>;
|
||||
actionPromise?: Promise<void>;
|
||||
|
||||
protected searchControl = new FormControl("", { nonNullable: true });
|
||||
protected statusToggle = new BehaviorSubject<StatusType | null>(null);
|
||||
protected statusToggle = new BehaviorSubject<StatusType | undefined>(undefined);
|
||||
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
@@ -100,15 +99,20 @@ export abstract class BaseMembersComponent<UserView extends UserViewTypes> {
|
||||
);
|
||||
}
|
||||
|
||||
abstract edit(user: UserView): void;
|
||||
abstract getUsers(): Promise<ListResponse<UserView> | UserView[]>;
|
||||
abstract removeUser(id: string): Promise<void>;
|
||||
abstract reinviteUser(id: string): Promise<void>;
|
||||
abstract confirmUser(user: UserView, publicKey: Uint8Array): Promise<void>;
|
||||
abstract edit(user: UserView, organization?: Organization): void;
|
||||
abstract getUsers(organization?: Organization): Promise<ListResponse<UserView> | UserView[]>;
|
||||
abstract removeUser(id: string, organization?: Organization): Promise<void>;
|
||||
abstract reinviteUser(id: string, organization?: Organization): Promise<void>;
|
||||
abstract confirmUser(
|
||||
user: UserView,
|
||||
publicKey: Uint8Array,
|
||||
organization?: Organization,
|
||||
): Promise<void>;
|
||||
abstract invite(organization?: Organization): void;
|
||||
|
||||
async load() {
|
||||
async load(organization?: Organization) {
|
||||
// Load new users from the server
|
||||
const response = await this.getUsers();
|
||||
const response = await this.getUsers(organization);
|
||||
|
||||
// GetUsers can return a ListResponse or an Array
|
||||
if (response instanceof ListResponse) {
|
||||
@@ -120,10 +124,6 @@ export abstract class BaseMembersComponent<UserView extends UserViewTypes> {
|
||||
this.firstLoaded = true;
|
||||
}
|
||||
|
||||
invite() {
|
||||
this.edit(null);
|
||||
}
|
||||
|
||||
protected async removeUserConfirmationDialog(user: UserView) {
|
||||
return this.dialogService.openSimpleDialog({
|
||||
title: this.userNamePipe.transform(user),
|
||||
@@ -132,64 +132,61 @@ export abstract class BaseMembersComponent<UserView extends UserViewTypes> {
|
||||
});
|
||||
}
|
||||
|
||||
async remove(user: UserView) {
|
||||
async remove(user: UserView, organization?: Organization) {
|
||||
const confirmed = await this.removeUserConfirmationDialog(user);
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.actionPromise = this.removeUser(user.id);
|
||||
this.actionPromise = this.removeUser(user.id, organization);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("removedUserId", this.userNamePipe.transform(user)),
|
||||
});
|
||||
this.dataSource.removeUser(user);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
this.actionPromise = undefined;
|
||||
}
|
||||
|
||||
async reinvite(user: UserView) {
|
||||
async reinvite(user: UserView, organization?: Organization) {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionPromise = this.reinviteUser(user.id);
|
||||
this.actionPromise = this.reinviteUser(user.id, organization);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user)),
|
||||
});
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
this.actionPromise = undefined;
|
||||
}
|
||||
|
||||
async confirm(user: UserView) {
|
||||
async confirm(user: UserView, organization?: Organization) {
|
||||
const confirmUser = async (publicKey: Uint8Array) => {
|
||||
try {
|
||||
this.actionPromise = this.confirmUser(user, publicKey);
|
||||
this.actionPromise = this.confirmUser(user, publicKey, organization);
|
||||
await this.actionPromise;
|
||||
user.status = this.userStatusType.Confirmed;
|
||||
this.dataSource.replaceUser(user);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user)),
|
||||
});
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
throw e;
|
||||
} finally {
|
||||
this.actionPromise = null;
|
||||
this.actionPromise = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -204,11 +201,14 @@ export abstract class BaseMembersComponent<UserView extends UserViewTypes> {
|
||||
const autoConfirm = await firstValueFrom(
|
||||
this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$,
|
||||
);
|
||||
if (user == null) {
|
||||
throw new Error("Cannot confirm null user.");
|
||||
}
|
||||
if (autoConfirm == null || !autoConfirm) {
|
||||
const dialogRef = UserConfirmComponent.open(this.dialogService, {
|
||||
data: {
|
||||
name: this.userNamePipe.transform(user),
|
||||
userId: user != null ? user.userId : null,
|
||||
userId: user.id,
|
||||
publicKey: publicKey,
|
||||
confirmUser: () => confirmUser(publicKey),
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ const MaxCheckedCount = 500;
|
||||
/**
|
||||
* Returns true if the user matches the status, or where the status is `null`, if the user is active (not revoked).
|
||||
*/
|
||||
function statusFilter(user: UserViewTypes, status: StatusType) {
|
||||
function statusFilter(user: UserViewTypes, status?: StatusType) {
|
||||
if (status == null) {
|
||||
return user.status != OrganizationUserStatusType.Revoked;
|
||||
}
|
||||
@@ -35,7 +35,7 @@ function textFilter(user: UserViewTypes, text: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export function peopleFilter(searchText: string, status: StatusType) {
|
||||
export function peopleFilter(searchText: string, status?: StatusType) {
|
||||
return (user: UserViewTypes) => statusFilter(user, status) && textFilter(user, searchText);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<app-organization-free-trial-warning
|
||||
@let organization = this.organization();
|
||||
@if (organization) {
|
||||
<app-organization-free-trial-warning
|
||||
[organization]="organization"
|
||||
(clicked)="navigateToPaymentMethod()"
|
||||
>
|
||||
</app-organization-free-trial-warning>
|
||||
<app-header>
|
||||
(clicked)="navigateToPaymentMethod(organization)"
|
||||
>
|
||||
</app-organization-free-trial-warning>
|
||||
<app-header>
|
||||
<bit-search
|
||||
class="tw-grow"
|
||||
[formControl]="searchControl"
|
||||
@@ -14,21 +16,21 @@
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(click)="invite()"
|
||||
(click)="invite(organization)"
|
||||
[disabled]="!firstLoaded"
|
||||
*ngIf="showUserManagementControls$ | async"
|
||||
*ngIf="showUserManagementControls()"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "inviteMember" | i18n }}
|
||||
</button>
|
||||
</app-header>
|
||||
</app-header>
|
||||
|
||||
<div class="tw-mb-4 tw-flex tw-flex-col tw-space-y-4">
|
||||
<div class="tw-mb-4 tw-flex tw-flex-col tw-space-y-4">
|
||||
<bit-toggle-group
|
||||
[selected]="status"
|
||||
(selectedChange)="statusToggle.next($event)"
|
||||
[attr.aria-label]="'memberStatusFilter' | i18n"
|
||||
*ngIf="showUserManagementControls$ | async"
|
||||
*ngIf="showUserManagementControls()"
|
||||
>
|
||||
<bit-toggle [value]="null">
|
||||
{{ "all" | i18n }}
|
||||
@@ -58,16 +60,16 @@
|
||||
}}</span>
|
||||
</bit-toggle>
|
||||
</bit-toggle-group>
|
||||
</div>
|
||||
<ng-container *ngIf="!firstLoaded">
|
||||
</div>
|
||||
<ng-container *ngIf="!firstLoaded">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="firstLoaded">
|
||||
</ng-container>
|
||||
<ng-container *ngIf="firstLoaded">
|
||||
<p *ngIf="!dataSource.filteredData.length">{{ "noMembersInList" | i18n }}</p>
|
||||
<ng-container *ngIf="dataSource.filteredData.length">
|
||||
<bit-callout
|
||||
@@ -84,7 +86,7 @@
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell class="tw-w-20" *ngIf="showUserManagementControls$ | async">
|
||||
<th bitCell class="tw-w-20" *ngIf="showUserManagementControls()">
|
||||
<input
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
@@ -107,12 +109,12 @@
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
size="small"
|
||||
label="{{ 'options' | i18n }}"
|
||||
*ngIf="showUserManagementControls$ | async"
|
||||
*ngIf="showUserManagementControls()"
|
||||
></button>
|
||||
|
||||
<bit-menu #headerMenu>
|
||||
<ng-container *ngIf="canUseSecretsManager$ | async">
|
||||
<button type="button" bitMenuItem (click)="bulkEnableSM()">
|
||||
<ng-container *ngIf="canUseSecretsManager()">
|
||||
<button type="button" bitMenuItem (click)="bulkEnableSM(organization)">
|
||||
{{ "activateSecretsManager" | i18n }}
|
||||
</button>
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
@@ -120,7 +122,7 @@
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkReinvite()"
|
||||
(click)="bulkReinvite(organization)"
|
||||
*ngIf="showBulkReinviteUsers"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||
@@ -129,7 +131,7 @@
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkConfirm()"
|
||||
(click)="bulkConfirm(organization)"
|
||||
*ngIf="showBulkConfirmUsers"
|
||||
>
|
||||
<span class="tw-text-success">
|
||||
@@ -140,7 +142,7 @@
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkRestore()"
|
||||
(click)="bulkRestore(organization)"
|
||||
*ngIf="showBulkRestoreUsers"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||
@@ -149,7 +151,7 @@
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkRevoke()"
|
||||
(click)="bulkRevoke(organization)"
|
||||
*ngIf="showBulkRevokeUsers"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
|
||||
@@ -158,7 +160,7 @@
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkRemove()"
|
||||
(click)="bulkRemove(organization)"
|
||||
*ngIf="showBulkRemoveUsers"
|
||||
>
|
||||
<span class="tw-text-danger">
|
||||
@@ -169,7 +171,7 @@
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkDelete()"
|
||||
(click)="bulkDelete(organization)"
|
||||
*ngIf="showBulkDeleteUsers"
|
||||
>
|
||||
<span class="tw-text-danger">
|
||||
@@ -188,15 +190,11 @@
|
||||
alignContent="middle"
|
||||
[ngClass]="rowHeightClass"
|
||||
>
|
||||
<td
|
||||
bitCell
|
||||
(click)="dataSource.checkUser(u)"
|
||||
*ngIf="showUserManagementControls$ | async"
|
||||
>
|
||||
<td bitCell (click)="dataSource.checkUser(u)" *ngIf="showUserManagementControls()">
|
||||
<input type="checkbox" bitCheckbox [(ngModel)]="$any(u).checked" />
|
||||
</td>
|
||||
<ng-container *ngIf="showUserManagementControls$ | async; else readOnlyUserInfo">
|
||||
<td bitCell (click)="edit(u)" class="tw-cursor-pointer">
|
||||
<ng-container *ngIf="showUserManagementControls(); else readOnlyUserInfo">
|
||||
<td bitCell (click)="edit(u, organization)" class="tw-cursor-pointer">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
size="small"
|
||||
@@ -288,10 +286,16 @@
|
||||
</td>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="showUserManagementControls$ | async; else readOnlyGroupsCell">
|
||||
<ng-container *ngIf="showUserManagementControls(); else readOnlyGroupsCell">
|
||||
<td
|
||||
bitCell
|
||||
(click)="edit(u, organization.useGroups ? memberTab.Groups : memberTab.Collections)"
|
||||
(click)="
|
||||
edit(
|
||||
u,
|
||||
organization,
|
||||
organization.useGroups ? memberTab.Groups : memberTab.Collections
|
||||
)
|
||||
"
|
||||
class="tw-cursor-pointer"
|
||||
>
|
||||
<bit-badge-list
|
||||
@@ -311,10 +315,10 @@
|
||||
</td>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="showUserManagementControls$ | async; else readOnlyRoleCell">
|
||||
<ng-container *ngIf="showUserManagementControls(); else readOnlyRoleCell">
|
||||
<td
|
||||
bitCell
|
||||
(click)="edit(u, memberTab.Role)"
|
||||
(click)="edit(u, organization, memberTab.Role)"
|
||||
class="tw-cursor-pointer tw-text-sm tw-text-muted"
|
||||
>
|
||||
{{ u.type | userType }}
|
||||
@@ -326,7 +330,7 @@
|
||||
</td>
|
||||
</ng-template>
|
||||
|
||||
<td bitCell class="tw-text-muted tw-space-x-2">
|
||||
<td bitCell class="tw-text-muted">
|
||||
<ng-container *ngIf="u.twoFactorEnabled">
|
||||
<i
|
||||
class="bwi bwi-lock"
|
||||
@@ -335,7 +339,7 @@
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "userUsingTwoStep" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showEnrolledStatus($any(u))">
|
||||
<ng-container *ngIf="showEnrolledStatus($any(u), organization)">
|
||||
<i
|
||||
class="bwi bwi-key"
|
||||
title="{{ 'enrolledAccountRecovery' | i18n }}"
|
||||
@@ -354,11 +358,11 @@
|
||||
></button>
|
||||
|
||||
<bit-menu #rowMenu>
|
||||
<ng-container *ngIf="showUserManagementControls$ | async">
|
||||
<ng-container *ngIf="showUserManagementControls()">
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="reinvite(u)"
|
||||
(click)="reinvite(u, organization)"
|
||||
*ngIf="u.status === userStatusType.Invited"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
||||
@@ -367,7 +371,7 @@
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="confirm(u)"
|
||||
(click)="confirm(u, organization)"
|
||||
*ngIf="u.status === userStatusType.Accepted"
|
||||
>
|
||||
<span class="tw-text-success">
|
||||
@@ -379,18 +383,26 @@
|
||||
u.status === userStatusType.Accepted || u.status === userStatusType.Invited
|
||||
"
|
||||
></bit-menu-divider>
|
||||
<button type="button" bitMenuItem (click)="edit(u, memberTab.Role)">
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="edit(u, organization, memberTab.Role)"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "memberRole" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="edit(u, memberTab.Groups)"
|
||||
(click)="edit(u, organization, memberTab.Groups)"
|
||||
*ngIf="organization.useGroups"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "groups" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="edit(u, memberTab.Collections)">
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="edit(u, organization, memberTab.Collections)"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-collection-shared"></i>
|
||||
{{ "collections" | i18n }}
|
||||
</button>
|
||||
@@ -398,7 +410,7 @@
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="openEventsDialog(u)"
|
||||
(click)="openEventsDialog(u, organization)"
|
||||
*ngIf="organization.useEvents && u.status === userStatusType.Confirmed"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-file-text"></i> {{ "eventLogs" | i18n }}
|
||||
@@ -409,17 +421,17 @@
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="resetPassword(u)"
|
||||
*ngIf="allowResetPassword(u)"
|
||||
(click)="resetPassword(u, organization)"
|
||||
*ngIf="allowResetPassword(u, organization)"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-key"></i> {{ "recoverAccount" | i18n }}
|
||||
</button>
|
||||
|
||||
<ng-container *ngIf="showUserManagementControls$ | async">
|
||||
<ng-container *ngIf="showUserManagementControls()">
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="restore(u)"
|
||||
(click)="restore(u, organization)"
|
||||
*ngIf="u.status === userStatusType.Revoked"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-plus-circle"></i>
|
||||
@@ -428,7 +440,7 @@
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="revoke(u)"
|
||||
(click)="revoke(u, organization)"
|
||||
*ngIf="u.status !== userStatusType.Revoked"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-minus-circle"></i>
|
||||
@@ -438,7 +450,7 @@
|
||||
*ngIf="!u.managedByOrganization"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="remove(u)"
|
||||
(click)="remove(u, organization)"
|
||||
>
|
||||
<span class="tw-text-danger">
|
||||
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
|
||||
@@ -448,7 +460,7 @@
|
||||
*ngIf="u.managedByOrganization"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="deleteUser(u)"
|
||||
(click)="deleteUser(u, organization)"
|
||||
>
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-trash" aria-hidden="true"></i>
|
||||
@@ -463,4 +475,5 @@
|
||||
</bit-table>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Component, computed, Signal } from "@angular/core";
|
||||
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
concatMap,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
from,
|
||||
lastValueFrom,
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Observable,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
take,
|
||||
} from "rxjs";
|
||||
|
||||
import {
|
||||
@@ -47,13 +48,14 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
@@ -102,23 +104,24 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
memberTab = MemberDialogTab;
|
||||
protected dataSource = new MembersTableDataSource();
|
||||
|
||||
organization: Organization;
|
||||
status: OrganizationUserStatusType = null;
|
||||
organization: Signal<Organization | undefined>;
|
||||
status: OrganizationUserStatusType | undefined;
|
||||
orgResetPasswordPolicyEnabled = false;
|
||||
orgIsOnSecretsManagerStandalone = false;
|
||||
|
||||
protected canUseSecretsManager$: Observable<boolean>;
|
||||
protected showUserManagementControls$: Observable<boolean>;
|
||||
protected canUseSecretsManager: Signal<boolean> = computed(
|
||||
() => this.organization()?.useSecretsManager ?? false,
|
||||
);
|
||||
protected showUserManagementControls: Signal<boolean> = computed(
|
||||
() => this.organization()?.canManageUsers ?? false,
|
||||
);
|
||||
private refreshBillingMetadata$: BehaviorSubject<null> = new BehaviorSubject(null);
|
||||
protected billingMetadata$: Observable<OrganizationBillingMetadataResponse>;
|
||||
|
||||
// Fixed sizes used for cdkVirtualScroll
|
||||
protected rowHeight = 66;
|
||||
protected rowHeightClass = `tw-h-[66px]`;
|
||||
|
||||
private organizationUsersCount = 0;
|
||||
|
||||
get occupiedSeatCount(): number {
|
||||
return this.organizationUsersCount;
|
||||
}
|
||||
private userId$: Observable<UserId> = this.accountService.activeAccount$.pipe(getUserId);
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
@@ -162,61 +165,58 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
|
||||
const organization$ = this.route.params.pipe(
|
||||
concatMap((params) =>
|
||||
this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.userId$.pipe(
|
||||
switchMap((userId) =>
|
||||
this.organizationService
|
||||
.organizations$(account?.id)
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(params.organizationId)),
|
||||
),
|
||||
),
|
||||
),
|
||||
filter((organization): organization is Organization => organization != null),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
this.canUseSecretsManager$ = organization$.pipe(map((org) => org.useSecretsManager));
|
||||
this.organization = toSignal(organization$);
|
||||
|
||||
const policies$ = combineLatest([
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
organization$,
|
||||
]).pipe(
|
||||
switchMap(([userId, organization]) => {
|
||||
if (organization.isProviderUser) {
|
||||
return from(this.policyApiService.getPolicies(organization.id)).pipe(
|
||||
const policies$ = combineLatest([this.userId$, organization$]).pipe(
|
||||
switchMap(([userId, organization]) =>
|
||||
organization.isProviderUser
|
||||
? from(this.policyApiService.getPolicies(organization.id)).pipe(
|
||||
map((response) => Policy.fromListResponse(response)),
|
||||
);
|
||||
}
|
||||
|
||||
return this.policyService.policies$(userId);
|
||||
}),
|
||||
)
|
||||
: this.policyService.policies$(userId),
|
||||
),
|
||||
);
|
||||
|
||||
combineLatest([this.route.queryParams, policies$, organization$])
|
||||
.pipe(
|
||||
concatMap(async ([qParams, policies, organization]) => {
|
||||
this.organization = organization;
|
||||
|
||||
// Backfill pub/priv key if necessary
|
||||
if (
|
||||
this.organization.canManageUsersPassword &&
|
||||
!this.organization.hasPublicAndPrivateKeys
|
||||
) {
|
||||
if (organization.canManageUsersPassword && !organization.hasPublicAndPrivateKeys) {
|
||||
const orgShareKey = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
this.userId$.pipe(
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
map((orgKeys) => orgKeys[this.organization.id] ?? null),
|
||||
map((orgKeys) => {
|
||||
if (orgKeys == null || orgKeys[organization.id] == null) {
|
||||
throw new Error("Organization keys not found for provided User.");
|
||||
}
|
||||
return orgKeys[organization.id];
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const orgKeys = await this.keyService.makeKeyPair(orgShareKey);
|
||||
const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
|
||||
const response = await this.organizationApiService.updateKeys(
|
||||
this.organization.id,
|
||||
request,
|
||||
const [orgPublicKey, encryptedOrgPrivateKey] =
|
||||
await this.keyService.makeKeyPair(orgShareKey);
|
||||
if (encryptedOrgPrivateKey.encryptedString == null) {
|
||||
throw new Error("Encrypted private key is null.");
|
||||
}
|
||||
const request = new OrganizationKeysRequest(
|
||||
orgPublicKey,
|
||||
encryptedOrgPrivateKey.encryptedString,
|
||||
);
|
||||
const response = await this.organizationApiService.updateKeys(organization.id, request);
|
||||
if (response != null) {
|
||||
this.organization.hasPublicAndPrivateKeys =
|
||||
response.publicKey != null && response.privateKey != null;
|
||||
await this.syncService.fullSync(true); // Replace organizations with new data
|
||||
} else {
|
||||
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
||||
@@ -225,24 +225,17 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
|
||||
const resetPasswordPolicy = policies
|
||||
.filter((policy) => policy.type === PolicyType.ResetPassword)
|
||||
.find((p) => p.organizationId === this.organization.id);
|
||||
this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled;
|
||||
.find((p) => p.organizationId === organization.id);
|
||||
this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled ?? false;
|
||||
|
||||
const billingMetadata = await this.billingApiService.getOrganizationBillingMetadata(
|
||||
this.organization.id,
|
||||
);
|
||||
|
||||
this.orgIsOnSecretsManagerStandalone = billingMetadata.isOnSecretsManagerStandalone;
|
||||
this.organizationUsersCount = billingMetadata.organizationOccupiedSeats;
|
||||
|
||||
await this.load();
|
||||
await this.load(organization);
|
||||
|
||||
this.searchControl.setValue(qParams.search);
|
||||
|
||||
if (qParams.viewEvents != null) {
|
||||
const user = this.dataSource.data.filter((u) => u.id === qParams.viewEvents);
|
||||
if (user.length > 0 && user[0].status === OrganizationUserStatusType.Confirmed) {
|
||||
this.openEventsDialog(user[0]);
|
||||
this.openEventsDialog(user[0], organization);
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -250,10 +243,6 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.showUserManagementControls$ = organization$.pipe(
|
||||
map((organization) => organization.canManageUsers),
|
||||
);
|
||||
|
||||
organization$
|
||||
.pipe(
|
||||
switchMap((organization) =>
|
||||
@@ -265,23 +254,40 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.billingMetadata$ = combineLatest([this.refreshBillingMetadata$, organization$]).pipe(
|
||||
switchMap(([_, organization]) =>
|
||||
this.billingApiService.getOrganizationBillingMetadata(organization.id),
|
||||
),
|
||||
takeUntilDestroyed(),
|
||||
shareReplay({ bufferSize: 1, refCount: false }),
|
||||
);
|
||||
|
||||
// Stripe is slow, so kick this off in the background but without blocking page load.
|
||||
// Anyone who needs it will still await the first emission.
|
||||
this.billingMetadata$.pipe(take(1), takeUntilDestroyed()).subscribe();
|
||||
}
|
||||
|
||||
async getUsers(): Promise<OrganizationUserView[]> {
|
||||
let groupsPromise: Promise<Map<string, string>>;
|
||||
let collectionsPromise: Promise<Map<string, string>>;
|
||||
override async load(organization: Organization) {
|
||||
this.refreshBillingMetadata$.next(null);
|
||||
await super.load(organization);
|
||||
}
|
||||
|
||||
async getUsers(organization: Organization): Promise<OrganizationUserView[]> {
|
||||
let groupsPromise: Promise<Map<string, string>> | undefined;
|
||||
let collectionsPromise: Promise<Map<string, string>> | undefined;
|
||||
|
||||
// We don't need both groups and collections for the table, so only load one
|
||||
const userPromise = this.organizationUserApiService.getAllUsers(this.organization.id, {
|
||||
includeGroups: this.organization.useGroups,
|
||||
includeCollections: !this.organization.useGroups,
|
||||
const userPromise = this.organizationUserApiService.getAllUsers(organization.id, {
|
||||
includeGroups: organization.useGroups,
|
||||
includeCollections: !organization.useGroups,
|
||||
});
|
||||
|
||||
// Depending on which column is displayed, we need to load the group/collection names
|
||||
if (this.organization.useGroups) {
|
||||
groupsPromise = this.getGroupNameMap();
|
||||
if (organization.useGroups) {
|
||||
groupsPromise = this.getGroupNameMap(organization);
|
||||
} else {
|
||||
collectionsPromise = this.getCollectionNameMap();
|
||||
collectionsPromise = this.getCollectionNameMap(organization);
|
||||
}
|
||||
|
||||
const [usersResponse, groupNamesMap, collectionNamesMap] = await Promise.all([
|
||||
@@ -290,22 +296,26 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
collectionsPromise,
|
||||
]);
|
||||
|
||||
return usersResponse.data?.map<OrganizationUserView>((r) => {
|
||||
return (
|
||||
usersResponse.data?.map<OrganizationUserView>((r) => {
|
||||
const userView = OrganizationUserView.fromResponse(r);
|
||||
|
||||
userView.groupNames = userView.groups
|
||||
.map((g) => groupNamesMap.get(g))
|
||||
.map((g) => groupNamesMap?.get(g))
|
||||
.filter((name): name is string => name != null)
|
||||
.sort(this.i18nService.collator?.compare);
|
||||
userView.collectionNames = userView.collections
|
||||
.map((c) => collectionNamesMap.get(c.id))
|
||||
.map((c) => collectionNamesMap?.get(c.id))
|
||||
.filter((name): name is string => name != null)
|
||||
.sort(this.i18nService.collator?.compare);
|
||||
|
||||
return userView;
|
||||
});
|
||||
}) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
async getGroupNameMap(): Promise<Map<string, string>> {
|
||||
const groups = await this.groupService.getAll(this.organization.id);
|
||||
async getGroupNameMap(organization: Organization): Promise<Map<string, string>> {
|
||||
const groups = await this.groupService.getAll(organization.id);
|
||||
const groupNameMap = new Map<string, string>();
|
||||
groups.forEach((g) => groupNameMap.set(g.id, g.name));
|
||||
return groupNameMap;
|
||||
@@ -314,8 +324,8 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
/**
|
||||
* Retrieve a map of all collection IDs <-> names for the organization.
|
||||
*/
|
||||
async getCollectionNameMap() {
|
||||
const response = from(this.apiService.getCollections(this.organization.id)).pipe(
|
||||
async getCollectionNameMap(organization: Organization) {
|
||||
const response = from(this.apiService.getCollections(organization.id)).pipe(
|
||||
map((res) =>
|
||||
res.data.map((r) =>
|
||||
Collection.fromCollectionData(new CollectionData(r as CollectionDetailsResponse)),
|
||||
@@ -324,9 +334,9 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
);
|
||||
|
||||
const decryptedCollections$ = combineLatest([
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
this.userId$.pipe(
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
filter((orgKeys) => orgKeys != null),
|
||||
),
|
||||
response,
|
||||
]).pipe(
|
||||
@@ -343,91 +353,94 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
return await firstValueFrom(decryptedCollections$);
|
||||
}
|
||||
|
||||
removeUser(id: string): Promise<void> {
|
||||
return this.organizationUserApiService.removeOrganizationUser(this.organization.id, id);
|
||||
removeUser(id: string, organization: Organization): Promise<void> {
|
||||
return this.organizationUserApiService.removeOrganizationUser(organization.id, id);
|
||||
}
|
||||
|
||||
revokeUser(id: string): Promise<void> {
|
||||
return this.organizationUserApiService.revokeOrganizationUser(this.organization.id, id);
|
||||
revokeUser(id: string, organization: Organization): Promise<void> {
|
||||
return this.organizationUserApiService.revokeOrganizationUser(organization.id, id);
|
||||
}
|
||||
|
||||
restoreUser(id: string): Promise<void> {
|
||||
return this.organizationUserApiService.restoreOrganizationUser(this.organization.id, id);
|
||||
restoreUser(id: string, organization: Organization): Promise<void> {
|
||||
return this.organizationUserApiService.restoreOrganizationUser(organization.id, id);
|
||||
}
|
||||
|
||||
reinviteUser(id: string): Promise<void> {
|
||||
return this.organizationUserApiService.postOrganizationUserReinvite(this.organization.id, id);
|
||||
reinviteUser(id: string, organization: Organization): Promise<void> {
|
||||
return this.organizationUserApiService.postOrganizationUserReinvite(organization.id, id);
|
||||
}
|
||||
|
||||
async confirmUser(user: OrganizationUserView, publicKey: Uint8Array): Promise<void> {
|
||||
async confirmUser(
|
||||
user: OrganizationUserView,
|
||||
publicKey: Uint8Array,
|
||||
organization: Organization,
|
||||
): Promise<void> {
|
||||
if (
|
||||
await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation))
|
||||
) {
|
||||
await firstValueFrom(
|
||||
this.organizationUserService.confirmUser(this.organization, user, publicKey),
|
||||
);
|
||||
await firstValueFrom(this.organizationUserService.confirmUser(organization, user, publicKey));
|
||||
} else {
|
||||
const orgKey = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
const request = await firstValueFrom(
|
||||
this.userId$.pipe(
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
map((orgKeys) => orgKeys[this.organization.id] ?? null),
|
||||
filter((orgKeys) => orgKeys != null),
|
||||
map((orgKeys) => orgKeys[organization.id]),
|
||||
switchMap((orgKey) => this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey)),
|
||||
map((encKey) => {
|
||||
const req = new OrganizationUserConfirmRequest();
|
||||
req.key = encKey.encryptedString;
|
||||
return req;
|
||||
}),
|
||||
),
|
||||
);
|
||||
const key = await this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey);
|
||||
const request = new OrganizationUserConfirmRequest();
|
||||
request.key = key.encryptedString;
|
||||
|
||||
await this.organizationUserApiService.postOrganizationUserConfirm(
|
||||
this.organization.id,
|
||||
organization.id,
|
||||
user.id,
|
||||
request,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async revoke(user: OrganizationUserView) {
|
||||
async revoke(user: OrganizationUserView, organization: Organization) {
|
||||
const confirmed = await this.revokeUserConfirmationDialog(user);
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.actionPromise = this.revokeUser(user.id);
|
||||
this.actionPromise = this.revokeUser(user.id, organization);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("revokedUserId", this.userNamePipe.transform(user)),
|
||||
});
|
||||
await this.load();
|
||||
await this.load(organization);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
this.actionPromise = undefined;
|
||||
}
|
||||
|
||||
async restore(user: OrganizationUserView) {
|
||||
this.actionPromise = this.restoreUser(user.id);
|
||||
async restore(user: OrganizationUserView, organization: Organization) {
|
||||
this.actionPromise = this.restoreUser(user.id, organization);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("restoredUserId", this.userNamePipe.transform(user)),
|
||||
});
|
||||
await this.load();
|
||||
await this.load(organization);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
this.actionPromise = undefined;
|
||||
}
|
||||
|
||||
allowResetPassword(orgUser: OrganizationUserView): boolean {
|
||||
// Hierarchy check
|
||||
allowResetPassword(orgUser: OrganizationUserView, organization: Organization): boolean {
|
||||
let callingUserHasPermission = false;
|
||||
|
||||
switch (this.organization.type) {
|
||||
switch (organization.type) {
|
||||
case OrganizationUserType.Owner:
|
||||
callingUserHasPermission = true;
|
||||
break;
|
||||
@@ -441,33 +454,35 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
break;
|
||||
}
|
||||
|
||||
// Final
|
||||
return (
|
||||
this.organization.canManageUsersPassword &&
|
||||
organization.canManageUsersPassword &&
|
||||
callingUserHasPermission &&
|
||||
this.organization.useResetPassword &&
|
||||
this.organization.hasPublicAndPrivateKeys &&
|
||||
organization.useResetPassword &&
|
||||
organization.hasPublicAndPrivateKeys &&
|
||||
orgUser.resetPasswordEnrolled &&
|
||||
this.orgResetPasswordPolicyEnabled &&
|
||||
orgUser.status === OrganizationUserStatusType.Confirmed
|
||||
);
|
||||
}
|
||||
|
||||
showEnrolledStatus(orgUser: OrganizationUserUserDetailsResponse): boolean {
|
||||
showEnrolledStatus(
|
||||
orgUser: OrganizationUserUserDetailsResponse,
|
||||
organization: Organization,
|
||||
): boolean {
|
||||
return (
|
||||
this.organization.useResetPassword &&
|
||||
organization.useResetPassword &&
|
||||
orgUser.resetPasswordEnrolled &&
|
||||
this.orgResetPasswordPolicyEnabled
|
||||
);
|
||||
}
|
||||
|
||||
private getManageBillingText(): string {
|
||||
return this.organization.canEditSubscription ? "ManageBilling" : "NoManageBilling";
|
||||
private getManageBillingText(organization: Organization): string {
|
||||
return organization.canEditSubscription ? "ManageBilling" : "NoManageBilling";
|
||||
}
|
||||
|
||||
private getProductKey(productType: ProductTierType): string {
|
||||
private getProductKey(organization: Organization): string {
|
||||
let product = "";
|
||||
switch (productType) {
|
||||
switch (organization.productTierType) {
|
||||
case ProductTierType.Free:
|
||||
product = "freeOrg";
|
||||
break;
|
||||
@@ -478,24 +493,21 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
product = "familiesPlan";
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported product type: ${productType}`);
|
||||
throw new Error(`Unsupported product type: ${organization.productTierType}`);
|
||||
}
|
||||
return `${product}InvLimitReached${this.getManageBillingText()}`;
|
||||
return `${product}InvLimitReached${this.getManageBillingText(organization)}`;
|
||||
}
|
||||
|
||||
private getDialogContent(): string {
|
||||
return this.i18nService.t(
|
||||
this.getProductKey(this.organization.productTierType),
|
||||
this.organization.seats,
|
||||
);
|
||||
private getDialogContent(organization: Organization): string {
|
||||
return this.i18nService.t(this.getProductKey(organization), organization.seats);
|
||||
}
|
||||
|
||||
private getAcceptButtonText(): string {
|
||||
if (!this.organization.canEditSubscription) {
|
||||
private getAcceptButtonText(organization: Organization): string {
|
||||
if (!organization.canEditSubscription) {
|
||||
return this.i18nService.t("ok");
|
||||
}
|
||||
|
||||
const productType = this.organization.productTierType;
|
||||
const productType = organization.productTierType;
|
||||
|
||||
if (isNotSelfUpgradable(productType)) {
|
||||
throw new Error(`Unsupported product type: ${productType}`);
|
||||
@@ -504,82 +516,88 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
return this.i18nService.t("upgrade");
|
||||
}
|
||||
|
||||
private async handleDialogClose(result: boolean | undefined): Promise<void> {
|
||||
if (!result || !this.organization.canEditSubscription) {
|
||||
private async handleDialogClose(
|
||||
result: boolean | undefined,
|
||||
organization: Organization,
|
||||
): Promise<void> {
|
||||
if (!result || !organization.canEditSubscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
const productType = this.organization.productTierType;
|
||||
const productType = organization.productTierType;
|
||||
|
||||
if (isNotSelfUpgradable(productType)) {
|
||||
throw new Error(`Unsupported product type: ${this.organization.productTierType}`);
|
||||
throw new Error(`Unsupported product type: ${organization.productTierType}`);
|
||||
}
|
||||
|
||||
await this.router.navigate(
|
||||
["/organizations", this.organization.id, "billing", "subscription"],
|
||||
{ queryParams: { upgrade: true } },
|
||||
);
|
||||
await this.router.navigate(["/organizations", organization.id, "billing", "subscription"], {
|
||||
queryParams: { upgrade: true },
|
||||
});
|
||||
}
|
||||
|
||||
private async showSeatLimitReachedDialog(): Promise<void> {
|
||||
private async showSeatLimitReachedDialog(organization: Organization): Promise<void> {
|
||||
const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = {
|
||||
title: this.i18nService.t("upgradeOrganization"),
|
||||
content: this.getDialogContent(),
|
||||
content: this.getDialogContent(organization),
|
||||
type: "primary",
|
||||
acceptButtonText: this.getAcceptButtonText(),
|
||||
acceptButtonText: this.getAcceptButtonText(organization),
|
||||
};
|
||||
|
||||
if (!this.organization.canEditSubscription) {
|
||||
if (!organization.canEditSubscription) {
|
||||
orgUpgradeSimpleDialogOpts.cancelButtonText = null;
|
||||
}
|
||||
|
||||
const simpleDialog = this.dialogService.openSimpleDialogRef(orgUpgradeSimpleDialogOpts);
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
firstValueFrom(simpleDialog.closed).then(this.handleDialogClose.bind(this));
|
||||
await lastValueFrom(
|
||||
simpleDialog.closed.pipe(map((closed) => this.handleDialogClose(closed, organization))),
|
||||
);
|
||||
}
|
||||
|
||||
private async handleInviteDialog() {
|
||||
private async handleInviteDialog(organization: Organization) {
|
||||
const billingMetadata = await firstValueFrom(this.billingMetadata$);
|
||||
const dialog = openUserAddEditDialog(this.dialogService, {
|
||||
data: {
|
||||
kind: "Add",
|
||||
organizationId: this.organization.id,
|
||||
organizationId: organization.id,
|
||||
allOrganizationUserEmails: this.dataSource.data?.map((user) => user.email) ?? [],
|
||||
occupiedSeatCount: this.occupiedSeatCount,
|
||||
isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone,
|
||||
occupiedSeatCount: billingMetadata?.organizationOccupiedSeats ?? 0,
|
||||
isOnSecretsManagerStandalone: billingMetadata?.isOnSecretsManagerStandalone ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
|
||||
if (result === MemberDialogResult.Saved) {
|
||||
await this.load();
|
||||
await this.load(organization);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSeatLimitForFixedTiers() {
|
||||
if (!this.organization.canEditSubscription) {
|
||||
await this.showSeatLimitReachedDialog();
|
||||
private async handleSeatLimitForFixedTiers(organization: Organization) {
|
||||
if (!organization.canEditSubscription) {
|
||||
await this.showSeatLimitReachedDialog(organization);
|
||||
return;
|
||||
}
|
||||
|
||||
const reference = openChangePlanDialog(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organization.id,
|
||||
subscription: null,
|
||||
productTierType: this.organization.productTierType,
|
||||
organizationId: organization.id,
|
||||
productTierType: organization.productTierType,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(reference.closed);
|
||||
|
||||
if (result === ChangePlanDialogResultType.Submitted) {
|
||||
await this.load();
|
||||
await this.load(organization);
|
||||
}
|
||||
}
|
||||
|
||||
async invite() {
|
||||
if (this.organization.hasReseller && this.organization.seats === this.occupiedSeatCount) {
|
||||
async invite(organization: Organization) {
|
||||
const billingMetadata = await firstValueFrom(this.billingMetadata$);
|
||||
if (
|
||||
organization.hasReseller &&
|
||||
organization.seats === billingMetadata?.organizationOccupiedSeats
|
||||
) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("seatLimitReached"),
|
||||
@@ -590,26 +608,31 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
}
|
||||
|
||||
if (
|
||||
this.occupiedSeatCount === this.organization.seats &&
|
||||
isFixedSeatPlan(this.organization.productTierType)
|
||||
billingMetadata?.organizationOccupiedSeats === organization.seats &&
|
||||
isFixedSeatPlan(organization.productTierType)
|
||||
) {
|
||||
await this.handleSeatLimitForFixedTiers();
|
||||
await this.handleSeatLimitForFixedTiers(organization);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.handleInviteDialog();
|
||||
await this.handleInviteDialog(organization);
|
||||
}
|
||||
|
||||
async edit(user: OrganizationUserView, initialTab: MemberDialogTab = MemberDialogTab.Role) {
|
||||
async edit(
|
||||
user: OrganizationUserView,
|
||||
organization: Organization,
|
||||
initialTab: MemberDialogTab = MemberDialogTab.Role,
|
||||
) {
|
||||
const billingMetadata = await firstValueFrom(this.billingMetadata$);
|
||||
const dialog = openUserAddEditDialog(this.dialogService, {
|
||||
data: {
|
||||
kind: "Edit",
|
||||
name: this.userNamePipe.transform(user),
|
||||
organizationId: this.organization.id,
|
||||
organizationId: organization.id,
|
||||
organizationUserId: user.id,
|
||||
usesKeyConnector: user.usesKeyConnector,
|
||||
isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone,
|
||||
isOnSecretsManagerStandalone: billingMetadata?.isOnSecretsManagerStandalone ?? false,
|
||||
initialTab: initialTab,
|
||||
managedByOrganization: user.managedByOrganization,
|
||||
},
|
||||
@@ -623,35 +646,35 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
case MemberDialogResult.Saved:
|
||||
case MemberDialogResult.Revoked:
|
||||
case MemberDialogResult.Restored:
|
||||
await this.load();
|
||||
await this.load(organization);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async bulkRemove() {
|
||||
async bulkRemove(organization: Organization) {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organization.id,
|
||||
organizationId: organization.id,
|
||||
users: this.dataSource.getCheckedUsers(),
|
||||
},
|
||||
});
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
await this.load();
|
||||
await this.load(organization);
|
||||
}
|
||||
|
||||
async bulkDelete() {
|
||||
async bulkDelete(organization: Organization) {
|
||||
const warningAcknowledged = await firstValueFrom(
|
||||
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
|
||||
this.deleteManagedMemberWarningService.warningAcknowledged(organization.id),
|
||||
);
|
||||
|
||||
if (
|
||||
!warningAcknowledged &&
|
||||
this.organization.canManageUsers &&
|
||||
this.organization.productTierType === ProductTierType.Enterprise
|
||||
organization.canManageUsers &&
|
||||
organization.productTierType === ProductTierType.Enterprise
|
||||
) {
|
||||
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
|
||||
if (!acknowledged) {
|
||||
@@ -665,38 +688,38 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
|
||||
const dialogRef = BulkDeleteDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organization.id,
|
||||
organizationId: organization.id,
|
||||
users: this.dataSource.getCheckedUsers(),
|
||||
},
|
||||
});
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
await this.load();
|
||||
await this.load(organization);
|
||||
}
|
||||
|
||||
async bulkRevoke() {
|
||||
await this.bulkRevokeOrRestore(true);
|
||||
async bulkRevoke(organization: Organization) {
|
||||
await this.bulkRevokeOrRestore(true, organization);
|
||||
}
|
||||
|
||||
async bulkRestore() {
|
||||
await this.bulkRevokeOrRestore(false);
|
||||
async bulkRestore(organization: Organization) {
|
||||
await this.bulkRevokeOrRestore(false, organization);
|
||||
}
|
||||
|
||||
async bulkRevokeOrRestore(isRevoking: boolean) {
|
||||
async bulkRevokeOrRestore(isRevoking: boolean, organization: Organization) {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ref = BulkRestoreRevokeComponent.open(this.dialogService, {
|
||||
organizationId: this.organization.id,
|
||||
organizationId: organization.id,
|
||||
users: this.dataSource.getCheckedUsers(),
|
||||
isRevoking: isRevoking,
|
||||
});
|
||||
|
||||
await firstValueFrom(ref.closed);
|
||||
await this.load();
|
||||
await this.load(organization);
|
||||
}
|
||||
|
||||
async bulkReinvite() {
|
||||
async bulkReinvite(organization: Organization) {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
@@ -715,7 +738,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
|
||||
try {
|
||||
const response = this.organizationUserApiService.postManyOrganizationUserReinvite(
|
||||
this.organization.id,
|
||||
organization.id,
|
||||
filteredUsers.map((user) => user.id),
|
||||
);
|
||||
// Bulk Status component open
|
||||
@@ -731,26 +754,26 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
this.actionPromise = undefined;
|
||||
}
|
||||
|
||||
async bulkConfirm() {
|
||||
async bulkConfirm(organization: Organization) {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
organization: this.organization,
|
||||
organization: organization,
|
||||
users: this.dataSource.getCheckedUsers(),
|
||||
},
|
||||
});
|
||||
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
await this.load();
|
||||
await this.load(organization);
|
||||
}
|
||||
|
||||
async bulkEnableSM() {
|
||||
async bulkEnableSM(organization: Organization) {
|
||||
const users = this.dataSource.getCheckedUsers().filter((ou) => !ou.accessSecretsManager);
|
||||
|
||||
if (users.length === 0) {
|
||||
@@ -763,20 +786,20 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
}
|
||||
|
||||
const dialogRef = BulkEnableSecretsManagerDialogComponent.open(this.dialogService, {
|
||||
orgId: this.organization.id,
|
||||
orgId: organization.id,
|
||||
users,
|
||||
});
|
||||
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
this.dataSource.uncheckAllUsers();
|
||||
await this.load();
|
||||
await this.load(organization);
|
||||
}
|
||||
|
||||
openEventsDialog(user: OrganizationUserView) {
|
||||
openEventsDialog(user: OrganizationUserView, organization: Organization) {
|
||||
openEntityEventsDialog(this.dialogService, {
|
||||
data: {
|
||||
name: this.userNamePipe.transform(user),
|
||||
organizationId: this.organization.id,
|
||||
organizationId: organization.id,
|
||||
entityId: user.id,
|
||||
showUser: false,
|
||||
entity: "user",
|
||||
@@ -784,7 +807,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
});
|
||||
}
|
||||
|
||||
async resetPassword(user: OrganizationUserView) {
|
||||
async resetPassword(user: OrganizationUserView, organization: Organization) {
|
||||
if (!user || !user.email || !user.id) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
@@ -800,14 +823,14 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
data: {
|
||||
name: this.userNamePipe.transform(user),
|
||||
email: user.email,
|
||||
organizationId: this.organization.id as OrganizationId,
|
||||
organizationId: organization.id as OrganizationId,
|
||||
organizationUserId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === AccountRecoveryDialogResultType.Ok) {
|
||||
await this.load();
|
||||
await this.load(organization);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -857,15 +880,15 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
return true;
|
||||
}
|
||||
|
||||
async deleteUser(user: OrganizationUserView) {
|
||||
async deleteUser(user: OrganizationUserView, organization: Organization) {
|
||||
const warningAcknowledged = await firstValueFrom(
|
||||
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
|
||||
this.deleteManagedMemberWarningService.warningAcknowledged(organization.id),
|
||||
);
|
||||
|
||||
if (
|
||||
!warningAcknowledged &&
|
||||
this.organization.canManageUsers &&
|
||||
this.organization.productTierType === ProductTierType.Enterprise
|
||||
organization.canManageUsers &&
|
||||
organization.productTierType === ProductTierType.Enterprise
|
||||
) {
|
||||
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
|
||||
if (!acknowledged) {
|
||||
@@ -891,24 +914,23 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organization.id);
|
||||
await this.deleteManagedMemberWarningService.acknowledgeWarning(organization.id);
|
||||
|
||||
this.actionPromise = this.organizationUserApiService.deleteOrganizationUser(
|
||||
this.organization.id,
|
||||
organization.id,
|
||||
user.id,
|
||||
);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("organizationUserDeleted", this.userNamePipe.transform(user)),
|
||||
});
|
||||
this.dataSource.removeUser(user);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
this.actionPromise = undefined;
|
||||
}
|
||||
|
||||
private async noMasterPasswordConfirmationDialog(user: OrganizationUserView) {
|
||||
@@ -952,12 +974,12 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
.every((member) => member.managedByOrganization && validStatuses.includes(member.status));
|
||||
}
|
||||
|
||||
async navigateToPaymentMethod() {
|
||||
async navigateToPaymentMethod(organization: Organization) {
|
||||
const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout,
|
||||
);
|
||||
const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method";
|
||||
await this.router.navigate(["organizations", `${this.organization?.id}`, "billing", route], {
|
||||
await this.router.navigate(["organizations", `${organization.id}`, "billing", route], {
|
||||
state: { launchPaymentModalAutomatically: true },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ import { PaymentComponent } from "../shared/payment/payment.component";
|
||||
|
||||
type ChangePlanDialogParams = {
|
||||
organizationId: string;
|
||||
subscription: OrganizationSubscriptionResponse;
|
||||
productTierType: ProductTierType;
|
||||
subscription?: OrganizationSubscriptionResponse;
|
||||
};
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
|
||||
@@ -170,6 +170,10 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
|
||||
}
|
||||
}
|
||||
|
||||
async invite() {
|
||||
await this.edit(null);
|
||||
}
|
||||
|
||||
async bulkRemove(): Promise<void> {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
|
||||
@@ -36,7 +36,7 @@ export class Policy extends Domain {
|
||||
return new Policy(new PolicyData(response));
|
||||
}
|
||||
|
||||
static fromListResponse(response: ListResponse<PolicyResponse>): Policy[] | undefined {
|
||||
return response.data?.map((d) => Policy.fromResponse(d)) ?? undefined;
|
||||
static fromListResponse(response: ListResponse<PolicyResponse>): Policy[] {
|
||||
return response.data.map((d) => Policy.fromResponse(d));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ResetPasswordPolicyOptions } from "../../models/domain/reset-password-p
|
||||
|
||||
import { POLICIES } from "./policy-state";
|
||||
|
||||
export function policyRecordToArray(policiesMap: { [id: string]: PolicyData }) {
|
||||
export function policyRecordToArray(policiesMap: { [id: string]: PolicyData }): Policy[] {
|
||||
return Object.values(policiesMap || {}).map((f) => new Policy(f));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user