mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +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 { Directive } from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { FormControl } from "@angular/forms";
|
import { FormControl } from "@angular/forms";
|
||||||
@@ -14,6 +12,7 @@ import {
|
|||||||
ProviderUserStatusType,
|
ProviderUserStatusType,
|
||||||
ProviderUserType,
|
ProviderUserType,
|
||||||
} from "@bitwarden/common/admin-console/enums";
|
} 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 { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response";
|
||||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
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>;
|
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.
|
* 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 searchControl = new FormControl("", { nonNullable: true });
|
||||||
protected statusToggle = new BehaviorSubject<StatusType | null>(null);
|
protected statusToggle = new BehaviorSubject<StatusType | undefined>(undefined);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected apiService: ApiService,
|
protected apiService: ApiService,
|
||||||
@@ -100,15 +99,20 @@ export abstract class BaseMembersComponent<UserView extends UserViewTypes> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract edit(user: UserView): void;
|
abstract edit(user: UserView, organization?: Organization): void;
|
||||||
abstract getUsers(): Promise<ListResponse<UserView> | UserView[]>;
|
abstract getUsers(organization?: Organization): Promise<ListResponse<UserView> | UserView[]>;
|
||||||
abstract removeUser(id: string): Promise<void>;
|
abstract removeUser(id: string, organization?: Organization): Promise<void>;
|
||||||
abstract reinviteUser(id: string): Promise<void>;
|
abstract reinviteUser(id: string, organization?: Organization): Promise<void>;
|
||||||
abstract confirmUser(user: UserView, publicKey: Uint8Array): 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
|
// 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
|
// GetUsers can return a ListResponse or an Array
|
||||||
if (response instanceof ListResponse) {
|
if (response instanceof ListResponse) {
|
||||||
@@ -120,10 +124,6 @@ export abstract class BaseMembersComponent<UserView extends UserViewTypes> {
|
|||||||
this.firstLoaded = true;
|
this.firstLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
invite() {
|
|
||||||
this.edit(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async removeUserConfirmationDialog(user: UserView) {
|
protected async removeUserConfirmationDialog(user: UserView) {
|
||||||
return this.dialogService.openSimpleDialog({
|
return this.dialogService.openSimpleDialog({
|
||||||
title: this.userNamePipe.transform(user),
|
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);
|
const confirmed = await this.removeUserConfirmationDialog(user);
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.actionPromise = this.removeUser(user.id);
|
this.actionPromise = this.removeUser(user.id, organization);
|
||||||
try {
|
try {
|
||||||
await this.actionPromise;
|
await this.actionPromise;
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
title: null,
|
|
||||||
message: this.i18nService.t("removedUserId", this.userNamePipe.transform(user)),
|
message: this.i18nService.t("removedUserId", this.userNamePipe.transform(user)),
|
||||||
});
|
});
|
||||||
this.dataSource.removeUser(user);
|
this.dataSource.removeUser(user);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.validationService.showError(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) {
|
if (this.actionPromise != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.actionPromise = this.reinviteUser(user.id);
|
this.actionPromise = this.reinviteUser(user.id, organization);
|
||||||
try {
|
try {
|
||||||
await this.actionPromise;
|
await this.actionPromise;
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
title: null,
|
|
||||||
message: this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user)),
|
message: this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user)),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.validationService.showError(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) => {
|
const confirmUser = async (publicKey: Uint8Array) => {
|
||||||
try {
|
try {
|
||||||
this.actionPromise = this.confirmUser(user, publicKey);
|
this.actionPromise = this.confirmUser(user, publicKey, organization);
|
||||||
await this.actionPromise;
|
await this.actionPromise;
|
||||||
user.status = this.userStatusType.Confirmed;
|
user.status = this.userStatusType.Confirmed;
|
||||||
this.dataSource.replaceUser(user);
|
this.dataSource.replaceUser(user);
|
||||||
|
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
title: null,
|
|
||||||
message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user)),
|
message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user)),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.validationService.showError(e);
|
this.validationService.showError(e);
|
||||||
throw e;
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
this.actionPromise = null;
|
this.actionPromise = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -204,11 +201,14 @@ export abstract class BaseMembersComponent<UserView extends UserViewTypes> {
|
|||||||
const autoConfirm = await firstValueFrom(
|
const autoConfirm = await firstValueFrom(
|
||||||
this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$,
|
this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$,
|
||||||
);
|
);
|
||||||
|
if (user == null) {
|
||||||
|
throw new Error("Cannot confirm null user.");
|
||||||
|
}
|
||||||
if (autoConfirm == null || !autoConfirm) {
|
if (autoConfirm == null || !autoConfirm) {
|
||||||
const dialogRef = UserConfirmComponent.open(this.dialogService, {
|
const dialogRef = UserConfirmComponent.open(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
name: this.userNamePipe.transform(user),
|
name: this.userNamePipe.transform(user),
|
||||||
userId: user != null ? user.userId : null,
|
userId: user.id,
|
||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
confirmUser: () => confirmUser(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).
|
* 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) {
|
if (status == null) {
|
||||||
return user.status != OrganizationUserStatusType.Revoked;
|
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);
|
return (user: UserViewTypes) => statusFilter(user, status) && textFilter(user, searchText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,466 +1,479 @@
|
|||||||
<app-organization-free-trial-warning
|
@let organization = this.organization();
|
||||||
[organization]="organization"
|
@if (organization) {
|
||||||
(clicked)="navigateToPaymentMethod()"
|
<app-organization-free-trial-warning
|
||||||
>
|
[organization]="organization"
|
||||||
</app-organization-free-trial-warning>
|
(clicked)="navigateToPaymentMethod(organization)"
|
||||||
<app-header>
|
|
||||||
<bit-search
|
|
||||||
class="tw-grow"
|
|
||||||
[formControl]="searchControl"
|
|
||||||
[placeholder]="'searchMembers' | i18n"
|
|
||||||
></bit-search>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitButton
|
|
||||||
buttonType="primary"
|
|
||||||
(click)="invite()"
|
|
||||||
[disabled]="!firstLoaded"
|
|
||||||
*ngIf="showUserManagementControls$ | async"
|
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
</app-organization-free-trial-warning>
|
||||||
{{ "inviteMember" | i18n }}
|
<app-header>
|
||||||
</button>
|
<bit-search
|
||||||
</app-header>
|
class="tw-grow"
|
||||||
|
[formControl]="searchControl"
|
||||||
|
[placeholder]="'searchMembers' | i18n"
|
||||||
|
></bit-search>
|
||||||
|
|
||||||
<div class="tw-mb-4 tw-flex tw-flex-col tw-space-y-4">
|
<button
|
||||||
<bit-toggle-group
|
type="button"
|
||||||
[selected]="status"
|
bitButton
|
||||||
(selectedChange)="statusToggle.next($event)"
|
buttonType="primary"
|
||||||
[attr.aria-label]="'memberStatusFilter' | i18n"
|
(click)="invite(organization)"
|
||||||
*ngIf="showUserManagementControls$ | async"
|
[disabled]="!firstLoaded"
|
||||||
>
|
*ngIf="showUserManagementControls()"
|
||||||
<bit-toggle [value]="null">
|
|
||||||
{{ "all" | i18n }}
|
|
||||||
<span bitBadge variant="info" *ngIf="dataSource.activeUserCount as allCount">{{
|
|
||||||
allCount
|
|
||||||
}}</span>
|
|
||||||
</bit-toggle>
|
|
||||||
|
|
||||||
<bit-toggle [value]="userStatusType.Invited">
|
|
||||||
{{ "invited" | i18n }}
|
|
||||||
<span bitBadge variant="info" *ngIf="dataSource.invitedUserCount as invitedCount">{{
|
|
||||||
invitedCount
|
|
||||||
}}</span>
|
|
||||||
</bit-toggle>
|
|
||||||
|
|
||||||
<bit-toggle [value]="userStatusType.Accepted">
|
|
||||||
{{ "needsConfirmation" | i18n }}
|
|
||||||
<span bitBadge variant="info" *ngIf="dataSource.acceptedUserCount as acceptedUserCount">{{
|
|
||||||
acceptedUserCount
|
|
||||||
}}</span>
|
|
||||||
</bit-toggle>
|
|
||||||
|
|
||||||
<bit-toggle [value]="userStatusType.Revoked">
|
|
||||||
{{ "revoked" | i18n }}
|
|
||||||
<span bitBadge variant="info" *ngIf="dataSource.revokedUserCount as revokedCount">{{
|
|
||||||
revokedCount
|
|
||||||
}}</span>
|
|
||||||
</bit-toggle>
|
|
||||||
</bit-toggle-group>
|
|
||||||
</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">
|
|
||||||
<p *ngIf="!dataSource.filteredData.length">{{ "noMembersInList" | i18n }}</p>
|
|
||||||
<ng-container *ngIf="dataSource.filteredData.length">
|
|
||||||
<bit-callout
|
|
||||||
type="info"
|
|
||||||
title="{{ 'confirmUsers' | i18n }}"
|
|
||||||
icon="bwi-check-circle"
|
|
||||||
*ngIf="showConfirmUsers"
|
|
||||||
>
|
>
|
||||||
{{ "usersNeedConfirmed" | i18n }}
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
</bit-callout>
|
{{ "inviteMember" | i18n }}
|
||||||
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
|
</button>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<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()"
|
||||||
|
>
|
||||||
|
<bit-toggle [value]="null">
|
||||||
|
{{ "all" | i18n }}
|
||||||
|
<span bitBadge variant="info" *ngIf="dataSource.activeUserCount as allCount">{{
|
||||||
|
allCount
|
||||||
|
}}</span>
|
||||||
|
</bit-toggle>
|
||||||
|
|
||||||
|
<bit-toggle [value]="userStatusType.Invited">
|
||||||
|
{{ "invited" | i18n }}
|
||||||
|
<span bitBadge variant="info" *ngIf="dataSource.invitedUserCount as invitedCount">{{
|
||||||
|
invitedCount
|
||||||
|
}}</span>
|
||||||
|
</bit-toggle>
|
||||||
|
|
||||||
|
<bit-toggle [value]="userStatusType.Accepted">
|
||||||
|
{{ "needsConfirmation" | i18n }}
|
||||||
|
<span bitBadge variant="info" *ngIf="dataSource.acceptedUserCount as acceptedUserCount">{{
|
||||||
|
acceptedUserCount
|
||||||
|
}}</span>
|
||||||
|
</bit-toggle>
|
||||||
|
|
||||||
|
<bit-toggle [value]="userStatusType.Revoked">
|
||||||
|
{{ "revoked" | i18n }}
|
||||||
|
<span bitBadge variant="info" *ngIf="dataSource.revokedUserCount as revokedCount">{{
|
||||||
|
revokedCount
|
||||||
|
}}</span>
|
||||||
|
</bit-toggle>
|
||||||
|
</bit-toggle-group>
|
||||||
|
</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">
|
||||||
|
<p *ngIf="!dataSource.filteredData.length">{{ "noMembersInList" | i18n }}</p>
|
||||||
|
<ng-container *ngIf="dataSource.filteredData.length">
|
||||||
|
<bit-callout
|
||||||
|
type="info"
|
||||||
|
title="{{ 'confirmUsers' | i18n }}"
|
||||||
|
icon="bwi-check-circle"
|
||||||
|
*ngIf="showConfirmUsers"
|
||||||
|
>
|
||||||
|
{{ "usersNeedConfirmed" | i18n }}
|
||||||
|
</bit-callout>
|
||||||
|
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
|
||||||
from overflowing the <main> element. -->
|
from overflowing the <main> element. -->
|
||||||
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
|
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
|
||||||
<bit-table [dataSource]="dataSource">
|
<bit-table [dataSource]="dataSource">
|
||||||
<ng-container header>
|
<ng-container header>
|
||||||
<tr>
|
<tr>
|
||||||
<th bitCell class="tw-w-20" *ngIf="showUserManagementControls$ | async">
|
<th bitCell class="tw-w-20" *ngIf="showUserManagementControls()">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
bitCheckbox
|
bitCheckbox
|
||||||
class="tw-mr-1"
|
class="tw-mr-1"
|
||||||
(change)="dataSource.checkAllFilteredUsers($any($event.target).checked)"
|
(change)="dataSource.checkAllFilteredUsers($any($event.target).checked)"
|
||||||
id="selectAll"
|
id="selectAll"
|
||||||
/>
|
/>
|
||||||
<label class="tw-mb-0 !tw-font-bold !tw-text-muted" for="selectAll">{{
|
<label class="tw-mb-0 !tw-font-bold !tw-text-muted" for="selectAll">{{
|
||||||
"all" | i18n
|
"all" | i18n
|
||||||
}}</label>
|
}}</label>
|
||||||
</th>
|
</th>
|
||||||
<th bitCell bitSortable="email" default>{{ "name" | i18n }}</th>
|
<th bitCell bitSortable="email" default>{{ "name" | i18n }}</th>
|
||||||
<th bitCell>{{ (organization.useGroups ? "groups" : "collections") | i18n }}</th>
|
<th bitCell>{{ (organization.useGroups ? "groups" : "collections") | i18n }}</th>
|
||||||
<th bitCell bitSortable="type">{{ "role" | i18n }}</th>
|
<th bitCell bitSortable="type">{{ "role" | i18n }}</th>
|
||||||
<th bitCell>{{ "policies" | i18n }}</th>
|
<th bitCell>{{ "policies" | i18n }}</th>
|
||||||
<th bitCell class="tw-w-10">
|
<th bitCell class="tw-w-10">
|
||||||
<button
|
<button
|
||||||
[bitMenuTriggerFor]="headerMenu"
|
[bitMenuTriggerFor]="headerMenu"
|
||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-ellipsis-v"
|
bitIconButton="bwi-ellipsis-v"
|
||||||
size="small"
|
size="small"
|
||||||
label="{{ 'options' | i18n }}"
|
label="{{ 'options' | i18n }}"
|
||||||
*ngIf="showUserManagementControls$ | async"
|
*ngIf="showUserManagementControls()"
|
||||||
></button>
|
></button>
|
||||||
|
|
||||||
<bit-menu #headerMenu>
|
<bit-menu #headerMenu>
|
||||||
<ng-container *ngIf="canUseSecretsManager$ | async">
|
<ng-container *ngIf="canUseSecretsManager()">
|
||||||
<button type="button" bitMenuItem (click)="bulkEnableSM()">
|
<button type="button" bitMenuItem (click)="bulkEnableSM(organization)">
|
||||||
{{ "activateSecretsManager" | i18n }}
|
{{ "activateSecretsManager" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<bit-menu-divider></bit-menu-divider>
|
<bit-menu-divider></bit-menu-divider>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitMenuItem
|
|
||||||
(click)="bulkReinvite()"
|
|
||||||
*ngIf="showBulkReinviteUsers"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
|
||||||
{{ "reinviteSelected" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitMenuItem
|
|
||||||
(click)="bulkConfirm()"
|
|
||||||
*ngIf="showBulkConfirmUsers"
|
|
||||||
>
|
|
||||||
<span class="tw-text-success">
|
|
||||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
|
||||||
{{ "confirmSelected" | i18n }}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitMenuItem
|
|
||||||
(click)="bulkRestore()"
|
|
||||||
*ngIf="showBulkRestoreUsers"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
|
||||||
{{ "restoreAccess" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitMenuItem
|
|
||||||
(click)="bulkRevoke()"
|
|
||||||
*ngIf="showBulkRevokeUsers"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
|
|
||||||
{{ "revokeAccess" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitMenuItem
|
|
||||||
(click)="bulkRemove()"
|
|
||||||
*ngIf="showBulkRemoveUsers"
|
|
||||||
>
|
|
||||||
<span class="tw-text-danger">
|
|
||||||
<i aria-hidden="true" class="bwi bwi-fw bwi-close"></i>
|
|
||||||
{{ "remove" | i18n }}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitMenuItem
|
|
||||||
(click)="bulkDelete()"
|
|
||||||
*ngIf="showBulkDeleteUsers"
|
|
||||||
>
|
|
||||||
<span class="tw-text-danger">
|
|
||||||
<i aria-hidden="true" class="bwi bwi-fw bwi-trash"></i>
|
|
||||||
{{ "delete" | i18n }}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</bit-menu>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</ng-container>
|
|
||||||
<ng-template body let-rows$>
|
|
||||||
<tr
|
|
||||||
bitRow
|
|
||||||
*cdkVirtualFor="let u of rows$"
|
|
||||||
alignContent="middle"
|
|
||||||
[ngClass]="rowHeightClass"
|
|
||||||
>
|
|
||||||
<td
|
|
||||||
bitCell
|
|
||||||
(click)="dataSource.checkUser(u)"
|
|
||||||
*ngIf="showUserManagementControls$ | async"
|
|
||||||
>
|
|
||||||
<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">
|
|
||||||
<div class="tw-flex tw-items-center">
|
|
||||||
<bit-avatar
|
|
||||||
size="small"
|
|
||||||
[text]="u | userName"
|
|
||||||
[id]="u.userId"
|
|
||||||
[color]="u.avatarColor"
|
|
||||||
class="tw-mr-3"
|
|
||||||
></bit-avatar>
|
|
||||||
<div class="tw-flex tw-flex-col">
|
|
||||||
<div class="tw-flex tw-flex-row tw-gap-2">
|
|
||||||
<button type="button" bitLink>
|
|
||||||
{{ u.name ?? u.email }}
|
|
||||||
</button>
|
|
||||||
<span
|
|
||||||
bitBadge
|
|
||||||
class="tw-text-xs"
|
|
||||||
variant="secondary"
|
|
||||||
*ngIf="u.status === userStatusType.Invited"
|
|
||||||
>
|
|
||||||
{{ "invited" | i18n }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
bitBadge
|
|
||||||
class="tw-text-xs"
|
|
||||||
variant="warning"
|
|
||||||
*ngIf="u.status === userStatusType.Accepted"
|
|
||||||
>
|
|
||||||
{{ "needsConfirmation" | i18n }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
bitBadge
|
|
||||||
class="tw-text-xs"
|
|
||||||
variant="secondary"
|
|
||||||
*ngIf="u.status === userStatusType.Revoked"
|
|
||||||
>
|
|
||||||
{{ "revoked" | i18n }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
|
|
||||||
{{ u.email }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
<ng-template #readOnlyUserInfo>
|
|
||||||
<td bitCell>
|
|
||||||
<div class="tw-flex tw-items-center">
|
|
||||||
<bit-avatar
|
|
||||||
size="small"
|
|
||||||
[text]="u | userName"
|
|
||||||
[id]="u.userId"
|
|
||||||
[color]="u.avatarColor"
|
|
||||||
class="tw-mr-3"
|
|
||||||
></bit-avatar>
|
|
||||||
<div class="tw-flex tw-flex-col">
|
|
||||||
<div class="tw-flex tw-flex-row tw-gap-2">
|
|
||||||
<span>{{ u.name ?? u.email }}</span>
|
|
||||||
<span
|
|
||||||
bitBadge
|
|
||||||
class="tw-text-xs"
|
|
||||||
variant="secondary"
|
|
||||||
*ngIf="u.status === userStatusType.Invited"
|
|
||||||
>
|
|
||||||
{{ "invited" | i18n }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
bitBadge
|
|
||||||
class="tw-text-xs"
|
|
||||||
variant="warning"
|
|
||||||
*ngIf="u.status === userStatusType.Accepted"
|
|
||||||
>
|
|
||||||
{{ "needsConfirmation" | i18n }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
bitBadge
|
|
||||||
class="tw-text-xs"
|
|
||||||
variant="secondary"
|
|
||||||
*ngIf="u.status === userStatusType.Revoked"
|
|
||||||
>
|
|
||||||
{{ "revoked" | i18n }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
|
|
||||||
{{ u.email }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-container *ngIf="showUserManagementControls$ | async; else readOnlyGroupsCell">
|
|
||||||
<td
|
|
||||||
bitCell
|
|
||||||
(click)="edit(u, organization.useGroups ? memberTab.Groups : memberTab.Collections)"
|
|
||||||
class="tw-cursor-pointer"
|
|
||||||
>
|
|
||||||
<bit-badge-list
|
|
||||||
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
|
|
||||||
[maxItems]="3"
|
|
||||||
variant="secondary"
|
|
||||||
></bit-badge-list>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
<ng-template #readOnlyGroupsCell>
|
|
||||||
<td bitCell>
|
|
||||||
<bit-badge-list
|
|
||||||
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
|
|
||||||
[maxItems]="3"
|
|
||||||
variant="secondary"
|
|
||||||
></bit-badge-list>
|
|
||||||
</td>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-container *ngIf="showUserManagementControls$ | async; else readOnlyRoleCell">
|
|
||||||
<td
|
|
||||||
bitCell
|
|
||||||
(click)="edit(u, memberTab.Role)"
|
|
||||||
class="tw-cursor-pointer tw-text-sm tw-text-muted"
|
|
||||||
>
|
|
||||||
{{ u.type | userType }}
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
<ng-template #readOnlyRoleCell>
|
|
||||||
<td bitCell class="tw-text-sm tw-text-muted">
|
|
||||||
{{ u.type | userType }}
|
|
||||||
</td>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<td bitCell class="tw-text-muted tw-space-x-2">
|
|
||||||
<ng-container *ngIf="u.twoFactorEnabled">
|
|
||||||
<i
|
|
||||||
class="bwi bwi-lock"
|
|
||||||
title="{{ 'userUsingTwoStep' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
<span class="tw-sr-only">{{ "userUsingTwoStep" | i18n }}</span>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="showEnrolledStatus($any(u))">
|
|
||||||
<i
|
|
||||||
class="bwi bwi-key"
|
|
||||||
title="{{ 'enrolledAccountRecovery' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
<span class="tw-sr-only">{{ "enrolledAccountRecovery" | i18n }}</span>
|
|
||||||
</ng-container>
|
|
||||||
</td>
|
|
||||||
<td bitCell>
|
|
||||||
<button
|
|
||||||
[bitMenuTriggerFor]="rowMenu"
|
|
||||||
type="button"
|
|
||||||
bitIconButton="bwi-ellipsis-v"
|
|
||||||
size="small"
|
|
||||||
label="{{ 'options' | i18n }}"
|
|
||||||
></button>
|
|
||||||
|
|
||||||
<bit-menu #rowMenu>
|
|
||||||
<ng-container *ngIf="showUserManagementControls$ | async">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitMenuItem
|
bitMenuItem
|
||||||
(click)="reinvite(u)"
|
(click)="bulkReinvite(organization)"
|
||||||
*ngIf="u.status === userStatusType.Invited"
|
*ngIf="showBulkReinviteUsers"
|
||||||
>
|
>
|
||||||
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||||
{{ "resendInvitation" | i18n }}
|
{{ "reinviteSelected" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitMenuItem
|
bitMenuItem
|
||||||
(click)="confirm(u)"
|
(click)="bulkConfirm(organization)"
|
||||||
*ngIf="u.status === userStatusType.Accepted"
|
*ngIf="showBulkConfirmUsers"
|
||||||
>
|
>
|
||||||
<span class="tw-text-success">
|
<span class="tw-text-success">
|
||||||
<i aria-hidden="true" class="bwi bwi-check"></i> {{ "confirm" | i18n }}
|
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||||
|
{{ "confirmSelected" | i18n }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<bit-menu-divider
|
|
||||||
*ngIf="
|
|
||||||
u.status === userStatusType.Accepted || u.status === userStatusType.Invited
|
|
||||||
"
|
|
||||||
></bit-menu-divider>
|
|
||||||
<button type="button" bitMenuItem (click)="edit(u, memberTab.Role)">
|
|
||||||
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "memberRole" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitMenuItem
|
bitMenuItem
|
||||||
(click)="edit(u, memberTab.Groups)"
|
(click)="bulkRestore(organization)"
|
||||||
*ngIf="organization.useGroups"
|
*ngIf="showBulkRestoreUsers"
|
||||||
>
|
>
|
||||||
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "groups" | i18n }}
|
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||||
</button>
|
|
||||||
<button type="button" bitMenuItem (click)="edit(u, memberTab.Collections)">
|
|
||||||
<i aria-hidden="true" class="bwi bwi-collection-shared"></i>
|
|
||||||
{{ "collections" | i18n }}
|
|
||||||
</button>
|
|
||||||
<bit-menu-divider></bit-menu-divider>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitMenuItem
|
|
||||||
(click)="openEventsDialog(u)"
|
|
||||||
*ngIf="organization.useEvents && u.status === userStatusType.Confirmed"
|
|
||||||
>
|
|
||||||
<i aria-hidden="true" class="bwi bwi-file-text"></i> {{ "eventLogs" | i18n }}
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Account recovery is available to all users with appropriate permissions -->
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitMenuItem
|
|
||||||
(click)="resetPassword(u)"
|
|
||||||
*ngIf="allowResetPassword(u)"
|
|
||||||
>
|
|
||||||
<i aria-hidden="true" class="bwi bwi-key"></i> {{ "recoverAccount" | i18n }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<ng-container *ngIf="showUserManagementControls$ | async">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitMenuItem
|
|
||||||
(click)="restore(u)"
|
|
||||||
*ngIf="u.status === userStatusType.Revoked"
|
|
||||||
>
|
|
||||||
<i aria-hidden="true" class="bwi bwi-plus-circle"></i>
|
|
||||||
{{ "restoreAccess" | i18n }}
|
{{ "restoreAccess" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitMenuItem
|
bitMenuItem
|
||||||
(click)="revoke(u)"
|
(click)="bulkRevoke(organization)"
|
||||||
*ngIf="u.status !== userStatusType.Revoked"
|
*ngIf="showBulkRevokeUsers"
|
||||||
>
|
>
|
||||||
<i aria-hidden="true" class="bwi bwi-minus-circle"></i>
|
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
|
||||||
{{ "revokeAccess" | i18n }}
|
{{ "revokeAccess" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
*ngIf="!u.managedByOrganization"
|
|
||||||
type="button"
|
type="button"
|
||||||
bitMenuItem
|
bitMenuItem
|
||||||
(click)="remove(u)"
|
(click)="bulkRemove(organization)"
|
||||||
|
*ngIf="showBulkRemoveUsers"
|
||||||
>
|
>
|
||||||
<span class="tw-text-danger">
|
<span class="tw-text-danger">
|
||||||
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
|
<i aria-hidden="true" class="bwi bwi-fw bwi-close"></i>
|
||||||
|
{{ "remove" | i18n }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
*ngIf="u.managedByOrganization"
|
|
||||||
type="button"
|
type="button"
|
||||||
bitMenuItem
|
bitMenuItem
|
||||||
(click)="deleteUser(u)"
|
(click)="bulkDelete(organization)"
|
||||||
|
*ngIf="showBulkDeleteUsers"
|
||||||
>
|
>
|
||||||
<span class="tw-text-danger">
|
<span class="tw-text-danger">
|
||||||
<i class="bwi bwi-trash" aria-hidden="true"></i>
|
<i aria-hidden="true" class="bwi bwi-fw bwi-trash"></i>
|
||||||
{{ "delete" | i18n }}
|
{{ "delete" | i18n }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
</bit-menu>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template body let-rows$>
|
||||||
|
<tr
|
||||||
|
bitRow
|
||||||
|
*cdkVirtualFor="let u of rows$"
|
||||||
|
alignContent="middle"
|
||||||
|
[ngClass]="rowHeightClass"
|
||||||
|
>
|
||||||
|
<td bitCell (click)="dataSource.checkUser(u)" *ngIf="showUserManagementControls()">
|
||||||
|
<input type="checkbox" bitCheckbox [(ngModel)]="$any(u).checked" />
|
||||||
|
</td>
|
||||||
|
<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"
|
||||||
|
[text]="u | userName"
|
||||||
|
[id]="u.userId"
|
||||||
|
[color]="u.avatarColor"
|
||||||
|
class="tw-mr-3"
|
||||||
|
></bit-avatar>
|
||||||
|
<div class="tw-flex tw-flex-col">
|
||||||
|
<div class="tw-flex tw-flex-row tw-gap-2">
|
||||||
|
<button type="button" bitLink>
|
||||||
|
{{ u.name ?? u.email }}
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
bitBadge
|
||||||
|
class="tw-text-xs"
|
||||||
|
variant="secondary"
|
||||||
|
*ngIf="u.status === userStatusType.Invited"
|
||||||
|
>
|
||||||
|
{{ "invited" | i18n }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
bitBadge
|
||||||
|
class="tw-text-xs"
|
||||||
|
variant="warning"
|
||||||
|
*ngIf="u.status === userStatusType.Accepted"
|
||||||
|
>
|
||||||
|
{{ "needsConfirmation" | i18n }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
bitBadge
|
||||||
|
class="tw-text-xs"
|
||||||
|
variant="secondary"
|
||||||
|
*ngIf="u.status === userStatusType.Revoked"
|
||||||
|
>
|
||||||
|
{{ "revoked" | i18n }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
|
||||||
|
{{ u.email }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #readOnlyUserInfo>
|
||||||
|
<td bitCell>
|
||||||
|
<div class="tw-flex tw-items-center">
|
||||||
|
<bit-avatar
|
||||||
|
size="small"
|
||||||
|
[text]="u | userName"
|
||||||
|
[id]="u.userId"
|
||||||
|
[color]="u.avatarColor"
|
||||||
|
class="tw-mr-3"
|
||||||
|
></bit-avatar>
|
||||||
|
<div class="tw-flex tw-flex-col">
|
||||||
|
<div class="tw-flex tw-flex-row tw-gap-2">
|
||||||
|
<span>{{ u.name ?? u.email }}</span>
|
||||||
|
<span
|
||||||
|
bitBadge
|
||||||
|
class="tw-text-xs"
|
||||||
|
variant="secondary"
|
||||||
|
*ngIf="u.status === userStatusType.Invited"
|
||||||
|
>
|
||||||
|
{{ "invited" | i18n }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
bitBadge
|
||||||
|
class="tw-text-xs"
|
||||||
|
variant="warning"
|
||||||
|
*ngIf="u.status === userStatusType.Accepted"
|
||||||
|
>
|
||||||
|
{{ "needsConfirmation" | i18n }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
bitBadge
|
||||||
|
class="tw-text-xs"
|
||||||
|
variant="secondary"
|
||||||
|
*ngIf="u.status === userStatusType.Revoked"
|
||||||
|
>
|
||||||
|
{{ "revoked" | i18n }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
|
||||||
|
{{ u.email }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-container *ngIf="showUserManagementControls(); else readOnlyGroupsCell">
|
||||||
|
<td
|
||||||
|
bitCell
|
||||||
|
(click)="
|
||||||
|
edit(
|
||||||
|
u,
|
||||||
|
organization,
|
||||||
|
organization.useGroups ? memberTab.Groups : memberTab.Collections
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="tw-cursor-pointer"
|
||||||
|
>
|
||||||
|
<bit-badge-list
|
||||||
|
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
|
||||||
|
[maxItems]="3"
|
||||||
|
variant="secondary"
|
||||||
|
></bit-badge-list>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #readOnlyGroupsCell>
|
||||||
|
<td bitCell>
|
||||||
|
<bit-badge-list
|
||||||
|
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
|
||||||
|
[maxItems]="3"
|
||||||
|
variant="secondary"
|
||||||
|
></bit-badge-list>
|
||||||
|
</td>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-container *ngIf="showUserManagementControls(); else readOnlyRoleCell">
|
||||||
|
<td
|
||||||
|
bitCell
|
||||||
|
(click)="edit(u, organization, memberTab.Role)"
|
||||||
|
class="tw-cursor-pointer tw-text-sm tw-text-muted"
|
||||||
|
>
|
||||||
|
{{ u.type | userType }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #readOnlyRoleCell>
|
||||||
|
<td bitCell class="tw-text-sm tw-text-muted">
|
||||||
|
{{ u.type | userType }}
|
||||||
|
</td>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<td bitCell class="tw-text-muted">
|
||||||
|
<ng-container *ngIf="u.twoFactorEnabled">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-lock"
|
||||||
|
title="{{ 'userUsingTwoStep' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="tw-sr-only">{{ "userUsingTwoStep" | i18n }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</bit-menu>
|
<ng-container *ngIf="showEnrolledStatus($any(u), organization)">
|
||||||
</td>
|
<i
|
||||||
</tr>
|
class="bwi bwi-key"
|
||||||
</ng-template>
|
title="{{ 'enrolledAccountRecovery' | i18n }}"
|
||||||
</bit-table>
|
aria-hidden="true"
|
||||||
</cdk-virtual-scroll-viewport>
|
></i>
|
||||||
|
<span class="tw-sr-only">{{ "enrolledAccountRecovery" | i18n }}</span>
|
||||||
|
</ng-container>
|
||||||
|
</td>
|
||||||
|
<td bitCell>
|
||||||
|
<button
|
||||||
|
[bitMenuTriggerFor]="rowMenu"
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-ellipsis-v"
|
||||||
|
size="small"
|
||||||
|
label="{{ 'options' | i18n }}"
|
||||||
|
></button>
|
||||||
|
|
||||||
|
<bit-menu #rowMenu>
|
||||||
|
<ng-container *ngIf="showUserManagementControls()">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="reinvite(u, organization)"
|
||||||
|
*ngIf="u.status === userStatusType.Invited"
|
||||||
|
>
|
||||||
|
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
||||||
|
{{ "resendInvitation" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="confirm(u, organization)"
|
||||||
|
*ngIf="u.status === userStatusType.Accepted"
|
||||||
|
>
|
||||||
|
<span class="tw-text-success">
|
||||||
|
<i aria-hidden="true" class="bwi bwi-check"></i> {{ "confirm" | i18n }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<bit-menu-divider
|
||||||
|
*ngIf="
|
||||||
|
u.status === userStatusType.Accepted || u.status === userStatusType.Invited
|
||||||
|
"
|
||||||
|
></bit-menu-divider>
|
||||||
|
<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, 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, organization, memberTab.Collections)"
|
||||||
|
>
|
||||||
|
<i aria-hidden="true" class="bwi bwi-collection-shared"></i>
|
||||||
|
{{ "collections" | i18n }}
|
||||||
|
</button>
|
||||||
|
<bit-menu-divider></bit-menu-divider>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="openEventsDialog(u, organization)"
|
||||||
|
*ngIf="organization.useEvents && u.status === userStatusType.Confirmed"
|
||||||
|
>
|
||||||
|
<i aria-hidden="true" class="bwi bwi-file-text"></i> {{ "eventLogs" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Account recovery is available to all users with appropriate permissions -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="resetPassword(u, organization)"
|
||||||
|
*ngIf="allowResetPassword(u, organization)"
|
||||||
|
>
|
||||||
|
<i aria-hidden="true" class="bwi bwi-key"></i> {{ "recoverAccount" | i18n }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ng-container *ngIf="showUserManagementControls()">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="restore(u, organization)"
|
||||||
|
*ngIf="u.status === userStatusType.Revoked"
|
||||||
|
>
|
||||||
|
<i aria-hidden="true" class="bwi bwi-plus-circle"></i>
|
||||||
|
{{ "restoreAccess" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="revoke(u, organization)"
|
||||||
|
*ngIf="u.status !== userStatusType.Revoked"
|
||||||
|
>
|
||||||
|
<i aria-hidden="true" class="bwi bwi-minus-circle"></i>
|
||||||
|
{{ "revokeAccess" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="!u.managedByOrganization"
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="remove(u, organization)"
|
||||||
|
>
|
||||||
|
<span class="tw-text-danger">
|
||||||
|
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="u.managedByOrganization"
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="deleteUser(u, organization)"
|
||||||
|
>
|
||||||
|
<span class="tw-text-danger">
|
||||||
|
<i class="bwi bwi-trash" aria-hidden="true"></i>
|
||||||
|
{{ "delete" | i18n }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</bit-menu>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</bit-table>
|
||||||
|
</cdk-virtual-scroll-viewport>
|
||||||
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
import { Component, computed, Signal } from "@angular/core";
|
||||||
// @ts-strict-ignore
|
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
|
||||||
import { Component } from "@angular/core";
|
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import {
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
concatMap,
|
concatMap,
|
||||||
|
filter,
|
||||||
firstValueFrom,
|
firstValueFrom,
|
||||||
from,
|
from,
|
||||||
lastValueFrom,
|
lastValueFrom,
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Observable,
|
Observable,
|
||||||
shareReplay,
|
shareReplay,
|
||||||
switchMap,
|
switchMap,
|
||||||
|
take,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -47,13 +48,14 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
|||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||||
import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/enums";
|
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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
|
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
@@ -102,23 +104,24 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
memberTab = MemberDialogTab;
|
memberTab = MemberDialogTab;
|
||||||
protected dataSource = new MembersTableDataSource();
|
protected dataSource = new MembersTableDataSource();
|
||||||
|
|
||||||
organization: Organization;
|
organization: Signal<Organization | undefined>;
|
||||||
status: OrganizationUserStatusType = null;
|
status: OrganizationUserStatusType | undefined;
|
||||||
orgResetPasswordPolicyEnabled = false;
|
orgResetPasswordPolicyEnabled = false;
|
||||||
orgIsOnSecretsManagerStandalone = false;
|
|
||||||
|
|
||||||
protected canUseSecretsManager$: Observable<boolean>;
|
protected canUseSecretsManager: Signal<boolean> = computed(
|
||||||
protected showUserManagementControls$: Observable<boolean>;
|
() => 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
|
// Fixed sizes used for cdkVirtualScroll
|
||||||
protected rowHeight = 66;
|
protected rowHeight = 66;
|
||||||
protected rowHeightClass = `tw-h-[66px]`;
|
protected rowHeightClass = `tw-h-[66px]`;
|
||||||
|
|
||||||
private organizationUsersCount = 0;
|
private userId$: Observable<UserId> = this.accountService.activeAccount$.pipe(getUserId);
|
||||||
|
|
||||||
get occupiedSeatCount(): number {
|
|
||||||
return this.organizationUsersCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
apiService: ApiService,
|
apiService: ApiService,
|
||||||
@@ -162,61 +165,58 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
|
|
||||||
const organization$ = this.route.params.pipe(
|
const organization$ = this.route.params.pipe(
|
||||||
concatMap((params) =>
|
concatMap((params) =>
|
||||||
this.accountService.activeAccount$.pipe(
|
this.userId$.pipe(
|
||||||
switchMap((account) =>
|
switchMap((userId) =>
|
||||||
this.organizationService
|
this.organizationService
|
||||||
.organizations$(account?.id)
|
.organizations$(userId)
|
||||||
.pipe(getOrganizationById(params.organizationId)),
|
.pipe(getOrganizationById(params.organizationId)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
filter((organization): organization is Organization => organization != null),
|
||||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.canUseSecretsManager$ = organization$.pipe(map((org) => org.useSecretsManager));
|
this.organization = toSignal(organization$);
|
||||||
|
|
||||||
const policies$ = combineLatest([
|
const policies$ = combineLatest([this.userId$, organization$]).pipe(
|
||||||
this.accountService.activeAccount$.pipe(getUserId),
|
switchMap(([userId, organization]) =>
|
||||||
organization$,
|
organization.isProviderUser
|
||||||
]).pipe(
|
? from(this.policyApiService.getPolicies(organization.id)).pipe(
|
||||||
switchMap(([userId, organization]) => {
|
map((response) => Policy.fromListResponse(response)),
|
||||||
if (organization.isProviderUser) {
|
)
|
||||||
return from(this.policyApiService.getPolicies(organization.id)).pipe(
|
: this.policyService.policies$(userId),
|
||||||
map((response) => Policy.fromListResponse(response)),
|
),
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.policyService.policies$(userId);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
combineLatest([this.route.queryParams, policies$, organization$])
|
combineLatest([this.route.queryParams, policies$, organization$])
|
||||||
.pipe(
|
.pipe(
|
||||||
concatMap(async ([qParams, policies, organization]) => {
|
concatMap(async ([qParams, policies, organization]) => {
|
||||||
this.organization = organization;
|
|
||||||
|
|
||||||
// Backfill pub/priv key if necessary
|
// Backfill pub/priv key if necessary
|
||||||
if (
|
if (organization.canManageUsersPassword && !organization.hasPublicAndPrivateKeys) {
|
||||||
this.organization.canManageUsersPassword &&
|
|
||||||
!this.organization.hasPublicAndPrivateKeys
|
|
||||||
) {
|
|
||||||
const orgShareKey = await firstValueFrom(
|
const orgShareKey = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(
|
this.userId$.pipe(
|
||||||
getUserId,
|
|
||||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
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 [orgPublicKey, encryptedOrgPrivateKey] =
|
||||||
const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
|
await this.keyService.makeKeyPair(orgShareKey);
|
||||||
const response = await this.organizationApiService.updateKeys(
|
if (encryptedOrgPrivateKey.encryptedString == null) {
|
||||||
this.organization.id,
|
throw new Error("Encrypted private key is null.");
|
||||||
request,
|
}
|
||||||
|
const request = new OrganizationKeysRequest(
|
||||||
|
orgPublicKey,
|
||||||
|
encryptedOrgPrivateKey.encryptedString,
|
||||||
);
|
);
|
||||||
|
const response = await this.organizationApiService.updateKeys(organization.id, request);
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
this.organization.hasPublicAndPrivateKeys =
|
|
||||||
response.publicKey != null && response.privateKey != null;
|
|
||||||
await this.syncService.fullSync(true); // Replace organizations with new data
|
await this.syncService.fullSync(true); // Replace organizations with new data
|
||||||
} else {
|
} else {
|
||||||
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
||||||
@@ -225,24 +225,17 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
|
|
||||||
const resetPasswordPolicy = policies
|
const resetPasswordPolicy = policies
|
||||||
.filter((policy) => policy.type === PolicyType.ResetPassword)
|
.filter((policy) => policy.type === PolicyType.ResetPassword)
|
||||||
.find((p) => p.organizationId === this.organization.id);
|
.find((p) => p.organizationId === organization.id);
|
||||||
this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled;
|
this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled ?? false;
|
||||||
|
|
||||||
const billingMetadata = await this.billingApiService.getOrganizationBillingMetadata(
|
await this.load(organization);
|
||||||
this.organization.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.orgIsOnSecretsManagerStandalone = billingMetadata.isOnSecretsManagerStandalone;
|
|
||||||
this.organizationUsersCount = billingMetadata.organizationOccupiedSeats;
|
|
||||||
|
|
||||||
await this.load();
|
|
||||||
|
|
||||||
this.searchControl.setValue(qParams.search);
|
this.searchControl.setValue(qParams.search);
|
||||||
|
|
||||||
if (qParams.viewEvents != null) {
|
if (qParams.viewEvents != null) {
|
||||||
const user = this.dataSource.data.filter((u) => u.id === qParams.viewEvents);
|
const user = this.dataSource.data.filter((u) => u.id === qParams.viewEvents);
|
||||||
if (user.length > 0 && user[0].status === OrganizationUserStatusType.Confirmed) {
|
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();
|
.subscribe();
|
||||||
|
|
||||||
this.showUserManagementControls$ = organization$.pipe(
|
|
||||||
map((organization) => organization.canManageUsers),
|
|
||||||
);
|
|
||||||
|
|
||||||
organization$
|
organization$
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((organization) =>
|
switchMap((organization) =>
|
||||||
@@ -265,23 +254,40 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
takeUntilDestroyed(),
|
takeUntilDestroyed(),
|
||||||
)
|
)
|
||||||
.subscribe();
|
.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[]> {
|
override async load(organization: Organization) {
|
||||||
let groupsPromise: Promise<Map<string, string>>;
|
this.refreshBillingMetadata$.next(null);
|
||||||
let collectionsPromise: Promise<Map<string, string>>;
|
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
|
// We don't need both groups and collections for the table, so only load one
|
||||||
const userPromise = this.organizationUserApiService.getAllUsers(this.organization.id, {
|
const userPromise = this.organizationUserApiService.getAllUsers(organization.id, {
|
||||||
includeGroups: this.organization.useGroups,
|
includeGroups: organization.useGroups,
|
||||||
includeCollections: !this.organization.useGroups,
|
includeCollections: !organization.useGroups,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Depending on which column is displayed, we need to load the group/collection names
|
// Depending on which column is displayed, we need to load the group/collection names
|
||||||
if (this.organization.useGroups) {
|
if (organization.useGroups) {
|
||||||
groupsPromise = this.getGroupNameMap();
|
groupsPromise = this.getGroupNameMap(organization);
|
||||||
} else {
|
} else {
|
||||||
collectionsPromise = this.getCollectionNameMap();
|
collectionsPromise = this.getCollectionNameMap(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [usersResponse, groupNamesMap, collectionNamesMap] = await Promise.all([
|
const [usersResponse, groupNamesMap, collectionNamesMap] = await Promise.all([
|
||||||
@@ -290,22 +296,26 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
collectionsPromise,
|
collectionsPromise,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return usersResponse.data?.map<OrganizationUserView>((r) => {
|
return (
|
||||||
const userView = OrganizationUserView.fromResponse(r);
|
usersResponse.data?.map<OrganizationUserView>((r) => {
|
||||||
|
const userView = OrganizationUserView.fromResponse(r);
|
||||||
|
|
||||||
userView.groupNames = userView.groups
|
userView.groupNames = userView.groups
|
||||||
.map((g) => groupNamesMap.get(g))
|
.map((g) => groupNamesMap?.get(g))
|
||||||
.sort(this.i18nService.collator?.compare);
|
.filter((name): name is string => name != null)
|
||||||
userView.collectionNames = userView.collections
|
.sort(this.i18nService.collator?.compare);
|
||||||
.map((c) => collectionNamesMap.get(c.id))
|
userView.collectionNames = userView.collections
|
||||||
.sort(this.i18nService.collator?.compare);
|
.map((c) => collectionNamesMap?.get(c.id))
|
||||||
|
.filter((name): name is string => name != null)
|
||||||
|
.sort(this.i18nService.collator?.compare);
|
||||||
|
|
||||||
return userView;
|
return userView;
|
||||||
});
|
}) ?? []
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGroupNameMap(): Promise<Map<string, string>> {
|
async getGroupNameMap(organization: Organization): Promise<Map<string, string>> {
|
||||||
const groups = await this.groupService.getAll(this.organization.id);
|
const groups = await this.groupService.getAll(organization.id);
|
||||||
const groupNameMap = new Map<string, string>();
|
const groupNameMap = new Map<string, string>();
|
||||||
groups.forEach((g) => groupNameMap.set(g.id, g.name));
|
groups.forEach((g) => groupNameMap.set(g.id, g.name));
|
||||||
return groupNameMap;
|
return groupNameMap;
|
||||||
@@ -314,8 +324,8 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
/**
|
/**
|
||||||
* Retrieve a map of all collection IDs <-> names for the organization.
|
* Retrieve a map of all collection IDs <-> names for the organization.
|
||||||
*/
|
*/
|
||||||
async getCollectionNameMap() {
|
async getCollectionNameMap(organization: Organization) {
|
||||||
const response = from(this.apiService.getCollections(this.organization.id)).pipe(
|
const response = from(this.apiService.getCollections(organization.id)).pipe(
|
||||||
map((res) =>
|
map((res) =>
|
||||||
res.data.map((r) =>
|
res.data.map((r) =>
|
||||||
Collection.fromCollectionData(new CollectionData(r as CollectionDetailsResponse)),
|
Collection.fromCollectionData(new CollectionData(r as CollectionDetailsResponse)),
|
||||||
@@ -324,9 +334,9 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const decryptedCollections$ = combineLatest([
|
const decryptedCollections$ = combineLatest([
|
||||||
this.accountService.activeAccount$.pipe(
|
this.userId$.pipe(
|
||||||
getUserId,
|
|
||||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||||
|
filter((orgKeys) => orgKeys != null),
|
||||||
),
|
),
|
||||||
response,
|
response,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
@@ -343,91 +353,94 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
return await firstValueFrom(decryptedCollections$);
|
return await firstValueFrom(decryptedCollections$);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeUser(id: string): Promise<void> {
|
removeUser(id: string, organization: Organization): Promise<void> {
|
||||||
return this.organizationUserApiService.removeOrganizationUser(this.organization.id, id);
|
return this.organizationUserApiService.removeOrganizationUser(organization.id, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
revokeUser(id: string): Promise<void> {
|
revokeUser(id: string, organization: Organization): Promise<void> {
|
||||||
return this.organizationUserApiService.revokeOrganizationUser(this.organization.id, id);
|
return this.organizationUserApiService.revokeOrganizationUser(organization.id, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreUser(id: string): Promise<void> {
|
restoreUser(id: string, organization: Organization): Promise<void> {
|
||||||
return this.organizationUserApiService.restoreOrganizationUser(this.organization.id, id);
|
return this.organizationUserApiService.restoreOrganizationUser(organization.id, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
reinviteUser(id: string): Promise<void> {
|
reinviteUser(id: string, organization: Organization): Promise<void> {
|
||||||
return this.organizationUserApiService.postOrganizationUserReinvite(this.organization.id, id);
|
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 (
|
if (
|
||||||
await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation))
|
await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation))
|
||||||
) {
|
) {
|
||||||
await firstValueFrom(
|
await firstValueFrom(this.organizationUserService.confirmUser(organization, user, publicKey));
|
||||||
this.organizationUserService.confirmUser(this.organization, user, publicKey),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
const orgKey = await firstValueFrom(
|
const request = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(
|
this.userId$.pipe(
|
||||||
getUserId,
|
|
||||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
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(
|
await this.organizationUserApiService.postOrganizationUserConfirm(
|
||||||
this.organization.id,
|
organization.id,
|
||||||
user.id,
|
user.id,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async revoke(user: OrganizationUserView) {
|
async revoke(user: OrganizationUserView, organization: Organization) {
|
||||||
const confirmed = await this.revokeUserConfirmationDialog(user);
|
const confirmed = await this.revokeUserConfirmationDialog(user);
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.actionPromise = this.revokeUser(user.id);
|
this.actionPromise = this.revokeUser(user.id, organization);
|
||||||
try {
|
try {
|
||||||
await this.actionPromise;
|
await this.actionPromise;
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
title: null,
|
|
||||||
message: this.i18nService.t("revokedUserId", this.userNamePipe.transform(user)),
|
message: this.i18nService.t("revokedUserId", this.userNamePipe.transform(user)),
|
||||||
});
|
});
|
||||||
await this.load();
|
await this.load(organization);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.validationService.showError(e);
|
this.validationService.showError(e);
|
||||||
}
|
}
|
||||||
this.actionPromise = null;
|
this.actionPromise = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async restore(user: OrganizationUserView) {
|
async restore(user: OrganizationUserView, organization: Organization) {
|
||||||
this.actionPromise = this.restoreUser(user.id);
|
this.actionPromise = this.restoreUser(user.id, organization);
|
||||||
try {
|
try {
|
||||||
await this.actionPromise;
|
await this.actionPromise;
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
title: null,
|
|
||||||
message: this.i18nService.t("restoredUserId", this.userNamePipe.transform(user)),
|
message: this.i18nService.t("restoredUserId", this.userNamePipe.transform(user)),
|
||||||
});
|
});
|
||||||
await this.load();
|
await this.load(organization);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.validationService.showError(e);
|
this.validationService.showError(e);
|
||||||
}
|
}
|
||||||
this.actionPromise = null;
|
this.actionPromise = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
allowResetPassword(orgUser: OrganizationUserView): boolean {
|
allowResetPassword(orgUser: OrganizationUserView, organization: Organization): boolean {
|
||||||
// Hierarchy check
|
|
||||||
let callingUserHasPermission = false;
|
let callingUserHasPermission = false;
|
||||||
|
|
||||||
switch (this.organization.type) {
|
switch (organization.type) {
|
||||||
case OrganizationUserType.Owner:
|
case OrganizationUserType.Owner:
|
||||||
callingUserHasPermission = true;
|
callingUserHasPermission = true;
|
||||||
break;
|
break;
|
||||||
@@ -441,33 +454,35 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final
|
|
||||||
return (
|
return (
|
||||||
this.organization.canManageUsersPassword &&
|
organization.canManageUsersPassword &&
|
||||||
callingUserHasPermission &&
|
callingUserHasPermission &&
|
||||||
this.organization.useResetPassword &&
|
organization.useResetPassword &&
|
||||||
this.organization.hasPublicAndPrivateKeys &&
|
organization.hasPublicAndPrivateKeys &&
|
||||||
orgUser.resetPasswordEnrolled &&
|
orgUser.resetPasswordEnrolled &&
|
||||||
this.orgResetPasswordPolicyEnabled &&
|
this.orgResetPasswordPolicyEnabled &&
|
||||||
orgUser.status === OrganizationUserStatusType.Confirmed
|
orgUser.status === OrganizationUserStatusType.Confirmed
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
showEnrolledStatus(orgUser: OrganizationUserUserDetailsResponse): boolean {
|
showEnrolledStatus(
|
||||||
|
orgUser: OrganizationUserUserDetailsResponse,
|
||||||
|
organization: Organization,
|
||||||
|
): boolean {
|
||||||
return (
|
return (
|
||||||
this.organization.useResetPassword &&
|
organization.useResetPassword &&
|
||||||
orgUser.resetPasswordEnrolled &&
|
orgUser.resetPasswordEnrolled &&
|
||||||
this.orgResetPasswordPolicyEnabled
|
this.orgResetPasswordPolicyEnabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getManageBillingText(): string {
|
private getManageBillingText(organization: Organization): string {
|
||||||
return this.organization.canEditSubscription ? "ManageBilling" : "NoManageBilling";
|
return organization.canEditSubscription ? "ManageBilling" : "NoManageBilling";
|
||||||
}
|
}
|
||||||
|
|
||||||
private getProductKey(productType: ProductTierType): string {
|
private getProductKey(organization: Organization): string {
|
||||||
let product = "";
|
let product = "";
|
||||||
switch (productType) {
|
switch (organization.productTierType) {
|
||||||
case ProductTierType.Free:
|
case ProductTierType.Free:
|
||||||
product = "freeOrg";
|
product = "freeOrg";
|
||||||
break;
|
break;
|
||||||
@@ -478,24 +493,21 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
product = "familiesPlan";
|
product = "familiesPlan";
|
||||||
break;
|
break;
|
||||||
default:
|
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 {
|
private getDialogContent(organization: Organization): string {
|
||||||
return this.i18nService.t(
|
return this.i18nService.t(this.getProductKey(organization), organization.seats);
|
||||||
this.getProductKey(this.organization.productTierType),
|
|
||||||
this.organization.seats,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAcceptButtonText(): string {
|
private getAcceptButtonText(organization: Organization): string {
|
||||||
if (!this.organization.canEditSubscription) {
|
if (!organization.canEditSubscription) {
|
||||||
return this.i18nService.t("ok");
|
return this.i18nService.t("ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
const productType = this.organization.productTierType;
|
const productType = organization.productTierType;
|
||||||
|
|
||||||
if (isNotSelfUpgradable(productType)) {
|
if (isNotSelfUpgradable(productType)) {
|
||||||
throw new Error(`Unsupported product type: ${productType}`);
|
throw new Error(`Unsupported product type: ${productType}`);
|
||||||
@@ -504,82 +516,88 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
return this.i18nService.t("upgrade");
|
return this.i18nService.t("upgrade");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleDialogClose(result: boolean | undefined): Promise<void> {
|
private async handleDialogClose(
|
||||||
if (!result || !this.organization.canEditSubscription) {
|
result: boolean | undefined,
|
||||||
|
organization: Organization,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!result || !organization.canEditSubscription) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const productType = this.organization.productTierType;
|
const productType = organization.productTierType;
|
||||||
|
|
||||||
if (isNotSelfUpgradable(productType)) {
|
if (isNotSelfUpgradable(productType)) {
|
||||||
throw new Error(`Unsupported product type: ${this.organization.productTierType}`);
|
throw new Error(`Unsupported product type: ${organization.productTierType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.router.navigate(
|
await this.router.navigate(["/organizations", organization.id, "billing", "subscription"], {
|
||||||
["/organizations", this.organization.id, "billing", "subscription"],
|
queryParams: { upgrade: true },
|
||||||
{ queryParams: { upgrade: true } },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async showSeatLimitReachedDialog(): Promise<void> {
|
private async showSeatLimitReachedDialog(organization: Organization): Promise<void> {
|
||||||
const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = {
|
const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = {
|
||||||
title: this.i18nService.t("upgradeOrganization"),
|
title: this.i18nService.t("upgradeOrganization"),
|
||||||
content: this.getDialogContent(),
|
content: this.getDialogContent(organization),
|
||||||
type: "primary",
|
type: "primary",
|
||||||
acceptButtonText: this.getAcceptButtonText(),
|
acceptButtonText: this.getAcceptButtonText(organization),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this.organization.canEditSubscription) {
|
if (!organization.canEditSubscription) {
|
||||||
orgUpgradeSimpleDialogOpts.cancelButtonText = null;
|
orgUpgradeSimpleDialogOpts.cancelButtonText = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const simpleDialog = this.dialogService.openSimpleDialogRef(orgUpgradeSimpleDialogOpts);
|
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.
|
await lastValueFrom(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
simpleDialog.closed.pipe(map((closed) => this.handleDialogClose(closed, organization))),
|
||||||
firstValueFrom(simpleDialog.closed).then(this.handleDialogClose.bind(this));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleInviteDialog() {
|
private async handleInviteDialog(organization: Organization) {
|
||||||
|
const billingMetadata = await firstValueFrom(this.billingMetadata$);
|
||||||
const dialog = openUserAddEditDialog(this.dialogService, {
|
const dialog = openUserAddEditDialog(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
kind: "Add",
|
kind: "Add",
|
||||||
organizationId: this.organization.id,
|
organizationId: organization.id,
|
||||||
allOrganizationUserEmails: this.dataSource.data?.map((user) => user.email) ?? [],
|
allOrganizationUserEmails: this.dataSource.data?.map((user) => user.email) ?? [],
|
||||||
occupiedSeatCount: this.occupiedSeatCount,
|
occupiedSeatCount: billingMetadata?.organizationOccupiedSeats ?? 0,
|
||||||
isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone,
|
isOnSecretsManagerStandalone: billingMetadata?.isOnSecretsManagerStandalone ?? false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await lastValueFrom(dialog.closed);
|
const result = await lastValueFrom(dialog.closed);
|
||||||
|
|
||||||
if (result === MemberDialogResult.Saved) {
|
if (result === MemberDialogResult.Saved) {
|
||||||
await this.load();
|
await this.load(organization);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleSeatLimitForFixedTiers() {
|
private async handleSeatLimitForFixedTiers(organization: Organization) {
|
||||||
if (!this.organization.canEditSubscription) {
|
if (!organization.canEditSubscription) {
|
||||||
await this.showSeatLimitReachedDialog();
|
await this.showSeatLimitReachedDialog(organization);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reference = openChangePlanDialog(this.dialogService, {
|
const reference = openChangePlanDialog(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
organizationId: this.organization.id,
|
organizationId: organization.id,
|
||||||
subscription: null,
|
productTierType: organization.productTierType,
|
||||||
productTierType: this.organization.productTierType,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await lastValueFrom(reference.closed);
|
const result = await lastValueFrom(reference.closed);
|
||||||
|
|
||||||
if (result === ChangePlanDialogResultType.Submitted) {
|
if (result === ChangePlanDialogResultType.Submitted) {
|
||||||
await this.load();
|
await this.load(organization);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async invite() {
|
async invite(organization: Organization) {
|
||||||
if (this.organization.hasReseller && this.organization.seats === this.occupiedSeatCount) {
|
const billingMetadata = await firstValueFrom(this.billingMetadata$);
|
||||||
|
if (
|
||||||
|
organization.hasReseller &&
|
||||||
|
organization.seats === billingMetadata?.organizationOccupiedSeats
|
||||||
|
) {
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: this.i18nService.t("seatLimitReached"),
|
title: this.i18nService.t("seatLimitReached"),
|
||||||
@@ -590,26 +608,31 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.occupiedSeatCount === this.organization.seats &&
|
billingMetadata?.organizationOccupiedSeats === organization.seats &&
|
||||||
isFixedSeatPlan(this.organization.productTierType)
|
isFixedSeatPlan(organization.productTierType)
|
||||||
) {
|
) {
|
||||||
await this.handleSeatLimitForFixedTiers();
|
await this.handleSeatLimitForFixedTiers(organization);
|
||||||
|
|
||||||
return;
|
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, {
|
const dialog = openUserAddEditDialog(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
kind: "Edit",
|
kind: "Edit",
|
||||||
name: this.userNamePipe.transform(user),
|
name: this.userNamePipe.transform(user),
|
||||||
organizationId: this.organization.id,
|
organizationId: organization.id,
|
||||||
organizationUserId: user.id,
|
organizationUserId: user.id,
|
||||||
usesKeyConnector: user.usesKeyConnector,
|
usesKeyConnector: user.usesKeyConnector,
|
||||||
isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone,
|
isOnSecretsManagerStandalone: billingMetadata?.isOnSecretsManagerStandalone ?? false,
|
||||||
initialTab: initialTab,
|
initialTab: initialTab,
|
||||||
managedByOrganization: user.managedByOrganization,
|
managedByOrganization: user.managedByOrganization,
|
||||||
},
|
},
|
||||||
@@ -623,35 +646,35 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
case MemberDialogResult.Saved:
|
case MemberDialogResult.Saved:
|
||||||
case MemberDialogResult.Revoked:
|
case MemberDialogResult.Revoked:
|
||||||
case MemberDialogResult.Restored:
|
case MemberDialogResult.Restored:
|
||||||
await this.load();
|
await this.load(organization);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkRemove() {
|
async bulkRemove(organization: Organization) {
|
||||||
if (this.actionPromise != null) {
|
if (this.actionPromise != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, {
|
const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
organizationId: this.organization.id,
|
organizationId: organization.id,
|
||||||
users: this.dataSource.getCheckedUsers(),
|
users: this.dataSource.getCheckedUsers(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await lastValueFrom(dialogRef.closed);
|
await lastValueFrom(dialogRef.closed);
|
||||||
await this.load();
|
await this.load(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkDelete() {
|
async bulkDelete(organization: Organization) {
|
||||||
const warningAcknowledged = await firstValueFrom(
|
const warningAcknowledged = await firstValueFrom(
|
||||||
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
|
this.deleteManagedMemberWarningService.warningAcknowledged(organization.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!warningAcknowledged &&
|
!warningAcknowledged &&
|
||||||
this.organization.canManageUsers &&
|
organization.canManageUsers &&
|
||||||
this.organization.productTierType === ProductTierType.Enterprise
|
organization.productTierType === ProductTierType.Enterprise
|
||||||
) {
|
) {
|
||||||
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
|
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
|
||||||
if (!acknowledged) {
|
if (!acknowledged) {
|
||||||
@@ -665,38 +688,38 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
|
|
||||||
const dialogRef = BulkDeleteDialogComponent.open(this.dialogService, {
|
const dialogRef = BulkDeleteDialogComponent.open(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
organizationId: this.organization.id,
|
organizationId: organization.id,
|
||||||
users: this.dataSource.getCheckedUsers(),
|
users: this.dataSource.getCheckedUsers(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await lastValueFrom(dialogRef.closed);
|
await lastValueFrom(dialogRef.closed);
|
||||||
await this.load();
|
await this.load(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkRevoke() {
|
async bulkRevoke(organization: Organization) {
|
||||||
await this.bulkRevokeOrRestore(true);
|
await this.bulkRevokeOrRestore(true, organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkRestore() {
|
async bulkRestore(organization: Organization) {
|
||||||
await this.bulkRevokeOrRestore(false);
|
await this.bulkRevokeOrRestore(false, organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkRevokeOrRestore(isRevoking: boolean) {
|
async bulkRevokeOrRestore(isRevoking: boolean, organization: Organization) {
|
||||||
if (this.actionPromise != null) {
|
if (this.actionPromise != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ref = BulkRestoreRevokeComponent.open(this.dialogService, {
|
const ref = BulkRestoreRevokeComponent.open(this.dialogService, {
|
||||||
organizationId: this.organization.id,
|
organizationId: organization.id,
|
||||||
users: this.dataSource.getCheckedUsers(),
|
users: this.dataSource.getCheckedUsers(),
|
||||||
isRevoking: isRevoking,
|
isRevoking: isRevoking,
|
||||||
});
|
});
|
||||||
|
|
||||||
await firstValueFrom(ref.closed);
|
await firstValueFrom(ref.closed);
|
||||||
await this.load();
|
await this.load(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkReinvite() {
|
async bulkReinvite(organization: Organization) {
|
||||||
if (this.actionPromise != null) {
|
if (this.actionPromise != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -715,7 +738,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = this.organizationUserApiService.postManyOrganizationUserReinvite(
|
const response = this.organizationUserApiService.postManyOrganizationUserReinvite(
|
||||||
this.organization.id,
|
organization.id,
|
||||||
filteredUsers.map((user) => user.id),
|
filteredUsers.map((user) => user.id),
|
||||||
);
|
);
|
||||||
// Bulk Status component open
|
// Bulk Status component open
|
||||||
@@ -731,26 +754,26 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.validationService.showError(e);
|
this.validationService.showError(e);
|
||||||
}
|
}
|
||||||
this.actionPromise = null;
|
this.actionPromise = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkConfirm() {
|
async bulkConfirm(organization: Organization) {
|
||||||
if (this.actionPromise != null) {
|
if (this.actionPromise != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, {
|
const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
organization: this.organization,
|
organization: organization,
|
||||||
users: this.dataSource.getCheckedUsers(),
|
users: this.dataSource.getCheckedUsers(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await lastValueFrom(dialogRef.closed);
|
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);
|
const users = this.dataSource.getCheckedUsers().filter((ou) => !ou.accessSecretsManager);
|
||||||
|
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
@@ -763,20 +786,20 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dialogRef = BulkEnableSecretsManagerDialogComponent.open(this.dialogService, {
|
const dialogRef = BulkEnableSecretsManagerDialogComponent.open(this.dialogService, {
|
||||||
orgId: this.organization.id,
|
orgId: organization.id,
|
||||||
users,
|
users,
|
||||||
});
|
});
|
||||||
|
|
||||||
await lastValueFrom(dialogRef.closed);
|
await lastValueFrom(dialogRef.closed);
|
||||||
this.dataSource.uncheckAllUsers();
|
this.dataSource.uncheckAllUsers();
|
||||||
await this.load();
|
await this.load(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
openEventsDialog(user: OrganizationUserView) {
|
openEventsDialog(user: OrganizationUserView, organization: Organization) {
|
||||||
openEntityEventsDialog(this.dialogService, {
|
openEntityEventsDialog(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
name: this.userNamePipe.transform(user),
|
name: this.userNamePipe.transform(user),
|
||||||
organizationId: this.organization.id,
|
organizationId: organization.id,
|
||||||
entityId: user.id,
|
entityId: user.id,
|
||||||
showUser: false,
|
showUser: false,
|
||||||
entity: "user",
|
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) {
|
if (!user || !user.email || !user.id) {
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
@@ -800,14 +823,14 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
data: {
|
data: {
|
||||||
name: this.userNamePipe.transform(user),
|
name: this.userNamePipe.transform(user),
|
||||||
email: user.email,
|
email: user.email,
|
||||||
organizationId: this.organization.id as OrganizationId,
|
organizationId: organization.id as OrganizationId,
|
||||||
organizationUserId: user.id,
|
organizationUserId: user.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await lastValueFrom(dialogRef.closed);
|
const result = await lastValueFrom(dialogRef.closed);
|
||||||
if (result === AccountRecoveryDialogResultType.Ok) {
|
if (result === AccountRecoveryDialogResultType.Ok) {
|
||||||
await this.load();
|
await this.load(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -857,15 +880,15 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteUser(user: OrganizationUserView) {
|
async deleteUser(user: OrganizationUserView, organization: Organization) {
|
||||||
const warningAcknowledged = await firstValueFrom(
|
const warningAcknowledged = await firstValueFrom(
|
||||||
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
|
this.deleteManagedMemberWarningService.warningAcknowledged(organization.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!warningAcknowledged &&
|
!warningAcknowledged &&
|
||||||
this.organization.canManageUsers &&
|
organization.canManageUsers &&
|
||||||
this.organization.productTierType === ProductTierType.Enterprise
|
organization.productTierType === ProductTierType.Enterprise
|
||||||
) {
|
) {
|
||||||
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
|
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
|
||||||
if (!acknowledged) {
|
if (!acknowledged) {
|
||||||
@@ -891,24 +914,23 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organization.id);
|
await this.deleteManagedMemberWarningService.acknowledgeWarning(organization.id);
|
||||||
|
|
||||||
this.actionPromise = this.organizationUserApiService.deleteOrganizationUser(
|
this.actionPromise = this.organizationUserApiService.deleteOrganizationUser(
|
||||||
this.organization.id,
|
organization.id,
|
||||||
user.id,
|
user.id,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await this.actionPromise;
|
await this.actionPromise;
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
title: null,
|
|
||||||
message: this.i18nService.t("organizationUserDeleted", this.userNamePipe.transform(user)),
|
message: this.i18nService.t("organizationUserDeleted", this.userNamePipe.transform(user)),
|
||||||
});
|
});
|
||||||
this.dataSource.removeUser(user);
|
this.dataSource.removeUser(user);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.validationService.showError(e);
|
this.validationService.showError(e);
|
||||||
}
|
}
|
||||||
this.actionPromise = null;
|
this.actionPromise = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async noMasterPasswordConfirmationDialog(user: OrganizationUserView) {
|
private async noMasterPasswordConfirmationDialog(user: OrganizationUserView) {
|
||||||
@@ -952,12 +974,12 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
.every((member) => member.managedByOrganization && validStatuses.includes(member.status));
|
.every((member) => member.managedByOrganization && validStatuses.includes(member.status));
|
||||||
}
|
}
|
||||||
|
|
||||||
async navigateToPaymentMethod() {
|
async navigateToPaymentMethod(organization: Organization) {
|
||||||
const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag(
|
const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag(
|
||||||
FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout,
|
FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout,
|
||||||
);
|
);
|
||||||
const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method";
|
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 },
|
state: { launchPaymentModalAutomatically: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ import { PaymentComponent } from "../shared/payment/payment.component";
|
|||||||
|
|
||||||
type ChangePlanDialogParams = {
|
type ChangePlanDialogParams = {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
subscription: OrganizationSubscriptionResponse;
|
|
||||||
productTierType: ProductTierType;
|
productTierType: ProductTierType;
|
||||||
|
subscription?: OrganizationSubscriptionResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME: update to use a const object instead of a typescript enum
|
// 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> {
|
async bulkRemove(): Promise<void> {
|
||||||
if (this.actionPromise != null) {
|
if (this.actionPromise != null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class Policy extends Domain {
|
|||||||
return new Policy(new PolicyData(response));
|
return new Policy(new PolicyData(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromListResponse(response: ListResponse<PolicyResponse>): Policy[] | undefined {
|
static fromListResponse(response: ListResponse<PolicyResponse>): Policy[] {
|
||||||
return response.data?.map((d) => Policy.fromResponse(d)) ?? undefined;
|
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";
|
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));
|
return Object.values(policiesMap || {}).map((f) => new Policy(f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user