1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-20 11:24:07 +00:00
Files
browser/apps/web/src/app/admin-console/organizations/members/members.component.ts
Brandon Treston d4952d211e [PM-24096] replace getOrgKey with orgKey$, refactor collectionAdminService (#15928)
* replace getOrgKey with orgKey$, refactor collectionAdminService

* clean up

* uncomment accidental commet

* remove cache
2025-08-12 12:06:55 -04:00

958 lines
31 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import {
combineLatest,
concatMap,
firstValueFrom,
from,
lastValueFrom,
map,
Observable,
shareReplay,
switchMap,
tap,
} from "rxjs";
import {
OrganizationUserApiService,
OrganizationUserConfirmRequest,
OrganizationUserUserDetailsResponse,
CollectionService,
CollectionData,
Collection,
CollectionDetailsResponse,
} from "@bitwarden/admin-console/common";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import {
getOrganizationById,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import { PolicyApiServiceAbstraction as PolicyApiService } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import {
OrganizationUserStatusType,
OrganizationUserType,
PolicyType,
} from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import {
ChangePlanDialogResultType,
openChangePlanDialog,
} from "../../../billing/organizations/change-plan-dialog.component";
import { OrganizationWarningsService } from "../../../billing/warnings/services";
import { BaseMembersComponent } from "../../common/base-members.component";
import { PeopleTableDataSource } from "../../common/people-table-data-source";
import { GroupApiService } from "../core";
import { OrganizationUserView } from "../core/views/organization-user.view";
import { openEntityEventsDialog } from "../manage/entity-events.component";
import {
AccountRecoveryDialogComponent,
AccountRecoveryDialogResultType,
} from "./components/account-recovery/account-recovery-dialog.component";
import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component";
import { BulkDeleteDialogComponent } from "./components/bulk/bulk-delete-dialog.component";
import { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component";
import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.component";
import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component";
import { BulkStatusComponent } from "./components/bulk/bulk-status.component";
import {
MemberDialogResult,
MemberDialogTab,
openUserAddEditDialog,
} from "./components/member-dialog";
import { isFixedSeatPlan } from "./components/member-dialog/validators/org-seat-limit-reached.validator";
import { DeleteManagedMemberWarningService } from "./services/delete-managed-member/delete-managed-member-warning.service";
import { OrganizationUserService } from "./services/organization-user/organization-user.service";
class MembersTableDataSource extends PeopleTableDataSource<OrganizationUserView> {
protected statusType = OrganizationUserStatusType;
}
@Component({
templateUrl: "members.component.html",
standalone: false,
})
export class MembersComponent extends BaseMembersComponent<OrganizationUserView> {
userType = OrganizationUserType;
userStatusType = OrganizationUserStatusType;
memberTab = MemberDialogTab;
protected dataSource = new MembersTableDataSource();
organization: Organization;
status: OrganizationUserStatusType = null;
orgResetPasswordPolicyEnabled = false;
orgIsOnSecretsManagerStandalone = false;
protected canUseSecretsManager$: Observable<boolean>;
protected showUserManagementControls$: Observable<boolean>;
// Fixed sizes used for cdkVirtualScroll
protected rowHeight = 66;
protected rowHeightClass = `tw-h-[66px]`;
private organizationUsersCount = 0;
get occupiedSeatCount(): number {
return this.organizationUsersCount;
}
constructor(
apiService: ApiService,
i18nService: I18nService,
organizationManagementPreferencesService: OrganizationManagementPreferencesService,
keyService: KeyService,
private encryptService: EncryptService,
validationService: ValidationService,
logService: LogService,
userNamePipe: UserNamePipe,
dialogService: DialogService,
toastService: ToastService,
private policyService: PolicyService,
private policyApiService: PolicyApiService,
private route: ActivatedRoute,
private syncService: SyncService,
private organizationService: OrganizationService,
private accountService: AccountService,
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserApiService: OrganizationUserApiService,
private router: Router,
private groupService: GroupApiService,
private collectionService: CollectionService,
private billingApiService: BillingApiServiceAbstraction,
protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
private configService: ConfigService,
private organizationUserService: OrganizationUserService,
private organizationWarningsService: OrganizationWarningsService,
) {
super(
apiService,
i18nService,
keyService,
validationService,
logService,
userNamePipe,
dialogService,
organizationManagementPreferencesService,
toastService,
);
const organization$ = this.route.params.pipe(
concatMap((params) =>
this.accountService.activeAccount$.pipe(
switchMap((account) =>
this.organizationService
.organizations$(account?.id)
.pipe(getOrganizationById(params.organizationId)),
),
),
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
this.canUseSecretsManager$ = organization$.pipe(map((org) => org.useSecretsManager));
const policies$ = combineLatest([
this.accountService.activeAccount$.pipe(getUserId),
organization$,
]).pipe(
switchMap(([userId, organization]) => {
if (organization.isProviderUser) {
return from(this.policyApiService.getPolicies(organization.id)).pipe(
map((response) => Policy.fromListResponse(response)),
);
}
return this.policyService.policies$(userId);
}),
);
combineLatest([this.route.queryParams, policies$, organization$])
.pipe(
concatMap(async ([qParams, policies, organization]) => {
this.organization = organization;
// Backfill pub/priv key if necessary
if (
this.organization.canManageUsersPassword &&
!this.organization.hasPublicAndPrivateKeys
) {
const orgShareKey = await firstValueFrom(
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.keyService.orgKeys$(userId)),
map((orgKeys) => orgKeys[this.organization.id] ?? null),
),
);
const orgKeys = await this.keyService.makeKeyPair(orgShareKey);
const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
const response = await this.organizationApiService.updateKeys(
this.organization.id,
request,
);
if (response != null) {
this.organization.hasPublicAndPrivateKeys =
response.publicKey != null && response.privateKey != null;
await this.syncService.fullSync(true); // Replace organizations with new data
} else {
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
}
}
const resetPasswordPolicy = policies
.filter((policy) => policy.type === PolicyType.ResetPassword)
.find((p) => p.organizationId === this.organization.id);
this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled;
const billingMetadata = await this.billingApiService.getOrganizationBillingMetadata(
this.organization.id,
);
this.orgIsOnSecretsManagerStandalone = billingMetadata.isOnSecretsManagerStandalone;
this.organizationUsersCount = billingMetadata.organizationOccupiedSeats;
await this.load();
this.searchControl.setValue(qParams.search);
if (qParams.viewEvents != null) {
const user = this.dataSource.data.filter((u) => u.id === qParams.viewEvents);
if (user.length > 0 && user[0].status === OrganizationUserStatusType.Confirmed) {
this.openEventsDialog(user[0]);
}
}
}),
takeUntilDestroyed(),
)
.subscribe();
this.showUserManagementControls$ = organization$.pipe(
map((organization) => organization.canManageUsers),
);
organization$
.pipe(
takeUntilDestroyed(),
tap((org) => (this.organization = org)),
switchMap((org) => this.organizationWarningsService.showInactiveSubscriptionDialog$(org)),
)
.subscribe();
}
async getUsers(): Promise<OrganizationUserView[]> {
let groupsPromise: Promise<Map<string, string>>;
let collectionsPromise: Promise<Map<string, string>>;
// We don't need both groups and collections for the table, so only load one
const userPromise = this.organizationUserApiService.getAllUsers(this.organization.id, {
includeGroups: this.organization.useGroups,
includeCollections: !this.organization.useGroups,
});
// Depending on which column is displayed, we need to load the group/collection names
if (this.organization.useGroups) {
groupsPromise = this.getGroupNameMap();
} else {
collectionsPromise = this.getCollectionNameMap();
}
const [usersResponse, groupNamesMap, collectionNamesMap] = await Promise.all([
userPromise,
groupsPromise,
collectionsPromise,
]);
return usersResponse.data?.map<OrganizationUserView>((r) => {
const userView = OrganizationUserView.fromResponse(r);
userView.groupNames = userView.groups
.map((g) => groupNamesMap.get(g))
.sort(this.i18nService.collator?.compare);
userView.collectionNames = userView.collections
.map((c) => collectionNamesMap.get(c.id))
.sort(this.i18nService.collator?.compare);
return userView;
});
}
async getGroupNameMap(): Promise<Map<string, string>> {
const groups = await this.groupService.getAll(this.organization.id);
const groupNameMap = new Map<string, string>();
groups.forEach((g) => groupNameMap.set(g.id, g.name));
return groupNameMap;
}
/**
* Retrieve a map of all collection IDs <-> names for the organization.
*/
async getCollectionNameMap() {
const response = from(this.apiService.getCollections(this.organization.id)).pipe(
map((res) =>
res.data.map((r) => new Collection(new CollectionData(r as CollectionDetailsResponse))),
),
);
const decryptedCollections$ = combineLatest([
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.keyService.orgKeys$(userId)),
),
response,
]).pipe(
switchMap(([orgKeys, collections]) =>
this.collectionService.decryptMany$(collections, orgKeys),
),
map((collections) => {
const collectionMap = new Map<string, string>();
collections.forEach((c) => collectionMap.set(c.id, c.name));
return collectionMap;
}),
);
return await firstValueFrom(decryptedCollections$);
}
removeUser(id: string): Promise<void> {
return this.organizationUserApiService.removeOrganizationUser(this.organization.id, id);
}
revokeUser(id: string): Promise<void> {
return this.organizationUserApiService.revokeOrganizationUser(this.organization.id, id);
}
restoreUser(id: string): Promise<void> {
return this.organizationUserApiService.restoreOrganizationUser(this.organization.id, id);
}
reinviteUser(id: string): Promise<void> {
return this.organizationUserApiService.postOrganizationUserReinvite(this.organization.id, id);
}
async confirmUser(user: OrganizationUserView, publicKey: Uint8Array): Promise<void> {
if (
await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation))
) {
await firstValueFrom(
this.organizationUserService.confirmUser(this.organization, user, publicKey),
);
} else {
const orgKey = await firstValueFrom(
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.keyService.orgKeys$(userId)),
map((orgKeys) => orgKeys[this.organization.id] ?? null),
),
);
const key = await this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey);
const request = new OrganizationUserConfirmRequest();
request.key = key.encryptedString;
await this.organizationUserApiService.postOrganizationUserConfirm(
this.organization.id,
user.id,
request,
);
}
}
async revoke(user: OrganizationUserView) {
const confirmed = await this.revokeUserConfirmationDialog(user);
if (!confirmed) {
return false;
}
this.actionPromise = this.revokeUser(user.id);
try {
await this.actionPromise;
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("revokedUserId", this.userNamePipe.transform(user)),
});
await this.load();
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
async restore(user: OrganizationUserView) {
this.actionPromise = this.restoreUser(user.id);
try {
await this.actionPromise;
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("restoredUserId", this.userNamePipe.transform(user)),
});
await this.load();
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
allowResetPassword(orgUser: OrganizationUserView): boolean {
// Hierarchy check
let callingUserHasPermission = false;
switch (this.organization.type) {
case OrganizationUserType.Owner:
callingUserHasPermission = true;
break;
case OrganizationUserType.Admin:
callingUserHasPermission = orgUser.type !== OrganizationUserType.Owner;
break;
case OrganizationUserType.Custom:
callingUserHasPermission =
orgUser.type !== OrganizationUserType.Owner &&
orgUser.type !== OrganizationUserType.Admin;
break;
}
// Final
return (
this.organization.canManageUsersPassword &&
callingUserHasPermission &&
this.organization.useResetPassword &&
this.organization.hasPublicAndPrivateKeys &&
orgUser.resetPasswordEnrolled &&
this.orgResetPasswordPolicyEnabled &&
orgUser.status === OrganizationUserStatusType.Confirmed
);
}
showEnrolledStatus(orgUser: OrganizationUserUserDetailsResponse): boolean {
return (
this.organization.useResetPassword &&
orgUser.resetPasswordEnrolled &&
this.orgResetPasswordPolicyEnabled
);
}
private getManageBillingText(): string {
return this.organization.canEditSubscription ? "ManageBilling" : "NoManageBilling";
}
private getProductKey(productType: ProductTierType): string {
let product = "";
switch (productType) {
case ProductTierType.Free:
product = "freeOrg";
break;
case ProductTierType.TeamsStarter:
product = "teamsStarterPlan";
break;
case ProductTierType.Families:
product = "familiesPlan";
break;
default:
throw new Error(`Unsupported product type: ${productType}`);
}
return `${product}InvLimitReached${this.getManageBillingText()}`;
}
private getDialogContent(): string {
return this.i18nService.t(
this.getProductKey(this.organization.productTierType),
this.organization.seats,
);
}
private getAcceptButtonText(): string {
if (!this.organization.canEditSubscription) {
return this.i18nService.t("ok");
}
const productType = this.organization.productTierType;
if (isNotSelfUpgradable(productType)) {
throw new Error(`Unsupported product type: ${productType}`);
}
return this.i18nService.t("upgrade");
}
private async handleDialogClose(result: boolean | undefined): Promise<void> {
if (!result || !this.organization.canEditSubscription) {
return;
}
const productType = this.organization.productTierType;
if (isNotSelfUpgradable(productType)) {
throw new Error(`Unsupported product type: ${this.organization.productTierType}`);
}
await this.router.navigate(
["/organizations", this.organization.id, "billing", "subscription"],
{ queryParams: { upgrade: true } },
);
}
private async showSeatLimitReachedDialog(): Promise<void> {
const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = {
title: this.i18nService.t("upgradeOrganization"),
content: this.getDialogContent(),
type: "primary",
acceptButtonText: this.getAcceptButtonText(),
};
if (!this.organization.canEditSubscription) {
orgUpgradeSimpleDialogOpts.cancelButtonText = null;
}
const simpleDialog = this.dialogService.openSimpleDialogRef(orgUpgradeSimpleDialogOpts);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
firstValueFrom(simpleDialog.closed).then(this.handleDialogClose.bind(this));
}
private async handleInviteDialog() {
const dialog = openUserAddEditDialog(this.dialogService, {
data: {
kind: "Add",
organizationId: this.organization.id,
allOrganizationUserEmails: this.dataSource.data?.map((user) => user.email) ?? [],
occupiedSeatCount: this.occupiedSeatCount,
isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone,
},
});
const result = await lastValueFrom(dialog.closed);
if (result === MemberDialogResult.Saved) {
await this.load();
}
}
private async handleSeatLimitForFixedTiers() {
if (!this.organization.canEditSubscription) {
await this.showSeatLimitReachedDialog();
return;
}
const reference = openChangePlanDialog(this.dialogService, {
data: {
organizationId: this.organization.id,
subscription: null,
productTierType: this.organization.productTierType,
},
});
const result = await lastValueFrom(reference.closed);
if (result === ChangePlanDialogResultType.Submitted) {
await this.load();
}
}
async invite() {
if (this.organization.hasReseller && this.organization.seats === this.occupiedSeatCount) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("seatLimitReached"),
message: this.i18nService.t("contactYourProvider"),
});
return;
}
if (
this.occupiedSeatCount === this.organization.seats &&
isFixedSeatPlan(this.organization.productTierType)
) {
await this.handleSeatLimitForFixedTiers();
return;
}
await this.handleInviteDialog();
}
async edit(user: OrganizationUserView, initialTab: MemberDialogTab = MemberDialogTab.Role) {
const dialog = openUserAddEditDialog(this.dialogService, {
data: {
kind: "Edit",
name: this.userNamePipe.transform(user),
organizationId: this.organization.id,
organizationUserId: user.id,
usesKeyConnector: user.usesKeyConnector,
isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone,
initialTab: initialTab,
managedByOrganization: user.managedByOrganization,
},
});
const result = await lastValueFrom(dialog.closed);
switch (result) {
case MemberDialogResult.Deleted:
this.dataSource.removeUser(user);
break;
case MemberDialogResult.Saved:
case MemberDialogResult.Revoked:
case MemberDialogResult.Restored:
await this.load();
break;
}
}
async bulkRemove() {
if (this.actionPromise != null) {
return;
}
const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, {
data: {
organizationId: this.organization.id,
users: this.dataSource.getCheckedUsers(),
},
});
await lastValueFrom(dialogRef.closed);
await this.load();
}
async bulkDelete() {
const warningAcknowledged = await firstValueFrom(
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
);
if (
!warningAcknowledged &&
this.organization.canManageUsers &&
this.organization.productTierType === ProductTierType.Enterprise
) {
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
if (!acknowledged) {
return;
}
}
if (this.actionPromise != null) {
return;
}
const dialogRef = BulkDeleteDialogComponent.open(this.dialogService, {
data: {
organizationId: this.organization.id,
users: this.dataSource.getCheckedUsers(),
},
});
await lastValueFrom(dialogRef.closed);
await this.load();
}
async bulkRevoke() {
await this.bulkRevokeOrRestore(true);
}
async bulkRestore() {
await this.bulkRevokeOrRestore(false);
}
async bulkRevokeOrRestore(isRevoking: boolean) {
if (this.actionPromise != null) {
return;
}
const ref = BulkRestoreRevokeComponent.open(this.dialogService, {
organizationId: this.organization.id,
users: this.dataSource.getCheckedUsers(),
isRevoking: isRevoking,
});
await firstValueFrom(ref.closed);
await this.load();
}
async bulkReinvite() {
if (this.actionPromise != null) {
return;
}
const users = this.dataSource.getCheckedUsers();
const filteredUsers = users.filter((u) => u.status === OrganizationUserStatusType.Invited);
if (filteredUsers.length <= 0) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("noSelectedUsersApplicable"),
});
return;
}
try {
const response = this.organizationUserApiService.postManyOrganizationUserReinvite(
this.organization.id,
filteredUsers.map((user) => user.id),
);
// Bulk Status component open
const dialogRef = BulkStatusComponent.open(this.dialogService, {
data: {
users: users,
filteredUsers: filteredUsers,
request: response,
successfulMessage: this.i18nService.t("bulkReinviteMessage"),
},
});
await lastValueFrom(dialogRef.closed);
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
async bulkConfirm() {
if (this.actionPromise != null) {
return;
}
const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, {
data: {
organization: this.organization,
users: this.dataSource.getCheckedUsers(),
},
});
await lastValueFrom(dialogRef.closed);
await this.load();
}
async bulkEnableSM() {
const users = this.dataSource.getCheckedUsers().filter((ou) => !ou.accessSecretsManager);
if (users.length === 0) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("noSelectedUsersApplicable"),
});
return;
}
const dialogRef = BulkEnableSecretsManagerDialogComponent.open(this.dialogService, {
orgId: this.organization.id,
users,
});
await lastValueFrom(dialogRef.closed);
this.dataSource.uncheckAllUsers();
await this.load();
}
openEventsDialog(user: OrganizationUserView) {
openEntityEventsDialog(this.dialogService, {
data: {
name: this.userNamePipe.transform(user),
organizationId: this.organization.id,
entityId: user.id,
showUser: false,
entity: "user",
},
});
}
async resetPassword(user: OrganizationUserView) {
if (!user || !user.email || !user.id) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("orgUserDetailsNotFound"),
});
this.logService.error("Org user details not found when attempting account recovery");
return;
}
const dialogRef = AccountRecoveryDialogComponent.open(this.dialogService, {
data: {
name: this.userNamePipe.transform(user),
email: user.email,
organizationId: this.organization.id as OrganizationId,
organizationUserId: user.id,
},
});
const result = await lastValueFrom(dialogRef.closed);
if (result === AccountRecoveryDialogResultType.Ok) {
await this.load();
}
return;
}
protected async removeUserConfirmationDialog(user: OrganizationUserView) {
const content = user.usesKeyConnector
? "removeUserConfirmationKeyConnector"
: "removeOrgUserConfirmation";
const confirmed = await this.dialogService.openSimpleDialog({
title: {
key: "removeUserIdAccess",
placeholders: [this.userNamePipe.transform(user)],
},
content: { key: content },
type: "warning",
});
if (!confirmed) {
return false;
}
if (user.status > OrganizationUserStatusType.Invited && user.hasMasterPassword === false) {
return await this.noMasterPasswordConfirmationDialog(user);
}
return true;
}
protected async revokeUserConfirmationDialog(user: OrganizationUserView) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "revokeAccess", placeholders: [this.userNamePipe.transform(user)] },
content: this.i18nService.t("revokeUserConfirmation"),
acceptButtonText: { key: "revokeAccess" },
type: "warning",
});
if (!confirmed) {
return false;
}
if (user.status > OrganizationUserStatusType.Invited && user.hasMasterPassword === false) {
return await this.noMasterPasswordConfirmationDialog(user);
}
return true;
}
async deleteUser(user: OrganizationUserView) {
const warningAcknowledged = await firstValueFrom(
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
);
if (
!warningAcknowledged &&
this.organization.canManageUsers &&
this.organization.productTierType === ProductTierType.Enterprise
) {
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
if (!acknowledged) {
return false;
}
}
const confirmed = await this.dialogService.openSimpleDialog({
title: {
key: "deleteOrganizationUser",
placeholders: [this.userNamePipe.transform(user)],
},
content: {
key: "deleteOrganizationUserWarningDesc",
placeholders: [this.userNamePipe.transform(user)],
},
type: "warning",
acceptButtonText: { key: "delete" },
cancelButtonText: { key: "cancel" },
});
if (!confirmed) {
return false;
}
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organization.id);
this.actionPromise = this.organizationUserApiService.deleteOrganizationUser(
this.organization.id,
user.id,
);
try {
await this.actionPromise;
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("organizationUserDeleted", this.userNamePipe.transform(user)),
});
this.dataSource.removeUser(user);
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
private async noMasterPasswordConfirmationDialog(user: OrganizationUserView) {
return this.dialogService.openSimpleDialog({
title: {
key: "removeOrgUserNoMasterPasswordTitle",
},
content: {
key: "removeOrgUserNoMasterPasswordDesc",
placeholders: [this.userNamePipe.transform(user)],
},
type: "warning",
});
}
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 navigateToPaymentMethod() {
const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag(
FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout,
);
const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method";
await this.router.navigate(["organizations", `${this.organization?.id}`, "billing", route], {
state: { launchPaymentModalAutomatically: true },
});
}
}