diff --git a/apps/web/src/app/admin-console/common/new-base.people.component.ts b/apps/web/src/app/admin-console/common/base-members.component.ts similarity index 97% rename from apps/web/src/app/admin-console/common/new-base.people.component.ts rename to apps/web/src/app/admin-console/common/base-members.component.ts index 90c25e840c..c13bec78c5 100644 --- a/apps/web/src/app/admin-console/common/new-base.people.component.ts +++ b/apps/web/src/app/admin-console/common/base-members.component.ts @@ -4,7 +4,6 @@ import { FormControl } from "@angular/forms"; import { firstValueFrom, lastValueFrom, debounceTime, combineLatest, BehaviorSubject } from "rxjs"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { @@ -35,7 +34,7 @@ export type UserViewTypes = ProviderUserUserDetailsResponse | OrganizationUserVi * This will replace BasePeopleComponent once all subclasses have been changed over to use this class. */ @Directive() -export abstract class NewBasePeopleComponent { +export abstract class BaseMembersComponent { /** * Shows a banner alerting the admin that users need to be confirmed. */ @@ -52,6 +51,10 @@ export abstract class NewBasePeopleComponent { return this.dataSource.acceptedUserCount > 0; } + get showBulkReinviteUsers(): boolean { + return this.dataSource.invitedUserCount > 0; + } + abstract userType: typeof OrganizationUserType | typeof ProviderUserType; abstract userStatusType: typeof OrganizationUserStatusType | typeof ProviderUserStatusType; @@ -77,7 +80,6 @@ export abstract class NewBasePeopleComponent { protected i18nService: I18nService, protected cryptoService: CryptoService, protected validationService: ValidationService, - protected modalService: ModalService, private logService: LogService, protected userNamePipe: UserNamePipe, protected dialogService: DialogService, diff --git a/apps/web/src/app/admin-console/common/people-table-data-source.ts b/apps/web/src/app/admin-console/common/people-table-data-source.ts index db357b4dbc..5ce7e7bda7 100644 --- a/apps/web/src/app/admin-console/common/people-table-data-source.ts +++ b/apps/web/src/app/admin-console/common/people-table-data-source.ts @@ -4,7 +4,7 @@ import { } from "@bitwarden/common/admin-console/enums"; import { TableDataSource } from "@bitwarden/components"; -import { StatusType, UserViewTypes } from "./new-base.people.component"; +import { StatusType, UserViewTypes } from "./base-members.component"; const MaxCheckedCount = 500; diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts new file mode 100644 index 0000000000..8d634c38e0 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts @@ -0,0 +1,99 @@ +import { Directive, OnInit } from "@angular/core"; + +import { + OrganizationUserBulkPublicKeyResponse, + OrganizationUserBulkResponse, +} from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; + +import { BulkUserDetails } from "./bulk-status.component"; + +@Directive() +export abstract class BaseBulkConfirmComponent implements OnInit { + protected users: BulkUserDetails[]; + + protected excludedUsers: BulkUserDetails[]; + protected filteredUsers: BulkUserDetails[]; + + protected publicKeys: Map = new Map(); + protected fingerprints: Map = new Map(); + protected statuses: Map = new Map(); + + protected done = false; + protected loading = true; + protected error: string; + + protected constructor( + protected cryptoService: CryptoService, + protected i18nService: I18nService, + ) {} + + async ngOnInit() { + this.excludedUsers = this.users.filter((user) => !this.isAccepted(user)); + this.filteredUsers = this.users.filter((user) => this.isAccepted(user)); + + if (this.filteredUsers.length <= 0) { + this.done = true; + } + + const publicKeysResponse = await this.getPublicKeys(); + + for (const entry of publicKeysResponse.data) { + const publicKey = Utils.fromB64ToArray(entry.key); + const fingerprint = await this.cryptoService.getFingerprint(entry.userId, publicKey); + if (fingerprint != null) { + this.publicKeys.set(entry.id, publicKey); + this.fingerprints.set(entry.id, fingerprint.join("-")); + } + } + + this.loading = false; + } + + submit = async () => { + this.loading = true; + try { + const key = await this.getCryptoKey(); + const userIdsWithKeys: { id: string; key: string }[] = []; + + for (const user of this.filteredUsers) { + const publicKey = this.publicKeys.get(user.id); + if (publicKey == null) { + continue; + } + const encryptedKey = await this.cryptoService.rsaEncrypt(key.key, publicKey); + userIdsWithKeys.push({ + id: user.id, + key: encryptedKey.encryptedString, + }); + } + + const userBulkResponse = await this.postConfirmRequest(userIdsWithKeys); + + userBulkResponse.data.forEach((entry) => { + const error = entry.error !== "" ? entry.error : this.i18nService.t("bulkConfirmMessage"); + this.statuses.set(entry.id, error); + }); + + this.done = true; + } catch (e) { + this.error = e.message; + } + this.loading = false; + }; + + protected abstract getCryptoKey(): Promise; + protected abstract getPublicKeys(): Promise< + ListResponse + >; + protected abstract isAccepted(user: BulkUserDetails): boolean; + protected abstract postConfirmRequest( + userIdsWithKeys: { id: string; key: string }[], + ): Promise>; +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component.ts new file mode 100644 index 0000000000..6c73634660 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component.ts @@ -0,0 +1,40 @@ +import { Directive } from "@angular/core"; + +import { OrganizationUserBulkResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +@Directive() +export abstract class BaseBulkRemoveComponent { + protected showNoMasterPasswordWarning: boolean; + protected statuses: Map = new Map(); + + protected done = false; + protected loading = false; + protected error: string; + + protected constructor(protected i18nService: I18nService) {} + + submit = async () => { + this.loading = true; + try { + const deleteUsersResponse = await this.deleteUsers(); + deleteUsersResponse.data.forEach((entry) => { + const error = entry.error !== "" ? entry.error : this.i18nService.t("bulkRemovedMessage"); + this.statuses.set(entry.id, error); + }); + this.done = true; + } catch (e) { + this.error = e.message; + } + + this.loading = false; + }; + + protected abstract deleteUsers(): Promise< + ListResponse + >; + + protected abstract get removeUsersWarning(): string; +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts index ffaf27ea46..dba6319b27 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts @@ -33,7 +33,7 @@ type BulkStatusDialogData = { users: Array; filteredUsers: Array; request: Promise>; - successfullMessage: string; + successfulMessage: string; }; @Component({ @@ -67,7 +67,7 @@ export class BulkStatusComponent implements OnInit { ); this.users = data.users.map((user) => { - let message = keyedErrors[user.id] ?? data.successfullMessage; + let message = keyedErrors[user.id] ?? data.successfulMessage; // eslint-disable-next-line if (!keyedFilteredUsers.hasOwnProperty(user.id)) { message = this.i18nService.t("bulkFilteredMessage"); diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index 99afe8099a..64e4b34547 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -103,7 +103,12 @@ - diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 93827539f8..809f1e3935 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -45,7 +45,7 @@ import { Collection } from "@bitwarden/common/vault/models/domain/collection"; import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response"; import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; -import { NewBasePeopleComponent } from "../../common/new-base.people.component"; +import { BaseMembersComponent } from "../../common/base-members.component"; import { PeopleTableDataSource } from "../../common/people-table-data-source"; import { GroupService } from "../core"; import { OrganizationUserView } from "../core/views/organization-user.view"; @@ -70,7 +70,7 @@ class MembersTableDataSource extends PeopleTableDataSource @Component({ templateUrl: "members.component.html", }) -export class MembersComponent extends NewBasePeopleComponent { +export class MembersComponent extends BaseMembersComponent { @ViewChild("resetPasswordTemplate", { read: ViewContainerRef, static: true }) resetPasswordModalRef: ViewContainerRef; @@ -94,7 +94,6 @@ export class MembersComponent extends NewBasePeopleComponent + + + {{ title | uppercase }} + {{ dialogParams.user.name }} + +
+ +

{{ "providerInviteUserDesc" | i18n }}

+
+ + + {{ "email" | i18n }} + + + {{ "inviteMultipleEmailDesc" | i18n: "20" }} + +
+
+ +

+ {{ "userType" | i18n | uppercase }} + + + +

+ + + + {{ "serviceUser" | i18n }} + + {{ "serviceUserDesc" | i18n }} + + + + {{ "providerAdmin" | i18n }} + + {{ "providerAdminDesc" | i18n }} + + +
+
+ + + +
+ +
+
+
+ diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts new file mode 100644 index 0000000000..5f88bf177c --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts @@ -0,0 +1,127 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { ProviderUserInviteRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-invite.request"; +import { ProviderUserUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-update.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; + +export type AddEditMemberDialogParams = { + providerId: string; + user?: { + id: string; + name: string; + type: ProviderUserType; + }; +}; + +export enum AddEditMemberDialogResultType { + Closed = "closed", + Deleted = "deleted", + Saved = "saved", +} + +@Component({ + templateUrl: "add-edit-member-dialog.component.html", +}) +export class AddEditMemberDialogComponent { + editing = false; + loading = true; + title: string; + + protected ResultType = AddEditMemberDialogResultType; + protected UserType = ProviderUserType; + + protected formGroup = new FormGroup({ + emails: new FormControl("", [Validators.required]), + type: new FormControl(this.dialogParams.user?.type ?? ProviderUserType.ServiceUser), + }); + + constructor( + private apiService: ApiService, + @Inject(DIALOG_DATA) protected dialogParams: AddEditMemberDialogParams, + private dialogRef: DialogRef, + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + ) { + this.editing = this.loading = this.dialogParams.user != null; + if (this.editing) { + this.title = this.i18nService.t("editMember"); + const emailControl = this.formGroup.controls.emails; + emailControl.removeValidators(Validators.required); + emailControl.disable(); + } else { + this.title = this.i18nService.t("inviteMember"); + } + + this.loading = false; + } + + delete = async (): Promise => { + if (!this.editing) { + return; + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: this.dialogParams.user.name, + content: { key: "removeUserConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + await this.apiService.deleteProviderUser( + this.dialogParams.providerId, + this.dialogParams.user.id, + ); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("removedUserId", this.dialogParams.user.name), + }); + + this.dialogRef.close(AddEditMemberDialogResultType.Deleted); + }; + + submit = async (): Promise => { + if (this.editing) { + const request = new ProviderUserUpdateRequest(); + request.type = this.formGroup.value.type; + await this.apiService.putProviderUser( + this.dialogParams.providerId, + this.dialogParams.user.id, + request, + ); + } else { + const request = new ProviderUserInviteRequest(); + request.emails = this.formGroup.value.emails.trim().split(/\s*,\s*/); + request.type = this.formGroup.value.type; + await this.apiService.postProviderUserInvite(this.dialogParams.providerId, request); + } + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( + this.editing ? "editedUserId" : "invitedUsers", + this.dialogParams.user?.name, + ), + }); + + this.dialogRef.close(AddEditMemberDialogResultType.Saved); + }; + + static open(dialogService: DialogService, dialogConfig: DialogConfig) { + return dialogService.open( + AddEditMemberDialogComponent, + dialogConfig, + ); + } +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts new file mode 100644 index 0000000000..d4a179091a --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts @@ -0,0 +1,69 @@ +import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { + OrganizationUserBulkPublicKeyResponse, + OrganizationUserBulkResponse, +} from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { ProviderUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk-confirm.request"; +import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; +import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { DialogService } from "@bitwarden/components"; +import { BaseBulkConfirmComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component"; +import { BulkUserDetails } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; + +type BulkConfirmDialogParams = { + providerId: string; + users: BulkUserDetails[]; +}; + +@Component({ + templateUrl: + "../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.html", +}) +export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { + providerId: string; + + constructor( + private apiService: ApiService, + protected cryptoService: CryptoService, + @Inject(DIALOG_DATA) protected dialogParams: BulkConfirmDialogParams, + protected i18nService: I18nService, + ) { + super(cryptoService, i18nService); + + this.providerId = dialogParams.providerId; + this.users = dialogParams.users; + } + + protected getCryptoKey = (): Promise => + this.cryptoService.getProviderKey(this.providerId); + + protected getPublicKeys = async (): Promise< + ListResponse + > => { + const request = new ProviderUserBulkRequest(this.filteredUsers.map((user) => user.id)); + return await this.apiService.postProviderUsersPublicKey(this.providerId, request); + }; + + protected isAccepted = (user: BulkUserDetails): boolean => + user.status === ProviderUserStatusType.Accepted; + + protected postConfirmRequest = async ( + userIdsWithKeys: { id: string; key: string }[], + ): Promise> => { + const request = new ProviderUserBulkConfirmRequest(userIdsWithKeys); + return await this.apiService.postProviderUserBulkConfirm(this.providerId, request); + }; + + static open(dialogService: DialogService, dialogConfig: DialogConfig) { + return dialogService.open(BulkConfirmDialogComponent, dialogConfig); + } +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts new file mode 100644 index 0000000000..16e6470370 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts @@ -0,0 +1,49 @@ +import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService } from "@bitwarden/components"; +import { BaseBulkRemoveComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component"; +import { BulkUserDetails } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; + +type BulkRemoveDialogParams = { + providerId: string; + users: BulkUserDetails[]; +}; + +@Component({ + templateUrl: + "../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.html", +}) +export class BulkRemoveDialogComponent extends BaseBulkRemoveComponent { + providerId: string; + users: BulkUserDetails[]; + + constructor( + private apiService: ApiService, + @Inject(DIALOG_DATA) dialogParams: BulkRemoveDialogParams, + protected i18nService: I18nService, + ) { + super(i18nService); + + this.providerId = dialogParams.providerId; + this.users = dialogParams.users; + } + + protected deleteUsers = (): Promise> => { + const request = new ProviderUserBulkRequest(this.users.map((user) => user.id)); + return this.apiService.deleteManyProviderUsers(this.providerId, request); + }; + + protected get removeUsersWarning() { + return this.i18nService.t("removeOrgUsersConfirmation"); + } + + static open(dialogService: DialogService, dialogConfig: DialogConfig) { + return dialogService.open(BulkRemoveDialogComponent, dialogConfig); + } +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html new file mode 100644 index 0000000000..66c4267844 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html @@ -0,0 +1,225 @@ + + + + + + +
+ + + {{ "all" | i18n }} + + {{ allCount }} + + + + {{ "invited" | i18n }} + + {{ invitedCount }} + + + + {{ "needsConfirmation" | i18n }} + + {{ acceptedCount }} + + + +
+ + + + {{ "loading" | i18n }} + + + +

{{ "noMembersInList" | i18n }}

+ + + {{ "providerUsersNeedConfirmed" | i18n }} + + + + + + + + + + {{ "name" | i18n }} + {{ "role" | i18n }} + + + + + + + + + + + + + + + + +
+ +
+
+ + + {{ "invited" | i18n }} + + + {{ "needsConfirmation" | i18n }} + + + {{ "revoked" | i18n }} + +
+
+ {{ user.email }} +
+
+
+ + + {{ "providerAdmin" | i18n }} + {{ "serviceUser" | i18n }} + + + + + + + + + + + +
+
+
+
+
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts new file mode 100644 index 0000000000..247297ff96 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts @@ -0,0 +1,243 @@ +import { DialogRef } from "@angular/cdk/dialog"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute, Router } from "@angular/router"; +import { combineLatest, lastValueFrom, switchMap } from "rxjs"; +import { first } 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 { + OrganizationUserStatusType, + ProviderUserStatusType, + ProviderUserType, +} from "@bitwarden/common/admin-console/enums"; +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"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.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 { DialogService, ToastService } from "@bitwarden/components"; +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 { BulkStatusComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; + +import { + AddEditMemberDialogComponent, + AddEditMemberDialogParams, + AddEditMemberDialogResultType, +} from "./dialogs/add-edit-member-dialog.component"; +import { BulkConfirmDialogComponent } from "./dialogs/bulk-confirm-dialog.component"; +import { BulkRemoveDialogComponent } from "./dialogs/bulk-remove-dialog.component"; + +type ProviderUser = ProviderUserUserDetailsResponse; + +class MembersTableDataSource extends PeopleTableDataSource { + protected statusType = OrganizationUserStatusType; +} + +@Component({ + templateUrl: "members.component.html", +}) +export class MembersComponent extends BaseMembersComponent { + accessEvents = false; + dataSource = new MembersTableDataSource(); + loading = true; + providerId: string; + rowHeight = 62; + rowHeightClass = `tw-h-[62px]`; + status: ProviderUserStatusType = null; + + userStatusType = ProviderUserStatusType; + userType = ProviderUserType; + + constructor( + apiService: ApiService, + cryptoService: CryptoService, + dialogService: DialogService, + i18nService: I18nService, + logService: LogService, + organizationManagementPreferencesService: OrganizationManagementPreferencesService, + toastService: ToastService, + userNamePipe: UserNamePipe, + validationService: ValidationService, + private activatedRoute: ActivatedRoute, + private providerService: ProviderService, + private router: Router, + ) { + super( + apiService, + i18nService, + cryptoService, + validationService, + logService, + userNamePipe, + dialogService, + organizationManagementPreferencesService, + toastService, + ); + + combineLatest([ + this.activatedRoute.parent.params, + this.activatedRoute.queryParams.pipe(first()), + ]) + .pipe( + switchMap(async ([urlParams, queryParams]) => { + this.searchControl.setValue(queryParams.search, { emitEvent: false }); + this.dataSource.filter = peopleFilter(queryParams.search, null); + + this.providerId = urlParams.providerId; + const provider = await this.providerService.get(this.providerId); + 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); + } + } + }), + takeUntilDestroyed(), + ) + .subscribe(); + } + + async bulkConfirm(): Promise { + if (this.actionPromise != null) { + return; + } + + const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, { + data: { + providerId: this.providerId, + users: this.dataSource.getCheckedUsers(), + }, + }); + + await lastValueFrom(dialogRef.closed); + await this.load(); + } + + async bulkReinvite(): Promise { + if (this.actionPromise != null) { + return; + } + + const checkedUsers = this.dataSource.getCheckedUsers(); + const checkedInvitedUsers = checkedUsers.filter( + (user) => user.status === ProviderUserStatusType.Invited, + ); + + if (checkedInvitedUsers.length <= 0) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("noSelectedUsersApplicable"), + }); + return; + } + + try { + const request = this.apiService.postManyProviderUserReinvite( + this.providerId, + new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)), + ); + + const dialogRef = BulkStatusComponent.open(this.dialogService, { + data: { + users: checkedUsers, + filteredUsers: checkedInvitedUsers, + request, + successfulMessage: this.i18nService.t("bulkReinviteMessage"), + }, + }); + await lastValueFrom(dialogRef.closed); + } catch (error) { + this.validationService.showError(error); + } + } + + async bulkRemove(): Promise { + if (this.actionPromise != null) { + return; + } + + const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, { + data: { + providerId: this.providerId, + users: this.dataSource.getCheckedUsers(), + }, + }); + + await lastValueFrom(dialogRef.closed); + await this.load(); + } + + async confirmUser(user: ProviderUser, publicKey: Uint8Array): Promise { + const providerKey = await this.cryptoService.getProviderKey(this.providerId); + const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey); + const request = new ProviderUserConfirmRequest(); + request.key = key.encryptedString; + await this.apiService.postProviderUserConfirm(this.providerId, user.id, request); + } + + deleteUser = (id: string): Promise => + this.apiService.deleteProviderUser(this.providerId, id); + + edit = async (user: ProviderUser | null): Promise => { + const data: AddEditMemberDialogParams = { + providerId: this.providerId, + }; + + if (user != null) { + data.user = { + id: user.id, + name: this.userNamePipe.transform(user), + type: user.type, + }; + } + + const dialogRef = AddEditMemberDialogComponent.open(this.dialogService, { + data, + }); + + const result = await lastValueFrom(dialogRef.closed); + + switch (result) { + case AddEditMemberDialogResultType.Saved: + case AddEditMemberDialogResultType.Deleted: + await this.load(); + break; + } + }; + + openEventsDialog = (user: ProviderUser): DialogRef => + openEntityEventsDialog(this.dialogService, { + data: { + name: this.userNamePipe.transform(user), + providerId: this.providerId, + entityId: user.id, + showUser: false, + entity: "user", + }, + }); + + getUsers = (): Promise> => + this.apiService.getProviderUsers(this.providerId); + + reinviteUser = (id: string): Promise => + this.apiService.postProviderUserReinvite(this.providerId, id); +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts index 564808d005..1849809df5 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts @@ -15,6 +15,7 @@ import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/ 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"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -29,6 +30,9 @@ import { BulkConfirmComponent } from "./bulk/bulk-confirm.component"; import { BulkRemoveComponent } from "./bulk/bulk-remove.component"; import { UserAddEditComponent } from "./user-add-edit.component"; +/** + * @deprecated Please use the {@link MembersComponent} instead. + */ @Component({ selector: "provider-people", templateUrl: "people.component.html", @@ -70,6 +74,7 @@ export class PeopleComponent private providerService: ProviderService, dialogService: DialogService, organizationManagementPreferencesService: OrganizationManagementPreferencesService, + private configService: ConfigService, ) { super( apiService, @@ -228,7 +233,7 @@ export class PeopleComponent users: users, filteredUsers: filteredUsers, request: response, - successfullMessage: this.i18nService.t("bulkReinviteMessage"), + successfulMessage: this.i18nService.t("bulkReinviteMessage"), }, }); await lastValueFrom(dialogRef.closed); diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.ts index 664e399660..7406098ee4 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/user-add-edit.component.ts @@ -10,6 +10,9 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; +/** + * @deprecated Please use the {@link MembersDialogComponent} instead. + */ @Component({ selector: "provider-user-add-edit", templateUrl: "user-add-edit.component.html", diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index 9a94df877d..5f9b3f66bc 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -13,11 +13,7 @@ route="manage" *ngIf="showManageTab(provider)" > - + provider.canManageUsers), - ], - data: { - titleId: "people", + ...featureFlaggedRoute({ + defaultComponent: PeopleComponent, + flaggedComponent: MembersComponent, + featureFlag: FeatureFlag.AC2828_ProviderPortalMembersPage, + routeOptions: { + path: "people", + canActivate: [ + providerPermissionsGuard((provider: Provider) => provider.canManageUsers), + ], + data: { + titleId: "people", + }, }, - }, + }), { path: "events", component: EventsComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index d17c973181..9733e91be7 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -1,3 +1,4 @@ +import { ScrollingModule } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { FormsModule } from "@angular/forms"; @@ -28,7 +29,11 @@ import { CreateOrganizationComponent } from "./clients/create-organization.compo import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { BulkConfirmComponent } from "./manage/bulk/bulk-confirm.component"; import { BulkRemoveComponent } from "./manage/bulk/bulk-remove.component"; +import { AddEditMemberDialogComponent } from "./manage/dialogs/add-edit-member-dialog.component"; +import { BulkConfirmDialogComponent } from "./manage/dialogs/bulk-confirm-dialog.component"; +import { BulkRemoveDialogComponent } from "./manage/dialogs/bulk-remove-dialog.component"; import { EventsComponent } from "./manage/events.component"; +import { MembersComponent } from "./manage/members.component"; import { PeopleComponent } from "./manage/people.component"; import { UserAddEditComponent } from "./manage/user-add-edit.component"; import { ProvidersLayoutComponent } from "./providers-layout.component"; @@ -51,20 +56,25 @@ import { SetupComponent } from "./setup/setup.component"; PaymentMethodWarningsModule, TaxInfoComponent, DangerZoneComponent, + ScrollingModule, ], declarations: [ AcceptProviderComponent, AccountComponent, AddOrganizationComponent, BulkConfirmComponent, + BulkConfirmDialogComponent, BulkRemoveComponent, + BulkRemoveDialogComponent, ClientsComponent, CreateOrganizationComponent, EventsComponent, PeopleComponent, + MembersComponent, SetupComponent, SetupProviderComponent, UserAddEditComponent, + AddEditMemberDialogComponent, CreateClientDialogComponent, NoClientsComponent, ManageClientsComponent, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index e9b154d2f4..ba23b90cd2 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -23,6 +23,7 @@ export enum FeatureFlag { GroupsComponentRefactor = "groups-component-refactor", ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", VaultBulkManagementAction = "vault-bulk-management-action", + AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -56,6 +57,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.GroupsComponentRefactor]: FALSE, [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, [FeatureFlag.VaultBulkManagementAction]: FALSE, + [FeatureFlag.AC2828_ProviderPortalMembersPage]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;