mirror of
https://github.com/bitwarden/clients.git
synced 2025-12-05 19:17:06 -06:00
refactor WIP
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable, Subject } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationUserStatusType,
|
||||
ProviderUserStatusType,
|
||||
@@ -83,12 +85,19 @@ export abstract class PeopleTableDataSource<T extends UserViewTypes> extends Tab
|
||||
*/
|
||||
checkUser(user: T, select?: boolean) {
|
||||
(user as any).checked = select == null ? !(user as any).checked : select;
|
||||
this.checkedUsersUpdated$.next();
|
||||
}
|
||||
|
||||
getCheckedUsers() {
|
||||
return this.data.filter((u) => (u as any).checked);
|
||||
}
|
||||
|
||||
private checkedUsersUpdated$ = new Subject<void>();
|
||||
|
||||
usersUpdated(): Observable<void> {
|
||||
return this.checkedUsersUpdated$.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check all filtered users (i.e. those rows that are currently visible)
|
||||
* @param select check the filtered users (true) or uncheck the filtered users (false)
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
@let organization = this.organization();
|
||||
@let bulkActions = bulkFlags$ | async;
|
||||
|
||||
@if (!organization || !dataSource) {
|
||||
<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>
|
||||
}
|
||||
|
||||
@if (organization) {
|
||||
<app-organization-free-trial-warning
|
||||
[organization]="organization"
|
||||
@@ -12,173 +23,167 @@
|
||||
[placeholder]="'searchMembers' | i18n"
|
||||
></bit-search>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(click)="invite(organization)"
|
||||
[disabled]="!firstLoaded"
|
||||
*ngIf="showUserManagementControls()"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "inviteMember" | i18n }}
|
||||
</button>
|
||||
@if (showUserManagementControls()) {
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(click)="invite(organization)"
|
||||
[disabled]="!firstLoaded()"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "inviteMember" | i18n }}
|
||||
</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>
|
||||
@if (showUserManagementControls()) {
|
||||
<bit-toggle-group
|
||||
[selected]="statusToggle | async"
|
||||
(selectedChange)="statusToggle.next($event)"
|
||||
[attr.aria-label]="'memberStatusFilter' | i18n"
|
||||
>
|
||||
<bit-toggle [value]="undefined">
|
||||
{{ "all" | i18n }}
|
||||
@if (dataSource.activeUserCount; as allCount) {
|
||||
<span bitBadge variant="info">{{ 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.Invited">
|
||||
{{ "invited" | i18n }}
|
||||
@if (dataSource.invitedUserCount; as invitedCount) {
|
||||
<span bitBadge variant="info">{{ 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.Accepted">
|
||||
{{ "needsConfirmation" | i18n }}
|
||||
@if (dataSource.acceptedUserCount; as acceptedUserCount) {
|
||||
<span bitBadge variant="info">{{ 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>
|
||||
<bit-toggle [value]="userStatusType.Revoked">
|
||||
{{ "revoked" | i18n }}
|
||||
@if (dataSource.revokedUserCount; as revokedCount) {
|
||||
<span bitBadge variant="info">{{ revokedCount }}</span>
|
||||
}
|
||||
</bit-toggle>
|
||||
</bit-toggle-group>
|
||||
}
|
||||
</div>
|
||||
<ng-container *ngIf="!firstLoaded">
|
||||
@if (!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>
|
||||
} @else {
|
||||
@if (!dataSource.filteredData?.length) {
|
||||
<p>{{ "noMembersInList" | i18n }}</p>
|
||||
}
|
||||
@if (dataSource.filteredData?.length) {
|
||||
@if (bulkActions.showConfirmUsers) {
|
||||
<bit-callout type="info" title="{{ 'confirmUsers' | i18n }}" icon="bwi-check-circle">
|
||||
{{ "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. -->
|
||||
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell class="tw-w-20" *ngIf="showUserManagementControls()">
|
||||
<input
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
class="tw-mr-1"
|
||||
(change)="dataSource.checkAllFilteredUsers($any($event.target).checked)"
|
||||
id="selectAll"
|
||||
/>
|
||||
<label class="tw-mb-0 !tw-font-medium !tw-text-muted" for="selectAll">{{
|
||||
"all" | i18n
|
||||
}}</label>
|
||||
</th>
|
||||
@if (showUserManagementControls()) {
|
||||
<th bitCell class="tw-w-20">
|
||||
<input
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
class="tw-mr-1"
|
||||
(change)="dataSource.checkAllFilteredUsers($any($event.target).checked)"
|
||||
id="selectAll"
|
||||
/>
|
||||
<label class="tw-mb-0 !tw-font-medium !tw-text-muted" for="selectAll">{{
|
||||
"all" | i18n
|
||||
}}</label>
|
||||
</th>
|
||||
}
|
||||
<th bitCell bitSortable="email" default>{{ "name" | i18n }}</th>
|
||||
<th bitCell>{{ (organization.useGroups ? "groups" : "collections") | i18n }}</th>
|
||||
<th bitCell bitSortable="type">{{ "role" | i18n }}</th>
|
||||
<th bitCell>{{ "policies" | i18n }}</th>
|
||||
<th bitCell class="tw-w-10">
|
||||
<button
|
||||
[bitMenuTriggerFor]="headerMenu"
|
||||
type="button"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
size="small"
|
||||
label="{{ 'options' | i18n }}"
|
||||
*ngIf="showUserManagementControls()"
|
||||
></button>
|
||||
@if (showUserManagementControls()) {
|
||||
<button
|
||||
[bitMenuTriggerFor]="headerMenu"
|
||||
type="button"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
size="small"
|
||||
label="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
}
|
||||
|
||||
<bit-menu #headerMenu>
|
||||
<ng-container *ngIf="canUseSecretsManager()">
|
||||
@if (canUseSecretsManager()) {
|
||||
<button type="button" bitMenuItem (click)="bulkEnableSM(organization)">
|
||||
{{ "activateSecretsManager" | i18n }}
|
||||
</button>
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
</ng-container>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkReinvite(organization)"
|
||||
*ngIf="showBulkReinviteUsers"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||
{{ "reinviteSelected" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkConfirm(organization)"
|
||||
*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(organization)"
|
||||
*ngIf="showBulkRestoreUsers"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||
{{ "restoreAccess" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkRevoke(organization)"
|
||||
*ngIf="showBulkRevokeUsers"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
|
||||
{{ "revokeAccess" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkRemove(organization)"
|
||||
*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(organization)"
|
||||
*ngIf="showBulkDeleteUsers"
|
||||
>
|
||||
<span class="tw-text-danger">
|
||||
<i aria-hidden="true" class="bwi bwi-fw bwi-trash"></i>
|
||||
{{ "delete" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
@if (bulkActions.showBulkReinviteUsers) {
|
||||
<button type="button" bitMenuItem (click)="bulkReinvite(organization)">
|
||||
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||
{{ "reinviteSelected" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (bulkActions.showBulkConfirmUsers) {
|
||||
<button type="button" bitMenuItem (click)="bulkConfirm(organization)">
|
||||
<span class="tw-text-success">
|
||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||
{{ "confirmSelected" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
@if (bulkActions.showBulkRestoreUsers) {
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkRevokeOrRestore(false, organization)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||
{{ "restoreAccess" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (bulkActions.showBulkRevokeUsers) {
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkRevokeOrRestore(true, organization)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
|
||||
{{ "revokeAccess" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (bulkActions.showBulkRemoveUsers) {
|
||||
<button type="button" bitMenuItem (click)="bulkRemove(organization)">
|
||||
<span class="tw-text-danger">
|
||||
<i aria-hidden="true" class="bwi bwi-fw bwi-close"></i>
|
||||
{{ "remove" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
@if (bulkActions.showBulkDeleteUsers) {
|
||||
<button type="button" bitMenuItem (click)="bulkDelete(organization)">
|
||||
<span class="tw-text-danger">
|
||||
<i aria-hidden="true" class="bwi bwi-fw bwi-trash"></i>
|
||||
{{ "delete" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</bit-menu>
|
||||
</th>
|
||||
</tr>
|
||||
@@ -190,10 +195,12 @@
|
||||
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">
|
||||
@if (showUserManagementControls()) {
|
||||
<td bitCell (click)="dataSource.checkUser(u)">
|
||||
<input type="checkbox" bitCheckbox [(ngModel)]="$any(u).checked" />
|
||||
</td>
|
||||
}
|
||||
@if (showUserManagementControls()) {
|
||||
<td bitCell (click)="edit(u, organization)" class="tw-cursor-pointer">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
@@ -208,39 +215,31 @@
|
||||
<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 }}
|
||||
@if (u.status === userStatusType.Invited) {
|
||||
<span bitBadge class="tw-text-xs" variant="secondary">
|
||||
{{ "invited" | i18n }}
|
||||
</span>
|
||||
}
|
||||
@if (u.status === userStatusType.Accepted) {
|
||||
<span bitBadge class="tw-text-xs" variant="warning">
|
||||
{{ "needsConfirmation" | i18n }}
|
||||
</span>
|
||||
}
|
||||
@if (u.status === userStatusType.Revoked) {
|
||||
<span bitBadge class="tw-text-xs" variant="secondary">
|
||||
{{ "revoked" | i18n }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
@if (u.name) {
|
||||
<div class="tw-text-sm tw-text-muted">
|
||||
{{ u.email }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-template #readOnlyUserInfo>
|
||||
} @else {
|
||||
<td bitCell>
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
@@ -253,40 +252,33 @@
|
||||
<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 }}
|
||||
@if (u.status === userStatusType.Invited) {
|
||||
<span bitBadge class="tw-text-xs" variant="secondary">
|
||||
{{ "invited" | i18n }}
|
||||
</span>
|
||||
}
|
||||
@if (u.status === userStatusType.Accepted) {
|
||||
<span bitBadge class="tw-text-xs" variant="warning">
|
||||
{{ "needsConfirmation" | i18n }}
|
||||
</span>
|
||||
}
|
||||
@if (u.status === userStatusType.Revoked) {
|
||||
<span bitBadge class="tw-text-xs" variant="secondary">
|
||||
{{ "revoked" | i18n }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
@if (u.name) {
|
||||
<div class="tw-text-sm tw-text-muted">
|
||||
{{ u.email }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
<ng-container *ngIf="showUserManagementControls(); else readOnlyGroupsCell">
|
||||
@if (showUserManagementControls()) {
|
||||
<td
|
||||
bitCell
|
||||
(click)="
|
||||
@@ -304,8 +296,7 @@
|
||||
variant="secondary"
|
||||
></bit-badge-list>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-template #readOnlyGroupsCell>
|
||||
} @else {
|
||||
<td bitCell>
|
||||
<bit-badge-list
|
||||
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
|
||||
@@ -313,9 +304,9 @@
|
||||
variant="secondary"
|
||||
></bit-badge-list>
|
||||
</td>
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
<ng-container *ngIf="showUserManagementControls(); else readOnlyRoleCell">
|
||||
@if (showUserManagementControls()) {
|
||||
<td
|
||||
bitCell
|
||||
(click)="edit(u, organization, memberTab.Role)"
|
||||
@@ -323,33 +314,30 @@
|
||||
>
|
||||
{{ u.type | userType }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-template #readOnlyRoleCell>
|
||||
} @else {
|
||||
<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">
|
||||
@if (u.twoFactorEnabled) {
|
||||
<i
|
||||
class="bwi bwi-lock"
|
||||
title="{{ 'userUsingTwoStep' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "userUsingTwoStep" | i18n }}</span>
|
||||
</ng-container>
|
||||
}
|
||||
@let resetPasswordPolicyEnabled = resetPasswordPolicyEnabled$ | async;
|
||||
<ng-container
|
||||
*ngIf="showEnrolledStatus($any(u), organization, resetPasswordPolicyEnabled)"
|
||||
>
|
||||
@if (showEnrolledStatus($any(u), organization, resetPasswordPolicyEnabled)) {
|
||||
<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
|
||||
@@ -361,31 +349,25 @@
|
||||
></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>
|
||||
@if (showUserManagementControls()) {
|
||||
@if (u.status === userStatusType.Invited) {
|
||||
<button type="button" bitMenuItem (click)="reinvite(u, organization)">
|
||||
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
||||
{{ "resendInvitation" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (u.status === userStatusType.Accepted) {
|
||||
<button type="button" bitMenuItem (click)="confirm(u, organization)">
|
||||
<span class="tw-text-success">
|
||||
<i aria-hidden="true" class="bwi bwi-check"></i> {{ "confirm" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
@if (
|
||||
u.status === userStatusType.Accepted || u.status === userStatusType.Invited
|
||||
) {
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
@@ -393,14 +375,15 @@
|
||||
>
|
||||
<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>
|
||||
@if (organization.useGroups) {
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="edit(u, organization, memberTab.Groups)"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "groups" | i18n }}
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
@@ -410,73 +393,56 @@
|
||||
{{ "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>
|
||||
@if (organization.useEvents && u.status === userStatusType.Confirmed) {
|
||||
<button type="button" bitMenuItem (click)="openEventsDialog(u, organization)">
|
||||
<i aria-hidden="true" class="bwi bwi-file-text"></i>
|
||||
{{ "eventLogs" | i18n }}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Account recovery is available to all users with appropriate permissions -->
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="resetPassword(u, organization)"
|
||||
*ngIf="allowResetPassword(u, organization, resetPasswordPolicyEnabled)"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-key"></i> {{ "recoverAccount" | i18n }}
|
||||
</button>
|
||||
@if (allowResetPassword(u, organization, resetPasswordPolicyEnabled)) {
|
||||
<button type="button" bitMenuItem (click)="resetPassword(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>
|
||||
@if (showUserManagementControls()) {
|
||||
@if (u.status === userStatusType.Revoked) {
|
||||
<button type="button" bitMenuItem (click)="restore(u, organization)">
|
||||
<i aria-hidden="true" class="bwi bwi-plus-circle"></i>
|
||||
{{ "restoreAccess" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (u.status !== userStatusType.Revoked) {
|
||||
<button type="button" bitMenuItem (click)="revoke(u, organization)">
|
||||
<i aria-hidden="true" class="bwi bwi-minus-circle"></i>
|
||||
{{ "revokeAccess" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (!u.managedByOrganization) {
|
||||
<button 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>
|
||||
}
|
||||
@if (u.managedByOrganization) {
|
||||
<button 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>
|
||||
}
|
||||
}
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
import { Component, computed, Signal } from "@angular/core";
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
signal,
|
||||
Signal,
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
|
||||
import { FormControl } from "@angular/forms";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
concatMap,
|
||||
debounceTime,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
from,
|
||||
lastValueFrom,
|
||||
map,
|
||||
merge,
|
||||
Observable,
|
||||
shareReplay,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
} from "rxjs";
|
||||
@@ -37,15 +50,16 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
import { BillingConstraintService } from "@bitwarden/web-vault/app/billing/members/billing-constraint/billing-constraint.service";
|
||||
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
|
||||
|
||||
import { BaseMembersComponent } from "../../common/base-members.component";
|
||||
import { PeopleTableDataSource } from "../../common/people-table-data-source";
|
||||
import { PeopleTableDataSource, peopleFilter } from "../../common/people-table-data-source";
|
||||
import { OrganizationUserView } from "../core/views/organization-user.view";
|
||||
import { UserConfirmComponent } from "../manage/user-confirm.component";
|
||||
|
||||
import { AccountRecoveryDialogResultType } from "./components/account-recovery/account-recovery-dialog.component";
|
||||
import { MemberDialogResult, MemberDialogTab } from "./components/member-dialog";
|
||||
@@ -56,75 +70,145 @@ import {
|
||||
MemberActionResult,
|
||||
} from "./services/member-actions/member-actions.service";
|
||||
|
||||
interface BulkFlags {
|
||||
showConfirmUsers: boolean;
|
||||
showBulkConfirmUsers: boolean;
|
||||
showBulkReinviteUsers: boolean;
|
||||
showBulkRestoreUsers: boolean;
|
||||
showBulkRevokeUsers: boolean;
|
||||
showBulkRemoveUsers: boolean;
|
||||
showBulkDeleteUsers: boolean;
|
||||
}
|
||||
|
||||
class MembersTableDataSource extends PeopleTableDataSource<OrganizationUserView> {
|
||||
protected statusType = OrganizationUserStatusType;
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "members.component.html",
|
||||
standalone: false,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MembersComponent extends BaseMembersComponent<OrganizationUserView> {
|
||||
userType = OrganizationUserType;
|
||||
userStatusType = OrganizationUserStatusType;
|
||||
memberTab = MemberDialogTab;
|
||||
protected dataSource = new MembersTableDataSource();
|
||||
|
||||
readonly organization: Signal<Organization | undefined>;
|
||||
status: OrganizationUserStatusType | undefined;
|
||||
export class MembersComponent {
|
||||
protected apiService = inject(ApiService);
|
||||
protected i18nService = inject(I18nService);
|
||||
protected organizationManagementPreferencesService = inject(
|
||||
OrganizationManagementPreferencesService,
|
||||
);
|
||||
protected keyService = inject(KeyService);
|
||||
protected validationService = inject(ValidationService);
|
||||
protected logService = inject(LogService);
|
||||
protected userNamePipe = inject(UserNamePipe);
|
||||
protected dialogService = inject(DialogService);
|
||||
protected toastService = inject(ToastService);
|
||||
private route = inject(ActivatedRoute);
|
||||
protected deleteManagedMemberWarningService = inject(DeleteManagedMemberWarningService);
|
||||
private organizationWarningsService = inject(OrganizationWarningsService);
|
||||
private memberActionsService = inject(MemberActionsService);
|
||||
private memberDialogManager = inject(MemberDialogManagerService);
|
||||
protected billingConstraint = inject(BillingConstraintService);
|
||||
protected memberService = inject(OrganizationMembersService);
|
||||
private organizationService = inject(OrganizationService);
|
||||
private accountService = inject(AccountService);
|
||||
private policyService = inject(PolicyService);
|
||||
private policyApiService = inject(PolicyApiServiceAbstraction);
|
||||
private organizationMetadataService = inject(OrganizationMetadataServiceAbstraction);
|
||||
private changeDetectionRef = inject(ChangeDetectorRef);
|
||||
|
||||
private userId$: Observable<UserId> = this.accountService.activeAccount$.pipe(getUserId);
|
||||
|
||||
resetPasswordPolicyEnabled$: Observable<boolean>;
|
||||
protected userType = OrganizationUserType;
|
||||
protected userStatusType = OrganizationUserStatusType;
|
||||
protected memberTab = MemberDialogTab;
|
||||
protected dataSource = new MembersTableDataSource();
|
||||
|
||||
protected searchControl = new FormControl("", { nonNullable: true });
|
||||
protected statusToggle = new BehaviorSubject<OrganizationUserStatusType | undefined>(undefined);
|
||||
|
||||
protected readonly organization: Signal<Organization | undefined>;
|
||||
protected readonly firstLoaded = signal(false);
|
||||
|
||||
bulkFlags$: Observable<BulkFlags> = this.dataSource.usersUpdated().pipe(
|
||||
startWith(null), // initial emission to kick off reactive member options menu actions
|
||||
map(() => {
|
||||
const checkedUsers = this.dataSource.getCheckedUsers();
|
||||
const result = {
|
||||
showConfirmUsers: true,
|
||||
showBulkConfirmUsers: true,
|
||||
showBulkReinviteUsers: true,
|
||||
showBulkRestoreUsers: true,
|
||||
showBulkRevokeUsers: true,
|
||||
showBulkRemoveUsers: true,
|
||||
showBulkDeleteUsers: true,
|
||||
};
|
||||
|
||||
if (checkedUsers.length) {
|
||||
checkedUsers.forEach((member) => {
|
||||
if (member.status !== this.userStatusType.Accepted) {
|
||||
result.showBulkConfirmUsers = false;
|
||||
}
|
||||
if (member.status !== this.userStatusType.Invited) {
|
||||
result.showBulkReinviteUsers = false;
|
||||
}
|
||||
if (member.status !== this.userStatusType.Revoked) {
|
||||
result.showBulkRestoreUsers = false;
|
||||
}
|
||||
if (member.status == this.userStatusType.Revoked) {
|
||||
result.showBulkRevokeUsers = false;
|
||||
}
|
||||
|
||||
result.showBulkRemoveUsers = !member.managedByOrganization;
|
||||
|
||||
const validStatuses = [
|
||||
this.userStatusType.Accepted,
|
||||
this.userStatusType.Confirmed,
|
||||
this.userStatusType.Revoked,
|
||||
];
|
||||
|
||||
result.showBulkDeleteUsers =
|
||||
member.managedByOrganization && validStatuses.includes(member.status);
|
||||
});
|
||||
}
|
||||
|
||||
result.showConfirmUsers =
|
||||
this.dataSource.activeUserCount > 1 &&
|
||||
this.dataSource.confirmedUserCount > 0 &&
|
||||
this.dataSource.confirmedUserCount < 3 &&
|
||||
this.dataSource.acceptedUserCount > 0;
|
||||
|
||||
return result;
|
||||
}),
|
||||
);
|
||||
|
||||
protected readonly canUseSecretsManager: Signal<boolean> = computed(
|
||||
() => this.organization()?.useSecretsManager ?? false,
|
||||
);
|
||||
|
||||
protected readonly showUserManagementControls: Signal<boolean> = computed(
|
||||
() => this.organization()?.canManageUsers ?? false,
|
||||
);
|
||||
|
||||
protected billingMetadata$: Observable<OrganizationBillingMetadataResponse>;
|
||||
|
||||
protected resetPasswordPolicyEnabled$: Observable<boolean>;
|
||||
|
||||
// Fixed sizes used for cdkVirtualScroll
|
||||
protected rowHeight = 66;
|
||||
protected rowHeightClass = `tw-h-[66px]`;
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
i18nService: I18nService,
|
||||
organizationManagementPreferencesService: OrganizationManagementPreferencesService,
|
||||
keyService: KeyService,
|
||||
validationService: ValidationService,
|
||||
logService: LogService,
|
||||
userNamePipe: UserNamePipe,
|
||||
dialogService: DialogService,
|
||||
toastService: ToastService,
|
||||
private route: ActivatedRoute,
|
||||
protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
|
||||
private organizationWarningsService: OrganizationWarningsService,
|
||||
private memberActionsService: MemberActionsService,
|
||||
private memberDialogManager: MemberDialogManagerService,
|
||||
protected billingConstraint: BillingConstraintService,
|
||||
protected memberService: OrganizationMembersService,
|
||||
private organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
private policyService: PolicyService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private organizationMetadataService: OrganizationMetadataServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
i18nService,
|
||||
keyService,
|
||||
validationService,
|
||||
logService,
|
||||
userNamePipe,
|
||||
dialogService,
|
||||
organizationManagementPreferencesService,
|
||||
toastService,
|
||||
);
|
||||
constructor() {
|
||||
this.dataSource
|
||||
.connect()
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe(() => {
|
||||
this.changeDetectionRef.markForCheck();
|
||||
});
|
||||
|
||||
combineLatest([this.searchControl.valueChanges.pipe(debounceTime(200)), this.statusToggle])
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe(
|
||||
([searchText, status]) => (this.dataSource.filter = peopleFilter(searchText, status)),
|
||||
);
|
||||
|
||||
const organization$ = this.route.params.pipe(
|
||||
concatMap((params) =>
|
||||
@@ -201,80 +285,118 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
this.billingMetadata$.pipe(take(1), takeUntilDestroyed()).subscribe();
|
||||
}
|
||||
|
||||
override async load(organization: Organization) {
|
||||
await super.load(organization);
|
||||
async load(organization: Organization) {
|
||||
const response = await this.memberService.loadUsers(organization);
|
||||
this.dataSource.data = Array.isArray(response) ? response : [];
|
||||
this.firstLoaded.set(true);
|
||||
}
|
||||
|
||||
async getUsers(organization: Organization): Promise<OrganizationUserView[]> {
|
||||
return await this.memberService.loadUsers(organization);
|
||||
}
|
||||
|
||||
async removeUser(id: string, organization: Organization): Promise<MemberActionResult> {
|
||||
return await this.memberActionsService.removeUser(organization, id);
|
||||
}
|
||||
|
||||
async revokeUser(id: string, organization: Organization): Promise<MemberActionResult> {
|
||||
return await this.memberActionsService.revokeUser(organization, id);
|
||||
}
|
||||
|
||||
async restoreUser(id: string, organization: Organization): Promise<MemberActionResult> {
|
||||
return await this.memberActionsService.restoreUser(organization, id);
|
||||
}
|
||||
|
||||
async reinviteUser(id: string, organization: Organization): Promise<MemberActionResult> {
|
||||
return await this.memberActionsService.reinviteUser(organization, id);
|
||||
}
|
||||
|
||||
async confirmUser(
|
||||
user: OrganizationUserView,
|
||||
publicKey: Uint8Array,
|
||||
organization: Organization,
|
||||
): Promise<MemberActionResult> {
|
||||
return await this.memberActionsService.confirmUser(user, publicKey, organization);
|
||||
}
|
||||
|
||||
async revoke(user: OrganizationUserView, organization: Organization) {
|
||||
const confirmed = await this.revokeUserConfirmationDialog(user);
|
||||
async remove(user: OrganizationUserView, organization: Organization) {
|
||||
const confirmed = await this.memberDialogManager.openRemoveUserConfirmationDialog(user);
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.actionPromise = this.revokeUser(user.id, organization);
|
||||
try {
|
||||
const result = await this.actionPromise;
|
||||
if (result.success) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("revokedUserId", this.userNamePipe.transform(user)),
|
||||
});
|
||||
await this.load(organization);
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
const result = await this.memberActionsService.removeUser(organization, user.id);
|
||||
const sideEffect = () => this.dataSource.removeUser(user);
|
||||
await this.handleMemberActionResult(result, "removedUserId", user, sideEffect);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
}
|
||||
|
||||
async reinvite(user: OrganizationUserView, organization: Organization) {
|
||||
try {
|
||||
const result = await this.memberActionsService.reinviteUser(organization, user.id);
|
||||
await this.handleMemberActionResult(result, "hasBeenReinvited", user);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
}
|
||||
|
||||
async confirm(user: OrganizationUserView, organization: Organization) {
|
||||
const confirmUserSideEffect = () => {
|
||||
user.status = this.userStatusType.Confirmed;
|
||||
this.dataSource.replaceUser(user);
|
||||
};
|
||||
|
||||
const confirmUser = async (publicKey: Uint8Array) => {
|
||||
try {
|
||||
const result = await this.memberActionsService.confirmUser(user, publicKey, organization);
|
||||
await this.handleMemberActionResult(
|
||||
result,
|
||||
"hasBeenConfirmed",
|
||||
user,
|
||||
confirmUserSideEffect,
|
||||
);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
|
||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
|
||||
const autoConfirm = await firstValueFrom(
|
||||
this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$,
|
||||
);
|
||||
if (user == null) {
|
||||
throw new Error("Cannot confirm null user.");
|
||||
}
|
||||
if (autoConfirm == null || !autoConfirm) {
|
||||
const dialogRef = UserConfirmComponent.open(this.dialogService, {
|
||||
data: {
|
||||
name: this.userNamePipe.transform(user),
|
||||
userId: user.userId,
|
||||
publicKey: publicKey,
|
||||
confirmUser: () => confirmUser(publicKey),
|
||||
},
|
||||
});
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fingerprint = await this.keyService.getFingerprint(user.userId, publicKey);
|
||||
this.logService.info(`User's fingerprint: ${fingerprint.join("-")}`);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
await confirmUser(publicKey);
|
||||
} catch (e) {
|
||||
this.logService.error(`Handled exception: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async revoke(user: OrganizationUserView, organization: Organization) {
|
||||
const confirmed = await this.memberDialogManager.openRevokeUserConfirmationDialog(user);
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.memberActionsService.revokeUser(organization, user.id);
|
||||
const sideEffect = async () => await this.load(organization);
|
||||
await this.handleMemberActionResult(result, "revokedUserId", user, sideEffect);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = undefined;
|
||||
}
|
||||
|
||||
async restore(user: OrganizationUserView, organization: Organization) {
|
||||
this.actionPromise = this.restoreUser(user.id, organization);
|
||||
try {
|
||||
const result = await this.actionPromise;
|
||||
if (result.success) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("restoredUserId", this.userNamePipe.transform(user)),
|
||||
});
|
||||
await this.load(organization);
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
const result = await this.memberActionsService.restoreUser(organization, user.id);
|
||||
const sideEffect = async () => await this.load(organization);
|
||||
await this.handleMemberActionResult(result, "restoredUserId", user, sideEffect);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = undefined;
|
||||
}
|
||||
|
||||
allowResetPassword(
|
||||
@@ -352,10 +474,6 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
}
|
||||
|
||||
async bulkRemove(organization: Organization) {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.memberDialogManager.openBulkRemoveDialog(
|
||||
organization,
|
||||
this.dataSource.getCheckedUsers(),
|
||||
@@ -365,10 +483,6 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
}
|
||||
|
||||
async bulkDelete(organization: Organization) {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.memberDialogManager.openBulkDeleteDialog(
|
||||
organization,
|
||||
this.dataSource.getCheckedUsers(),
|
||||
@@ -376,19 +490,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
await this.load(organization);
|
||||
}
|
||||
|
||||
async bulkRevoke(organization: Organization) {
|
||||
await this.bulkRevokeOrRestore(true, organization);
|
||||
}
|
||||
|
||||
async bulkRestore(organization: Organization) {
|
||||
await this.bulkRevokeOrRestore(false, organization);
|
||||
}
|
||||
|
||||
async bulkRevokeOrRestore(isRevoking: boolean, organization: Organization) {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.memberDialogManager.openBulkRestoreRevokeDialog(
|
||||
organization,
|
||||
this.dataSource.getCheckedUsers(),
|
||||
@@ -398,10 +500,6 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
}
|
||||
|
||||
async bulkReinvite(organization: Organization) {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const users = this.dataSource.getCheckedUsers();
|
||||
const filteredUsers = users.filter((u) => u.status === OrganizationUserStatusType.Invited);
|
||||
|
||||
@@ -434,14 +532,9 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = undefined;
|
||||
}
|
||||
|
||||
async bulkConfirm(organization: Organization) {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.memberDialogManager.openBulkConfirmDialog(
|
||||
organization,
|
||||
this.dataSource.getCheckedUsers(),
|
||||
@@ -451,7 +544,6 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
|
||||
async bulkEnableSM(organization: Organization) {
|
||||
const users = this.dataSource.getCheckedUsers();
|
||||
|
||||
await this.memberDialogManager.openBulkEnableSecretsManagerDialog(organization, users);
|
||||
|
||||
this.dataSource.uncheckAllUsers();
|
||||
@@ -482,14 +574,6 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
return;
|
||||
}
|
||||
|
||||
protected async removeUserConfirmationDialog(user: OrganizationUserView) {
|
||||
return await this.memberDialogManager.openRemoveUserConfirmationDialog(user);
|
||||
}
|
||||
|
||||
protected async revokeUserConfirmationDialog(user: OrganizationUserView) {
|
||||
return await this.memberDialogManager.openRevokeUserConfirmationDialog(user);
|
||||
}
|
||||
|
||||
async deleteUser(user: OrganizationUserView, organization: Organization) {
|
||||
const confirmed = await this.memberDialogManager.openDeleteUserConfirmationDialog(
|
||||
user,
|
||||
@@ -500,48 +584,32 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
return false;
|
||||
}
|
||||
|
||||
this.actionPromise = this.memberActionsService.deleteUser(organization, user.id);
|
||||
try {
|
||||
const result = await this.actionPromise;
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("organizationUserDeleted", this.userNamePipe.transform(user)),
|
||||
const result = await this.memberActionsService.deleteUser(organization, user.id);
|
||||
await this.handleMemberActionResult(result, "organizationUserDeleted", user, () => {
|
||||
this.dataSource.removeUser(user);
|
||||
});
|
||||
this.dataSource.removeUser(user);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = undefined;
|
||||
}
|
||||
|
||||
get showBulkRestoreUsers(): boolean {
|
||||
return this.dataSource
|
||||
.getCheckedUsers()
|
||||
.every((member) => member.status == this.userStatusType.Revoked);
|
||||
}
|
||||
|
||||
get showBulkRevokeUsers(): boolean {
|
||||
return this.dataSource
|
||||
.getCheckedUsers()
|
||||
.every((member) => member.status != this.userStatusType.Revoked);
|
||||
}
|
||||
|
||||
get showBulkRemoveUsers(): boolean {
|
||||
return this.dataSource.getCheckedUsers().every((member) => !member.managedByOrganization);
|
||||
}
|
||||
|
||||
get showBulkDeleteUsers(): boolean {
|
||||
const validStatuses = [
|
||||
this.userStatusType.Accepted,
|
||||
this.userStatusType.Confirmed,
|
||||
this.userStatusType.Revoked,
|
||||
];
|
||||
|
||||
return this.dataSource
|
||||
.getCheckedUsers()
|
||||
.every((member) => member.managedByOrganization && validStatuses.includes(member.status));
|
||||
async handleMemberActionResult(
|
||||
result: MemberActionResult,
|
||||
successKey: string,
|
||||
user: OrganizationUserView,
|
||||
sideEffect?: () => void | Promise<void>,
|
||||
) {
|
||||
if (result.success) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t(successKey, this.userNamePipe.transform(user)),
|
||||
});
|
||||
if (sideEffect) {
|
||||
await sideEffect();
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
@let providerId = providerId$ | async;
|
||||
<app-header>
|
||||
<bit-search class="tw-grow" [formControl]="searchControl" [placeholder]="'searchMembers' | i18n">
|
||||
</bit-search>
|
||||
<button type="button" bitButton buttonType="primary" (click)="invite()">
|
||||
<button type="button" bitButton buttonType="primary" (click)="invite(providerId)">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "inviteMember" | i18n }}
|
||||
</button>
|
||||
@@ -13,28 +14,28 @@
|
||||
(selectedChange)="statusToggle.next($event)"
|
||||
[attr.aria-label]="'memberStatusFilter' | i18n"
|
||||
>
|
||||
<bit-toggle [value]="null">
|
||||
<bit-toggle [value]="undefined">
|
||||
{{ "all" | i18n }}
|
||||
<span bitBadge variant="info" *ngIf="dataSource.activeUserCount as allCount">
|
||||
{{ allCount }}
|
||||
</span>
|
||||
@if (dataSource.activeUserCount; as allCount) {
|
||||
<span bitBadge variant="info">{{ allCount }}</span>
|
||||
}
|
||||
</bit-toggle>
|
||||
<bit-toggle [value]="userStatusType.Invited">
|
||||
{{ "invited" | i18n }}
|
||||
<span bitBadge variant="info" *ngIf="dataSource.invitedUserCount as invitedCount">
|
||||
{{ invitedCount }}
|
||||
</span>
|
||||
@if (dataSource.invitedUserCount; as invitedCount) {
|
||||
<span bitBadge variant="info">{{ invitedCount }}</span>
|
||||
}
|
||||
</bit-toggle>
|
||||
<bit-toggle [value]="userStatusType.Accepted">
|
||||
{{ "needsConfirmation" | i18n }}
|
||||
<span bitBadge variant="info" *ngIf="dataSource.acceptedUserCount as acceptedCount">
|
||||
{{ acceptedCount }}
|
||||
</span>
|
||||
@if (dataSource.acceptedUserCount; as acceptedCount) {
|
||||
<span bitBadge variant="info">{{ acceptedCount }}</span>
|
||||
}
|
||||
</bit-toggle>
|
||||
</bit-toggle-group>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!firstLoaded">
|
||||
@if (!firstLoaded()) {
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
@@ -42,19 +43,16 @@
|
||||
>
|
||||
</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"
|
||||
>
|
||||
{{ "providerUsersNeedConfirmed" | i18n }}
|
||||
</bit-callout>
|
||||
} @else {
|
||||
@if (!dataSource.filteredData?.length) {
|
||||
<p>{{ "noMembersInList" | i18n }}</p>
|
||||
}
|
||||
@if (dataSource.filteredData?.length) {
|
||||
@if (showConfirmUsers) {
|
||||
<bit-callout type="info" title="{{ 'confirmUsers' | i18n }}" icon="bwi-check-circle">
|
||||
{{ "providerUsersNeedConfirmed" | i18n }}
|
||||
</bit-callout>
|
||||
}
|
||||
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
@@ -82,27 +80,21 @@
|
||||
label="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #headerMenu>
|
||||
<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)="bulkRemove()">
|
||||
@if (showBulkReinviteUsers) {
|
||||
<button type="button" bitMenuItem (click)="bulkReinvite(providerId)">
|
||||
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||
{{ "reinviteSelected" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (showBulkConfirmUsers) {
|
||||
<button type="button" bitMenuItem (click)="bulkConfirm(providerId)">
|
||||
<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)="bulkRemove(providerId)">
|
||||
<span class="tw-text-danger">
|
||||
<i aria-hidden="true" class="bwi bwi-close"></i>
|
||||
{{ "remove" | i18n }}
|
||||
@@ -122,7 +114,7 @@
|
||||
<td bitCell (click)="dataSource.checkUser(user)">
|
||||
<input type="checkbox" bitCheckbox [(ngModel)]="$any(user).checked" />
|
||||
</td>
|
||||
<td bitCell (click)="edit(user)" class="tw-cursor-pointer">
|
||||
<td bitCell (click)="edit(user, providerId)" class="tw-cursor-pointer">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
size="small"
|
||||
@@ -132,44 +124,41 @@
|
||||
class="tw-mr-3"
|
||||
></bit-avatar>
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<div>
|
||||
<div class="tw-flex tw-flex-row tw-gap-2">
|
||||
<button type="button" bitLink>
|
||||
{{ user.name ?? user.email }}
|
||||
</button>
|
||||
<span
|
||||
bitBadge
|
||||
class="tw-text-xs"
|
||||
variant="secondary"
|
||||
*ngIf="user.status === userStatusType.Invited"
|
||||
>
|
||||
{{ "invited" | i18n }}
|
||||
</span>
|
||||
<span
|
||||
bitBadge
|
||||
class="tw-text-xs"
|
||||
variant="warning"
|
||||
*ngIf="user.status === userStatusType.Accepted"
|
||||
>
|
||||
{{ "needsConfirmation" | i18n }}
|
||||
</span>
|
||||
<span
|
||||
bitBadge
|
||||
class="tw-text-xs"
|
||||
variant="secondary"
|
||||
*ngIf="user.status === userStatusType.Revoked"
|
||||
>
|
||||
{{ "revoked" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tw-text-sm tw-text-muted" *ngIf="user.name">
|
||||
{{ user.email }}
|
||||
@if (user.status === userStatusType.Invited) {
|
||||
<span bitBadge class="tw-text-xs" variant="secondary">
|
||||
{{ "invited" | i18n }}
|
||||
</span>
|
||||
}
|
||||
@if (user.status === userStatusType.Accepted) {
|
||||
<span bitBadge class="tw-text-xs" variant="warning">
|
||||
{{ "needsConfirmation" | i18n }}
|
||||
</span>
|
||||
}
|
||||
@if (user.status === userStatusType.Revoked) {
|
||||
<span bitBadge class="tw-text-xs" variant="secondary">
|
||||
{{ "revoked" | i18n }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
@if (user.name) {
|
||||
<div class="tw-text-sm tw-text-muted">
|
||||
{{ user.email }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td bitCell class="tw-text-muted">
|
||||
<span *ngIf="user.type === userType.ProviderAdmin">{{ "providerAdmin" | i18n }}</span>
|
||||
<span *ngIf="user.type === userType.ServiceUser">{{ "serviceUser" | i18n }}</span>
|
||||
@if (user.type === userType.ProviderAdmin) {
|
||||
<span>{{ "providerAdmin" | i18n }}</span>
|
||||
}
|
||||
@if (user.type === userType.ServiceUser) {
|
||||
<span>{{ "serviceUser" | i18n }}</span>
|
||||
}
|
||||
</td>
|
||||
<td bitCell>
|
||||
<button
|
||||
@@ -180,36 +169,27 @@
|
||||
label="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #rowMenu>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="reinvite(user)"
|
||||
*ngIf="user.status === userStatusType.Invited"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
||||
{{ "resendInvitation" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="confirm(user)"
|
||||
*ngIf="user.status === userStatusType.Accepted"
|
||||
>
|
||||
<span class="tw-text-success">
|
||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||
{{ "confirm" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="openEventsDialog(user)"
|
||||
*ngIf="user.status === userStatusType.Confirmed"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
|
||||
{{ "eventLogs" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="remove(user)">
|
||||
@if (user.status === userStatusType.Invited) {
|
||||
<button type="button" bitMenuItem (click)="reinvite(user, providerId)">
|
||||
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
||||
{{ "resendInvitation" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (user.status === userStatusType.Accepted) {
|
||||
<button type="button" bitMenuItem (click)="confirm(user, providerId)">
|
||||
<span class="tw-text-success">
|
||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||
{{ "confirm" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
@if (accessEvents && user.status === userStatusType.Confirmed) {
|
||||
<button type="button" bitMenuItem (click)="openEventsDialog(user, providerId)">
|
||||
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
|
||||
{{ "eventLogs" | i18n }}
|
||||
</button>
|
||||
}
|
||||
<button type="button" bitMenuItem (click)="remove(user, providerId)">
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||
{{ "remove" | i18n }}
|
||||
@@ -221,5 +201,5 @@
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component } from "@angular/core";
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
inject,
|
||||
signal,
|
||||
WritableSignal,
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormControl } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { combineLatest, firstValueFrom, lastValueFrom, switchMap } from "rxjs";
|
||||
import { first, map } from "rxjs/operators";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
debounceTime,
|
||||
firstValueFrom,
|
||||
lastValueFrom,
|
||||
Observable,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
import { first, map, tap } from "rxjs/operators";
|
||||
|
||||
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { ProviderUserStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request";
|
||||
import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-confirm.request";
|
||||
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response";
|
||||
@@ -18,19 +33,19 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { assertNonNullish } from "@bitwarden/common/auth/utils";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ProviderId } from "@bitwarden/common/types/guid";
|
||||
import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { BaseMembersComponent } from "@bitwarden/web-vault/app/admin-console/common/base-members.component";
|
||||
import {
|
||||
peopleFilter,
|
||||
PeopleTableDataSource,
|
||||
} from "@bitwarden/web-vault/app/admin-console/common/people-table-data-source";
|
||||
import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component";
|
||||
import { UserConfirmComponent } from "@bitwarden/web-vault/app/admin-console/organizations/manage/user-confirm.component";
|
||||
import { BulkStatusComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component";
|
||||
import { MemberActionResult } from "@bitwarden/web-vault/app/admin-console/organizations/members/services/member-actions/member-actions.service";
|
||||
|
||||
@@ -48,79 +63,113 @@ class MembersTableDataSource extends PeopleTableDataSource<ProviderUser> {
|
||||
protected statusType = ProviderUserStatusType;
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "members.component.html",
|
||||
standalone: false,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MembersComponent extends BaseMembersComponent<ProviderUser> {
|
||||
accessEvents = false;
|
||||
dataSource = new MembersTableDataSource();
|
||||
loading = true;
|
||||
providerId: string;
|
||||
rowHeight = 70;
|
||||
rowHeightClass = `tw-h-[70px]`;
|
||||
status: ProviderUserStatusType = null;
|
||||
export class MembersComponent {
|
||||
protected apiService = inject(ApiService);
|
||||
protected i18nService = inject(I18nService);
|
||||
protected keyService = inject(KeyService);
|
||||
protected validationService = inject(ValidationService);
|
||||
protected logService = inject(LogService);
|
||||
protected userNamePipe = inject(UserNamePipe);
|
||||
protected dialogService = inject(DialogService);
|
||||
protected organizationManagementPreferencesService = inject(
|
||||
OrganizationManagementPreferencesService,
|
||||
);
|
||||
protected toastService = inject(ToastService);
|
||||
private encryptService = inject(EncryptService);
|
||||
private activatedRoute = inject(ActivatedRoute);
|
||||
private providerService = inject(ProviderService);
|
||||
private router = inject(Router);
|
||||
private accountService = inject(AccountService);
|
||||
private changeDetectorRef = inject(ChangeDetectorRef);
|
||||
|
||||
userStatusType = ProviderUserStatusType;
|
||||
userType = ProviderUserType;
|
||||
protected accessEvents = false;
|
||||
protected dataSource = new MembersTableDataSource();
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
keyService: KeyService,
|
||||
dialogService: DialogService,
|
||||
i18nService: I18nService,
|
||||
logService: LogService,
|
||||
organizationManagementPreferencesService: OrganizationManagementPreferencesService,
|
||||
toastService: ToastService,
|
||||
userNamePipe: UserNamePipe,
|
||||
validationService: ValidationService,
|
||||
private encryptService: EncryptService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private providerService: ProviderService,
|
||||
private router: Router,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
i18nService,
|
||||
keyService,
|
||||
validationService,
|
||||
logService,
|
||||
userNamePipe,
|
||||
dialogService,
|
||||
organizationManagementPreferencesService,
|
||||
toastService,
|
||||
protected providerId$: Observable<ProviderId>;
|
||||
protected provider$: Observable<Provider | undefined>;
|
||||
|
||||
protected rowHeight = 70;
|
||||
protected rowHeightClass = `tw-h-[70px]`;
|
||||
protected status: ProviderUserStatusType | undefined;
|
||||
|
||||
protected userStatusType = ProviderUserStatusType;
|
||||
protected userType = ProviderUserType;
|
||||
|
||||
protected searchControl = new FormControl("", { nonNullable: true });
|
||||
protected statusToggle = new BehaviorSubject<ProviderUserStatusType | undefined>(undefined);
|
||||
protected readonly firstLoaded: WritableSignal<boolean> = signal(false);
|
||||
|
||||
/**
|
||||
* Shows a banner alerting the admin that users need to be confirmed.
|
||||
*/
|
||||
get showConfirmUsers(): boolean {
|
||||
return (
|
||||
this.dataSource.activeUserCount > 1 &&
|
||||
this.dataSource.confirmedUserCount > 0 &&
|
||||
this.dataSource.confirmedUserCount < 3 &&
|
||||
this.dataSource.acceptedUserCount > 0
|
||||
);
|
||||
}
|
||||
|
||||
get showBulkConfirmUsers(): boolean {
|
||||
return this.dataSource
|
||||
.getCheckedUsers()
|
||||
.every((member) => member.status == this.userStatusType.Accepted);
|
||||
}
|
||||
|
||||
get showBulkReinviteUsers(): boolean {
|
||||
return this.dataSource
|
||||
.getCheckedUsers()
|
||||
.every((member) => member.status == this.userStatusType.Invited);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.dataSource
|
||||
.connect()
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe(() => {
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Connect the search input and status toggles to the table dataSource filter
|
||||
combineLatest([this.searchControl.valueChanges.pipe(debounceTime(200)), this.statusToggle])
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe(
|
||||
([searchText, status]) => (this.dataSource.filter = peopleFilter(searchText, status)),
|
||||
);
|
||||
|
||||
this.providerId$ = this.activatedRoute.params.pipe(map((params) => params.providerId));
|
||||
|
||||
this.provider$ = combineLatest([
|
||||
this.providerId$,
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
]).pipe(
|
||||
switchMap(([providerId, userId]) => this.providerService.get$(providerId, userId)),
|
||||
tap(async (provider) => {
|
||||
if (!provider || !provider.canManageUsers) {
|
||||
return await this.router.navigate(["../"], { relativeTo: this.activatedRoute });
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
combineLatest([
|
||||
this.activatedRoute.parent.params,
|
||||
this.activatedRoute.queryParams.pipe(first()),
|
||||
])
|
||||
combineLatest([this.activatedRoute.queryParams, this.providerId$])
|
||||
.pipe(
|
||||
switchMap(async ([urlParams, queryParams]) => {
|
||||
first(),
|
||||
switchMap(async ([queryParams, providerId]) => {
|
||||
this.searchControl.setValue(queryParams.search);
|
||||
this.dataSource.filter = peopleFilter(queryParams.search, null);
|
||||
this.dataSource.filter = peopleFilter(queryParams.search, undefined);
|
||||
|
||||
this.providerId = urlParams.providerId;
|
||||
const provider = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.providerService.get$(this.providerId, userId)),
|
||||
),
|
||||
);
|
||||
|
||||
if (!provider || !provider.canManageUsers) {
|
||||
return await this.router.navigate(["../"], { relativeTo: this.activatedRoute });
|
||||
}
|
||||
this.accessEvents = provider.useEvents;
|
||||
await this.load();
|
||||
|
||||
if (queryParams.viewEvents != null) {
|
||||
const user = this.dataSource.data.find((user) => user.id === queryParams.viewEvents);
|
||||
if (user && user.status === ProviderUserStatusType.Confirmed) {
|
||||
this.openEventsDialog(user);
|
||||
this.openEventsDialog(user, providerId);
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -129,14 +178,17 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async bulkConfirm(): Promise<void> {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
async load() {
|
||||
const providerId = await firstValueFrom(this.providerId$);
|
||||
const response = await this.apiService.getProviderUsers(providerId);
|
||||
this.dataSource.data = response.data != null && response.data.length > 0 ? response.data : [];
|
||||
this.firstLoaded.set(true);
|
||||
}
|
||||
|
||||
async bulkConfirm(providerId: ProviderId): Promise<void> {
|
||||
const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
providerId: this.providerId,
|
||||
providerId: providerId,
|
||||
users: this.dataSource.getCheckedUsers(),
|
||||
},
|
||||
});
|
||||
@@ -145,11 +197,7 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
async bulkReinvite(): Promise<void> {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
async bulkReinvite(providerId: ProviderId): Promise<void> {
|
||||
const checkedUsers = this.dataSource.getCheckedUsers();
|
||||
const checkedInvitedUsers = checkedUsers.filter(
|
||||
(user) => user.status === ProviderUserStatusType.Invited,
|
||||
@@ -166,7 +214,7 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
|
||||
|
||||
try {
|
||||
const request = this.apiService.postManyProviderUserReinvite(
|
||||
this.providerId,
|
||||
providerId,
|
||||
new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)),
|
||||
);
|
||||
|
||||
@@ -184,18 +232,14 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
|
||||
}
|
||||
}
|
||||
|
||||
async invite() {
|
||||
await this.edit(null);
|
||||
async invite(providerId: ProviderId) {
|
||||
await this.edit(null, providerId);
|
||||
}
|
||||
|
||||
async bulkRemove(): Promise<void> {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
async bulkRemove(providerId: ProviderId): Promise<void> {
|
||||
const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
providerId: this.providerId,
|
||||
providerId: providerId,
|
||||
users: this.dataSource.getCheckedUsers(),
|
||||
},
|
||||
});
|
||||
@@ -204,13 +248,120 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
async confirmUser(user: ProviderUser, publicKey: Uint8Array): Promise<MemberActionResult> {
|
||||
private async removeUserConfirmationDialog(user: ProviderUser) {
|
||||
return this.dialogService.openSimpleDialog({
|
||||
title: this.userNamePipe.transform(user),
|
||||
content: { key: "removeUserConfirmation" },
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
|
||||
async remove(user: ProviderUser, providerId: ProviderId) {
|
||||
const confirmed = await this.removeUserConfirmationDialog(user);
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.removeUserInternal(user.id, providerId);
|
||||
if (result.success) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("removedUserId", this.userNamePipe.transform(user)),
|
||||
});
|
||||
this.dataSource.removeUser(user);
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
}
|
||||
|
||||
async reinvite(user: ProviderUser, providerId: ProviderId) {
|
||||
try {
|
||||
const result = await this.reinviteUserInternal(user.id, providerId);
|
||||
if (result.success) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user)),
|
||||
});
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
}
|
||||
|
||||
async confirm(user: ProviderUser, providerId: ProviderId) {
|
||||
const confirmUser = async (publicKey: Uint8Array) => {
|
||||
try {
|
||||
const result = await this.confirmUserInternal(user, publicKey, providerId);
|
||||
if (result.success) {
|
||||
user.status = this.userStatusType.Confirmed;
|
||||
this.dataSource.replaceUser(user);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user)),
|
||||
});
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
|
||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
|
||||
const autoConfirm = await firstValueFrom(
|
||||
this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$,
|
||||
);
|
||||
if (user == null) {
|
||||
throw new Error("Cannot confirm null user.");
|
||||
}
|
||||
if (autoConfirm == null || !autoConfirm) {
|
||||
const dialogRef = UserConfirmComponent.open(this.dialogService, {
|
||||
data: {
|
||||
name: this.userNamePipe.transform(user),
|
||||
userId: user.userId,
|
||||
publicKey: publicKey,
|
||||
confirmUser: () => confirmUser(publicKey),
|
||||
},
|
||||
});
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fingerprint = await this.keyService.getFingerprint(user.userId, publicKey);
|
||||
this.logService.info(`User's fingerprint: ${fingerprint.join("-")}`);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
await confirmUser(publicKey);
|
||||
} catch (e) {
|
||||
this.logService.error(`Handled exception: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async confirmUserInternal(
|
||||
user: ProviderUser,
|
||||
publicKey: Uint8Array,
|
||||
providerId: ProviderId,
|
||||
): Promise<MemberActionResult> {
|
||||
try {
|
||||
const providerKey = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.providerKeys$(userId)),
|
||||
map((providerKeys) => providerKeys?.[this.providerId as ProviderId] ?? null),
|
||||
map((providerKeys) => providerKeys?.[providerId as ProviderId] ?? null),
|
||||
),
|
||||
);
|
||||
assertNonNullish(providerKey, "Provider key not found");
|
||||
@@ -218,25 +369,28 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
|
||||
const key = await this.encryptService.encapsulateKeyUnsigned(providerKey, publicKey);
|
||||
const request = new ProviderUserConfirmRequest();
|
||||
request.key = key.encryptedString;
|
||||
await this.apiService.postProviderUserConfirm(this.providerId, user.id, request);
|
||||
await this.apiService.postProviderUserConfirm(providerId, user.id, request);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
removeUser = async (id: string): Promise<MemberActionResult> => {
|
||||
private async removeUserInternal(
|
||||
id: string,
|
||||
providerId: ProviderId,
|
||||
): Promise<MemberActionResult> {
|
||||
try {
|
||||
await this.apiService.deleteProviderUser(this.providerId, id);
|
||||
await this.apiService.deleteProviderUser(providerId, id);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
edit = async (user: ProviderUser | null): Promise<void> => {
|
||||
edit = async (user: ProviderUser | null, providerId: ProviderId): Promise<void> => {
|
||||
const data: AddEditMemberDialogParams = {
|
||||
providerId: this.providerId,
|
||||
providerId: providerId,
|
||||
};
|
||||
|
||||
if (user != null) {
|
||||
@@ -261,26 +415,26 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
|
||||
}
|
||||
};
|
||||
|
||||
openEventsDialog = (user: ProviderUser): DialogRef<void> =>
|
||||
openEventsDialog = (user: ProviderUser, providerId: ProviderId): DialogRef<void> =>
|
||||
openEntityEventsDialog(this.dialogService, {
|
||||
data: {
|
||||
name: this.userNamePipe.transform(user),
|
||||
providerId: this.providerId,
|
||||
providerId: providerId,
|
||||
entityId: user.id,
|
||||
showUser: false,
|
||||
entity: "user",
|
||||
},
|
||||
});
|
||||
|
||||
getUsers = (): Promise<ListResponse<ProviderUser>> =>
|
||||
this.apiService.getProviderUsers(this.providerId);
|
||||
|
||||
reinviteUser = async (id: string): Promise<MemberActionResult> => {
|
||||
private async reinviteUserInternal(
|
||||
id: string,
|
||||
providerId: ProviderId,
|
||||
): Promise<MemberActionResult> {
|
||||
try {
|
||||
await this.apiService.postProviderUserReinvite(this.providerId, id);
|
||||
await this.apiService.postProviderUserReinvite(providerId, id);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user