diff --git a/apps/web/src/app/common/base.people.component.ts b/apps/web/src/app/common/base.people.component.ts index c3ee6bb209d..fd14e5be162 100644 --- a/apps/web/src/app/common/base.people.component.ts +++ b/apps/web/src/app/common/base.people.component.ts @@ -20,6 +20,7 @@ import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response"; import { ProviderUserUserDetailsResponse } from "@bitwarden/common/models/response/provider/provider-user.response"; +import { OrganizationUserView } from "../organizations/core/views/organization-user.view"; import { UserConfirmComponent } from "../organizations/manage/user-confirm.component"; type StatusType = OrganizationUserStatusType | ProviderUserStatusType; @@ -28,7 +29,7 @@ const MaxCheckedCount = 500; @Directive() export abstract class BasePeopleComponent< - UserType extends ProviderUserUserDetailsResponse | OrganizationUserUserDetailsResponse + UserType extends ProviderUserUserDetailsResponse | OrganizationUserView > { @ViewChild("confirmTemplate", { read: ViewContainerRef, static: true }) confirmModalRef: ViewContainerRef; @@ -110,7 +111,7 @@ export abstract class BasePeopleComponent< ) {} abstract edit(user: UserType): void; - abstract getUsers(): Promise>; + abstract getUsers(): Promise | UserType[]>; abstract deleteUser(id: string): Promise; abstract revokeUser(id: string): Promise; abstract restoreUser(id: string): Promise; @@ -125,9 +126,14 @@ export abstract class BasePeopleComponent< this.statusMap.set(status, []); } - this.allUsers = response.data != null && response.data.length > 0 ? response.data : []; + if (response instanceof ListResponse) { + this.allUsers = response.data != null && response.data.length > 0 ? response.data : []; + } else if (Array.isArray(response)) { + this.allUsers = response; + } + this.allUsers.sort( - Utils.getSortFunction( + Utils.getSortFunction( this.i18nService, "email" ) diff --git a/apps/web/src/app/organizations/core/views/organization-user.view.ts b/apps/web/src/app/organizations/core/views/organization-user.view.ts new file mode 100644 index 00000000000..477cad2be60 --- /dev/null +++ b/apps/web/src/app/organizations/core/views/organization-user.view.ts @@ -0,0 +1,40 @@ +import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType"; +import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType"; +import { PermissionsApi } from "@bitwarden/common/models/api/permissions.api"; +import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response"; + +import { CollectionAccessSelectionView } from "./collection-access-selection.view"; + +export class OrganizationUserView { + id: string; + userId: string; + type: OrganizationUserType; + status: OrganizationUserStatusType; + accessAll: boolean; + permissions: PermissionsApi; + resetPasswordEnrolled: boolean; + name: string; + email: string; + twoFactorEnabled: boolean; + usesKeyConnector: boolean; + + collections: CollectionAccessSelectionView[] = []; + groups: string[] = []; + + groupNames: string[] = []; + collectionNames: string[] = []; + + static fromResponse(response: OrganizationUserUserDetailsResponse): OrganizationUserView { + const view = Object.assign(new OrganizationUserView(), response) as OrganizationUserView; + + if (response.collections != undefined) { + view.collections = response.collections.map((c) => new CollectionAccessSelectionView(c)); + } + + if (response.groups != undefined) { + view.groups = response.groups; + } + + return view; + } +} diff --git a/apps/web/src/app/organizations/manage/organization-manage.module.ts b/apps/web/src/app/organizations/manage/organization-manage.module.ts index 7d2615904fb..58c0ad161a4 100644 --- a/apps/web/src/app/organizations/manage/organization-manage.module.ts +++ b/apps/web/src/app/organizations/manage/organization-manage.module.ts @@ -4,10 +4,9 @@ import { NgModule } from "@angular/core"; import { SharedModule } from "../../shared"; import { EntityUsersComponent } from "./entity-users.component"; -import { UserDialogModule } from "./member-dialog"; @NgModule({ - imports: [SharedModule, ScrollingModule, UserDialogModule], + imports: [SharedModule, ScrollingModule], declarations: [EntityUsersComponent], exports: [EntityUsersComponent], }) diff --git a/apps/web/src/app/organizations/manage/people.component.html b/apps/web/src/app/organizations/manage/people.component.html deleted file mode 100644 index be5888cc667..00000000000 --- a/apps/web/src/app/organizations/manage/people.component.html +++ /dev/null @@ -1,290 +0,0 @@ -
-
-

{{ "members" | i18n }}

-
-
- - - - -
-
- - -
- - -
-
- - - {{ "loading" | i18n }} - - -

{{ "noUsersInList" | i18n }}

- - - {{ "usersNeedConfirmed" | i18n }} - - - - - - - - - - - - -
- - - - - {{ u.email }} - {{ - "invited" | i18n - }} - {{ - "accepted" | i18n - }} - {{ - "revoked" | i18n - }} - {{ u.name }} - - - - {{ "userUsingTwoStep" | i18n }} - - - - {{ "enrolledPasswordReset" | i18n }} - - - {{ "owner" | i18n }} - {{ "admin" | i18n }} - {{ "manager" | i18n }} - {{ "user" | i18n }} - {{ "custom" | i18n }} - - -
-
-
-
- - - - - - - - diff --git a/apps/web/src/app/organizations/manage/bulk/bulk-confirm.component.html b/apps/web/src/app/organizations/members/components/bulk/bulk-confirm.component.html similarity index 100% rename from apps/web/src/app/organizations/manage/bulk/bulk-confirm.component.html rename to apps/web/src/app/organizations/members/components/bulk/bulk-confirm.component.html diff --git a/apps/web/src/app/organizations/manage/bulk/bulk-confirm.component.ts b/apps/web/src/app/organizations/members/components/bulk/bulk-confirm.component.ts similarity index 100% rename from apps/web/src/app/organizations/manage/bulk/bulk-confirm.component.ts rename to apps/web/src/app/organizations/members/components/bulk/bulk-confirm.component.ts diff --git a/apps/web/src/app/organizations/manage/bulk/bulk-remove.component.html b/apps/web/src/app/organizations/members/components/bulk/bulk-remove.component.html similarity index 100% rename from apps/web/src/app/organizations/manage/bulk/bulk-remove.component.html rename to apps/web/src/app/organizations/members/components/bulk/bulk-remove.component.html diff --git a/apps/web/src/app/organizations/manage/bulk/bulk-remove.component.ts b/apps/web/src/app/organizations/members/components/bulk/bulk-remove.component.ts similarity index 100% rename from apps/web/src/app/organizations/manage/bulk/bulk-remove.component.ts rename to apps/web/src/app/organizations/members/components/bulk/bulk-remove.component.ts diff --git a/apps/web/src/app/organizations/manage/bulk/bulk-restore-revoke.component.html b/apps/web/src/app/organizations/members/components/bulk/bulk-restore-revoke.component.html similarity index 100% rename from apps/web/src/app/organizations/manage/bulk/bulk-restore-revoke.component.html rename to apps/web/src/app/organizations/members/components/bulk/bulk-restore-revoke.component.html diff --git a/apps/web/src/app/organizations/manage/bulk/bulk-restore-revoke.component.ts b/apps/web/src/app/organizations/members/components/bulk/bulk-restore-revoke.component.ts similarity index 100% rename from apps/web/src/app/organizations/manage/bulk/bulk-restore-revoke.component.ts rename to apps/web/src/app/organizations/members/components/bulk/bulk-restore-revoke.component.ts diff --git a/apps/web/src/app/organizations/manage/bulk/bulk-status.component.html b/apps/web/src/app/organizations/members/components/bulk/bulk-status.component.html similarity index 100% rename from apps/web/src/app/organizations/manage/bulk/bulk-status.component.html rename to apps/web/src/app/organizations/members/components/bulk/bulk-status.component.html diff --git a/apps/web/src/app/organizations/manage/bulk/bulk-status.component.ts b/apps/web/src/app/organizations/members/components/bulk/bulk-status.component.ts similarity index 100% rename from apps/web/src/app/organizations/manage/bulk/bulk-status.component.ts rename to apps/web/src/app/organizations/members/components/bulk/bulk-status.component.ts diff --git a/apps/web/src/app/organizations/manage/member-dialog/index.ts b/apps/web/src/app/organizations/members/components/member-dialog/index.ts similarity index 100% rename from apps/web/src/app/organizations/manage/member-dialog/index.ts rename to apps/web/src/app/organizations/members/components/member-dialog/index.ts diff --git a/apps/web/src/app/organizations/manage/member-dialog/member-dialog.component.html b/apps/web/src/app/organizations/members/components/member-dialog/member-dialog.component.html similarity index 100% rename from apps/web/src/app/organizations/manage/member-dialog/member-dialog.component.html rename to apps/web/src/app/organizations/members/components/member-dialog/member-dialog.component.html diff --git a/apps/web/src/app/organizations/manage/member-dialog/member-dialog.component.ts b/apps/web/src/app/organizations/members/components/member-dialog/member-dialog.component.ts similarity index 100% rename from apps/web/src/app/organizations/manage/member-dialog/member-dialog.component.ts rename to apps/web/src/app/organizations/members/components/member-dialog/member-dialog.component.ts diff --git a/apps/web/src/app/organizations/manage/member-dialog/member-dialog.module.ts b/apps/web/src/app/organizations/members/components/member-dialog/member-dialog.module.ts similarity index 84% rename from apps/web/src/app/organizations/manage/member-dialog/member-dialog.module.ts rename to apps/web/src/app/organizations/members/components/member-dialog/member-dialog.module.ts index 71c0046557d..ffaaf268ea0 100644 --- a/apps/web/src/app/organizations/manage/member-dialog/member-dialog.module.ts +++ b/apps/web/src/app/organizations/members/components/member-dialog/member-dialog.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; -import { SharedModule } from "../../../shared/shared.module"; +import { SharedModule } from "../../../../shared/shared.module"; import { MemberDialogComponent } from "./member-dialog.component"; import { NestedCheckboxComponent } from "./nested-checkbox.component"; diff --git a/apps/web/src/app/organizations/manage/member-dialog/nested-checkbox.component.html b/apps/web/src/app/organizations/members/components/member-dialog/nested-checkbox.component.html similarity index 100% rename from apps/web/src/app/organizations/manage/member-dialog/nested-checkbox.component.html rename to apps/web/src/app/organizations/members/components/member-dialog/nested-checkbox.component.html diff --git a/apps/web/src/app/organizations/manage/member-dialog/nested-checkbox.component.ts b/apps/web/src/app/organizations/members/components/member-dialog/nested-checkbox.component.ts similarity index 100% rename from apps/web/src/app/organizations/manage/member-dialog/nested-checkbox.component.ts rename to apps/web/src/app/organizations/members/components/member-dialog/nested-checkbox.component.ts diff --git a/apps/web/src/app/organizations/manage/reset-password.component.html b/apps/web/src/app/organizations/members/components/reset-password.component.html similarity index 100% rename from apps/web/src/app/organizations/manage/reset-password.component.html rename to apps/web/src/app/organizations/members/components/reset-password.component.html diff --git a/apps/web/src/app/organizations/manage/reset-password.component.ts b/apps/web/src/app/organizations/members/components/reset-password.component.ts similarity index 100% rename from apps/web/src/app/organizations/manage/reset-password.component.ts rename to apps/web/src/app/organizations/members/components/reset-password.component.ts diff --git a/apps/web/src/app/organizations/manage/user-groups.component.html b/apps/web/src/app/organizations/members/components/user-groups.component.html similarity index 100% rename from apps/web/src/app/organizations/manage/user-groups.component.html rename to apps/web/src/app/organizations/members/components/user-groups.component.html diff --git a/apps/web/src/app/organizations/manage/user-groups.component.ts b/apps/web/src/app/organizations/members/components/user-groups.component.ts similarity index 98% rename from apps/web/src/app/organizations/manage/user-groups.component.ts rename to apps/web/src/app/organizations/members/components/user-groups.component.ts index 1cb8bf7fef6..810a55d5547 100644 --- a/apps/web/src/app/organizations/manage/user-groups.component.ts +++ b/apps/web/src/app/organizations/members/components/user-groups.component.ts @@ -7,7 +7,7 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti import { Utils } from "@bitwarden/common/misc/utils"; import { OrganizationUserUpdateGroupsRequest } from "@bitwarden/common/models/request/organization-user-update-groups.request"; -import { GroupService, GroupView } from "../core"; +import { GroupService, GroupView } from "../../core"; @Component({ selector: "app-user-groups", diff --git a/apps/web/src/app/organizations/members/index.ts b/apps/web/src/app/organizations/members/index.ts new file mode 100644 index 00000000000..95bd8baf7c7 --- /dev/null +++ b/apps/web/src/app/organizations/members/index.ts @@ -0,0 +1 @@ +export * from "./members.module"; diff --git a/apps/web/src/app/organizations/members/members-routing.module.ts b/apps/web/src/app/organizations/members/members-routing.module.ts new file mode 100644 index 00000000000..5f669a0b020 --- /dev/null +++ b/apps/web/src/app/organizations/members/members-routing.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; + +import { canAccessMembersTab } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; + +import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard"; + +import { PeopleComponent } from "./people.component"; + +const routes: Routes = [ + { + path: "", + component: PeopleComponent, + canActivate: [OrganizationPermissionsGuard], + data: { + titleId: "members", + organizationPermissions: canAccessMembersTab, + }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class MembersRoutingModule {} diff --git a/apps/web/src/app/organizations/members/members.module.ts b/apps/web/src/app/organizations/members/members.module.ts new file mode 100644 index 00000000000..bd79043306e --- /dev/null +++ b/apps/web/src/app/organizations/members/members.module.ts @@ -0,0 +1,39 @@ +import { ComponentFactoryResolver, NgModule } from "@angular/core"; + +import { ModalService } from "@bitwarden/angular/services/modal.service"; + +import { LooseComponentsModule } from "../../shared"; +import { SharedOrganizationModule } from "../shared"; + +import { BulkConfirmComponent } from "./components/bulk/bulk-confirm.component"; +import { BulkRemoveComponent } from "./components/bulk/bulk-remove.component"; +import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component"; +import { BulkStatusComponent } from "./components/bulk/bulk-status.component"; +import { UserDialogModule } from "./components/member-dialog"; +import { ResetPasswordComponent } from "./components/reset-password.component"; +import { UserGroupsComponent } from "./components/user-groups.component"; +import { MembersRoutingModule } from "./members-routing.module"; +import { PeopleComponent } from "./people.component"; + +@NgModule({ + imports: [ + SharedOrganizationModule, + LooseComponentsModule, + MembersRoutingModule, + UserDialogModule, + ], + declarations: [ + BulkConfirmComponent, + BulkRemoveComponent, + BulkRestoreRevokeComponent, + BulkStatusComponent, + PeopleComponent, + ResetPasswordComponent, + UserGroupsComponent, + ], +}) +export class MembersModule { + constructor(modalService: ModalService, componentFactoryResolver: ComponentFactoryResolver) { + modalService.registerComponentFactoryResolver(UserGroupsComponent, componentFactoryResolver); + } +} diff --git a/apps/web/src/app/organizations/members/people.component.html b/apps/web/src/app/organizations/members/people.component.html new file mode 100644 index 00000000000..8518c61d254 --- /dev/null +++ b/apps/web/src/app/organizations/members/people.component.html @@ -0,0 +1,305 @@ +
+
+

{{ "members" | i18n }}

+
+ + + {{ "all" | i18n }} {{ allCount }} + + + + {{ "invited" | i18n }} + {{ invitedCount }} + + + + {{ "needsConfirmation" | i18n }} + {{ acceptedCount }} + + + + {{ "revoked" | i18n }} + {{ revokedCount }} + + + + + + + +
+
+ + + {{ "loading" | i18n }} + + +

{{ "noMembersInList" | i18n }}

+ + + {{ "usersNeedConfirmed" | i18n }} + + + + + + + + + {{ "name" | i18n }} + {{ (accessGroups ? "groups" : "collections") | i18n }} + {{ "role" | i18n }} + {{ "policies" | i18n }} + + + + + + + + + + + + + + + + + + + +
+ +
+
+ {{ u.name ?? u.email }} + {{ "invited" | i18n }} + {{ "needsConfirmation" | i18n }} + {{ "revoked" | i18n }} +
+
+ {{ u.email }} +
+
+
+ + + + + {{ "all" | i18n }} + + + + {{ u.type | userType }} + + + + + + {{ "userUsingTwoStep" | i18n }} + + + + {{ "enrolledPasswordReset" | i18n }} + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + + + + + +
diff --git a/apps/web/src/app/organizations/manage/people.component.ts b/apps/web/src/app/organizations/members/people.component.ts similarity index 80% rename from apps/web/src/app/organizations/manage/people.component.ts rename to apps/web/src/app/organizations/members/people.component.ts index 358b06aa611..73355fdc3dc 100644 --- a/apps/web/src/app/organizations/manage/people.component.ts +++ b/apps/web/src/app/organizations/members/people.component.ts @@ -6,6 +6,7 @@ import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; 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 { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; @@ -20,31 +21,36 @@ import { ValidationService } from "@bitwarden/common/abstractions/validation.ser import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType"; import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType"; import { PolicyType } from "@bitwarden/common/enums/policyType"; +import { CollectionData } from "@bitwarden/common/models/data/collection.data"; +import { Collection } from "@bitwarden/common/models/domain/collection"; import { OrganizationKeysRequest } from "@bitwarden/common/models/request/organization-keys.request"; import { OrganizationUserBulkRequest } from "@bitwarden/common/models/request/organization-user-bulk.request"; import { OrganizationUserConfirmRequest } from "@bitwarden/common/models/request/organization-user-confirm.request"; +import { CollectionDetailsResponse } from "@bitwarden/common/models/response/collection.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { OrganizationUserBulkResponse } from "@bitwarden/common/models/response/organization-user-bulk.response"; import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response"; import { DialogService } from "@bitwarden/components"; import { BasePeopleComponent } from "../../common/base.people.component"; +import { GroupService } from "../core"; +import { OrganizationUserView } from "../core/views/organization-user.view"; +import { EntityEventsComponent } from "../manage/entity-events.component"; -import { BulkConfirmComponent } from "./bulk/bulk-confirm.component"; -import { BulkRemoveComponent } from "./bulk/bulk-remove.component"; -import { BulkRestoreRevokeComponent } from "./bulk/bulk-restore-revoke.component"; -import { BulkStatusComponent } from "./bulk/bulk-status.component"; -import { EntityEventsComponent } from "./entity-events.component"; -import { openUserAddEditDialog, MemberDialogResult } from "./member-dialog/member-dialog.component"; -import { ResetPasswordComponent } from "./reset-password.component"; -import { UserGroupsComponent } from "./user-groups.component"; +import { BulkConfirmComponent } from "./components/bulk/bulk-confirm.component"; +import { BulkRemoveComponent } from "./components/bulk/bulk-remove.component"; +import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component"; +import { BulkStatusComponent } from "./components/bulk/bulk-status.component"; +import { MemberDialogResult, openUserAddEditDialog } from "./components/member-dialog"; +import { ResetPasswordComponent } from "./components/reset-password.component"; +import { UserGroupsComponent } from "./components/user-groups.component"; @Component({ selector: "app-org-people", templateUrl: "people.component.html", }) export class PeopleComponent - extends BasePeopleComponent + extends BasePeopleComponent implements OnInit, OnDestroy { @ViewChild("groupsTemplate", { read: ViewContainerRef, static: true }) @@ -94,7 +100,9 @@ export class PeopleComponent stateService: StateService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, - private dialogService: DialogService + private dialogService: DialogService, + private groupService: GroupService, + private collectionService: CollectionService ) { super( apiService, @@ -167,12 +175,68 @@ export class PeopleComponent } async load() { - super.load(); await super.load(); } - getUsers(): Promise> { - return this.apiService.getOrganizationUsers(this.organizationId); + async getUsers(): Promise { + let groupsPromise: Promise>; + let collectionsPromise: Promise>; + + // We don't need both groups and collections for the table, so only load one + const userPromise = this.apiService.getOrganizationUsers(this.organizationId, { + includeGroups: this.accessGroups, + includeCollections: !this.accessGroups, + }); + + // Depending on which column is displayed, we need to load the group/collection names + if (this.accessGroups) { + groupsPromise = this.getGroupNameMap(); + } else { + collectionsPromise = this.getCollectionNameMap(); + } + + const [usersResponse, groupNamesMap, collectionNamesMap] = await Promise.all([ + userPromise, + groupsPromise, + collectionsPromise, + ]); + + return usersResponse.data?.map((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> { + const groups = await this.groupService.getAll(this.organizationId); + const groupNameMap = new Map(); + 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 collectionMap = new Map(); + const response = await this.apiService.getCollections(this.organizationId); + + const collections = response.data.map( + (r) => new Collection(new CollectionData(r as CollectionDetailsResponse)) + ); + const decryptedCollections = await this.collectionService.decryptMany(collections); + + decryptedCollections.forEach((c) => collectionMap.set(c.id, c.name)); + + return collectionMap; } deleteUser(id: string): Promise { @@ -191,10 +255,7 @@ export class PeopleComponent return this.apiService.postOrganizationUserReinvite(this.organizationId, id); } - async confirmUser( - user: OrganizationUserUserDetailsResponse, - publicKey: Uint8Array - ): Promise { + async confirmUser(user: OrganizationUserView, publicKey: Uint8Array): Promise { const orgKey = await this.cryptoService.getOrgKey(this.organizationId); const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer); const request = new OrganizationUserConfirmRequest(); @@ -202,7 +263,7 @@ export class PeopleComponent await this.apiService.postOrganizationUserConfirm(this.organizationId, user.id, request); } - allowResetPassword(orgUser: OrganizationUserUserDetailsResponse): boolean { + allowResetPassword(orgUser: OrganizationUserView): boolean { // Hierarchy check let callingUserHasPermission = false; @@ -240,7 +301,7 @@ export class PeopleComponent ); } - async edit(user: OrganizationUserUserDetailsResponse) { + async edit(user: OrganizationUserView) { const dialog = openUserAddEditDialog(this.dialogService, { data: { name: this.userNamePipe.transform(user), @@ -274,6 +335,7 @@ export class PeopleComponent // eslint-disable-next-line rxjs-angular/prefer-takeuntil comp.onSavedUser.subscribe(() => { modal.close(); + this.load(); }); } ); @@ -376,7 +438,7 @@ export class PeopleComponent await this.load(); } - async events(user: OrganizationUserUserDetailsResponse) { + async events(user: OrganizationUserView) { await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => { comp.name = this.userNamePipe.transform(user); comp.organizationId = this.organizationId; @@ -386,7 +448,7 @@ export class PeopleComponent }); } - async resetPassword(user: OrganizationUserUserDetailsResponse) { + async resetPassword(user: OrganizationUserView) { const [modal] = await this.modalService.openViewRef( ResetPasswordComponent, this.resetPasswordModalRef, @@ -405,7 +467,7 @@ export class PeopleComponent ); } - protected async removeUserConfirmationDialog(user: OrganizationUserUserDetailsResponse) { + protected async removeUserConfirmationDialog(user: OrganizationUserView) { const warningMessage = user.usesKeyConnector ? this.i18nService.t("removeUserConfirmationKeyConnector") : this.i18nService.t("removeOrgUserConfirmation"); @@ -420,8 +482,8 @@ export class PeopleComponent } private async showBulkStatus( - users: OrganizationUserUserDetailsResponse[], - filteredUsers: OrganizationUserUserDetailsResponse[], + users: OrganizationUserView[], + filteredUsers: OrganizationUserView[], request: Promise>, successfullMessage: string ) { diff --git a/apps/web/src/app/organizations/organization-routing.module.ts b/apps/web/src/app/organizations/organization-routing.module.ts index 1bfe0160233..f234d42aec2 100644 --- a/apps/web/src/app/organizations/organization-routing.module.ts +++ b/apps/web/src/app/organizations/organization-routing.module.ts @@ -3,9 +3,8 @@ import { RouterModule, Routes } from "@angular/router"; import { AuthGuard } from "@bitwarden/angular/guards/auth.guard"; import { - canAccessOrgAdmin, canAccessGroupsTab, - canAccessMembersTab, + canAccessOrgAdmin, } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { OrganizationPermissionsGuard } from "./guards/org-permissions.guard"; @@ -13,7 +12,6 @@ import { OrganizationLayoutComponent } from "./layouts/organization-layout.compo import { CollectionsComponent } from "./manage/collections.component"; import { GroupsComponent } from "./manage/groups.component"; import { ManageComponent } from "./manage/manage.component"; -import { PeopleComponent } from "./manage/people.component"; import { VaultModule } from "./vault/vault.module"; const routes: Routes = [ @@ -36,12 +34,7 @@ const routes: Routes = [ }, { path: "members", - component: PeopleComponent, - canActivate: [OrganizationPermissionsGuard], - data: { - titleId: "members", - organizationPermissions: canAccessMembersTab, - }, + loadChildren: () => import("./members").then((m) => m.MembersModule), }, { path: "groups", diff --git a/apps/web/src/app/organizations/organization.module.ts b/apps/web/src/app/organizations/organization.module.ts index 93c83be3ded..3188e97d798 100644 --- a/apps/web/src/app/organizations/organization.module.ts +++ b/apps/web/src/app/organizations/organization.module.ts @@ -3,12 +3,11 @@ import { NgModule } from "@angular/core"; import { CoreOrganizationModule } from "./core"; import { GroupAddEditComponent } from "./manage/group-add-edit.component"; import { GroupsComponent } from "./manage/groups.component"; -import { UserGroupsComponent } from "./manage/user-groups.component"; import { OrganizationsRoutingModule } from "./organization-routing.module"; import { SharedOrganizationModule } from "./shared"; @NgModule({ imports: [SharedOrganizationModule, CoreOrganizationModule, OrganizationsRoutingModule], - declarations: [GroupsComponent, GroupAddEditComponent, UserGroupsComponent], + declarations: [GroupsComponent, GroupAddEditComponent], }) export class OrganizationModule {} diff --git a/apps/web/src/app/organizations/shared/shared-organization.module.ts b/apps/web/src/app/organizations/shared/shared-organization.module.ts index 7979b6e2f36..96082de74f7 100644 --- a/apps/web/src/app/organizations/shared/shared-organization.module.ts +++ b/apps/web/src/app/organizations/shared/shared-organization.module.ts @@ -9,6 +9,6 @@ import { SearchInputComponent } from "./components/search-input/search-input.com @NgModule({ imports: [SharedModule, CollectionDialogModule, AccessSelectorModule], declarations: [SearchInputComponent], - exports: [SharedModule, CollectionDialogModule, AccessSelectorModule], + exports: [SharedModule, CollectionDialogModule, AccessSelectorModule, SearchInputComponent], }) export class SharedOrganizationModule {} diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 7b59a06a532..66a30146e63 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -27,16 +27,10 @@ import { NavbarComponent } from "../layouts/navbar.component"; import { UserLayoutComponent } from "../layouts/user-layout.component"; import { OrganizationCreateModule } from "../organizations/create/organization-create.module"; import { OrganizationLayoutComponent } from "../organizations/layouts/organization-layout.component"; -import { BulkConfirmComponent as OrgBulkConfirmComponent } from "../organizations/manage/bulk/bulk-confirm.component"; -import { BulkRemoveComponent as OrgBulkRemoveComponent } from "../organizations/manage/bulk/bulk-remove.component"; -import { BulkRestoreRevokeComponent as OrgBulkRestoreRevokeComponent } from "../organizations/manage/bulk/bulk-restore-revoke.component"; -import { BulkStatusComponent as OrgBulkStatusComponent } from "../organizations/manage/bulk/bulk-status.component"; import { CollectionsComponent as OrgManageCollectionsComponent } from "../organizations/manage/collections.component"; import { EntityEventsComponent as OrgEntityEventsComponent } from "../organizations/manage/entity-events.component"; import { EventsComponent as OrgEventsComponent } from "../organizations/manage/events.component"; import { ManageComponent as OrgManageComponent } from "../organizations/manage/manage.component"; -import { PeopleComponent as OrgPeopleComponent } from "../organizations/manage/people.component"; -import { ResetPasswordComponent as OrgResetPasswordComponent } from "../organizations/manage/reset-password.component"; import { UserConfirmComponent as OrgUserConfirmComponent } from "../organizations/manage/user-confirm.component"; import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component"; import { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component"; @@ -172,10 +166,6 @@ import { SharedModule } from "./shared.module"; OrganizationLayoutComponent, OrganizationPlansComponent, OrgAttachmentsComponent, - OrgBulkConfirmComponent, - OrgBulkRestoreRevokeComponent, - OrgBulkRemoveComponent, - OrgBulkStatusComponent, OrgCollectionsComponent, OrgEntityEventsComponent, OrgEventsComponent, @@ -183,8 +173,6 @@ import { SharedModule } from "./shared.module"; OrgInactiveTwoFactorReportComponent, OrgManageCollectionsComponent, OrgManageComponent, - OrgPeopleComponent, - OrgResetPasswordComponent, OrgReusedPasswordsReportComponent, OrgToolsComponent, OrgUnsecuredWebsitesReportComponent, @@ -289,10 +277,6 @@ import { SharedModule } from "./shared.module"; OrganizationLayoutComponent, OrganizationPlansComponent, OrgAttachmentsComponent, - OrgBulkConfirmComponent, - OrgBulkRestoreRevokeComponent, - OrgBulkRemoveComponent, - OrgBulkStatusComponent, OrgCollectionsComponent, OrgEntityEventsComponent, OrgEventsComponent, @@ -300,8 +284,6 @@ import { SharedModule } from "./shared.module"; OrgInactiveTwoFactorReportComponent, OrgManageCollectionsComponent, OrgManageComponent, - OrgPeopleComponent, - OrgResetPasswordComponent, OrgReusedPasswordsReportComponent, OrgToolsComponent, OrgUnsecuredWebsitesReportComponent, diff --git a/apps/web/src/app/shared/shared.module.ts b/apps/web/src/app/shared/shared.module.ts index 862453163d9..3d6ab6160e0 100644 --- a/apps/web/src/app/shared/shared.module.ts +++ b/apps/web/src/app/shared/shared.module.ts @@ -24,6 +24,7 @@ import { NavigationModule, TableModule, TabsModule, + ToggleGroupModule, } from "@bitwarden/components"; // Register the locales for the application @@ -59,6 +60,7 @@ import "./locales"; FormFieldModule, IconButtonModule, IconModule, + LinkModule, MenuModule, NavigationModule, TableModule, @@ -91,6 +93,7 @@ import "./locales"; MenuModule, NavigationModule, TableModule, + ToggleGroupModule, LinkModule, TabsModule, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 40fe3ea263d..445d05f8502 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -302,6 +302,9 @@ "searchOrganization": { "message": "Search organization" }, + "searchMembers": { + "message": "Search members" + }, "allItems": { "message": "All items" }, @@ -729,6 +732,9 @@ "noUsersInList": { "message": "There are no users to list." }, + "noMembersInList": { + "message": "There are no members to list." + }, "noEventsInList": { "message": "There are no events to list." }, @@ -2437,9 +2443,6 @@ "editMember": { "message": "Edit member" }, - "inviteMember": { - "message": "Invite member" - }, "inviteUserDesc": { "message": "Invite a new user to your organization by entering their Bitwarden account email address below. If they do not have a Bitwarden account already, they will be prompted to create a new account." }, @@ -2969,10 +2972,10 @@ } }, "confirmUsers": { - "message": "Confirm users" + "message": "Confirm members" }, "usersNeedConfirmed": { - "message": "You have users that have accepted their invitation, but still need to be confirmed. Users will not have access to the organization until they are confirmed." + "message": "You have members that have accepted their invitation, but still need to be confirmed. Members will not have access to the organization until they are confirmed." }, "startDate": { "message": "Start date" @@ -5883,19 +5886,25 @@ "selectGroupsAndMembers": { "message": "Select groups and members" }, - "selectMembers": { - "message": "Select members" - }, "userPermissionOverrideHelper": { "message": "Permissions set for a member will replace permissions set by that member's group" }, "noMembersOrGroupsAdded": { "message": "No members or groups added" }, - "noMembersAdded": { - "message": "No members added" - }, "deleted": { "message": "Deleted" + }, + "memberStatusFilter": { + "message": "Member status filter" + }, + "inviteMember": { + "message": "Invite member" + }, + "needsConfirmation": { + "message": "Needs confirmation" + }, + "memberRole": { + "message": "Member role" } } diff --git a/bitwarden_license/bit-web/src/app/providers/manage/bulk/bulk-confirm.component.ts b/bitwarden_license/bit-web/src/app/providers/manage/bulk/bulk-confirm.component.ts index 9459040a791..20c770daa17 100644 --- a/bitwarden_license/bit-web/src/app/providers/manage/bulk/bulk-confirm.component.ts +++ b/bitwarden_license/bit-web/src/app/providers/manage/bulk/bulk-confirm.component.ts @@ -3,12 +3,12 @@ import { Component, Input } from "@angular/core"; import { ProviderUserStatusType } from "@bitwarden/common/enums/providerUserStatusType"; import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/models/request/provider/provider-user-bulk-confirm.request"; import { ProviderUserBulkRequest } from "@bitwarden/common/models/request/provider/provider-user-bulk.request"; -import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "@bitwarden/web-vault/app/organizations/manage/bulk/bulk-confirm.component"; -import { BulkUserDetails } from "@bitwarden/web-vault/app/organizations/manage/bulk/bulk-status.component"; +import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-confirm.component"; +import { BulkUserDetails } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-status.component"; @Component({ templateUrl: - "../../../../../../../apps/web/src/app/organizations/manage/bulk/bulk-confirm.component.html", + "../../../../../../../apps/web/src/app/organizations/members/components/bulk/bulk-confirm.component.html", }) export class BulkConfirmComponent extends OrganizationBulkConfirmComponent { @Input() providerId: string; diff --git a/bitwarden_license/bit-web/src/app/providers/manage/bulk/bulk-remove.component.ts b/bitwarden_license/bit-web/src/app/providers/manage/bulk/bulk-remove.component.ts index 08d83b0ae47..1a3d39c4eaf 100644 --- a/bitwarden_license/bit-web/src/app/providers/manage/bulk/bulk-remove.component.ts +++ b/bitwarden_license/bit-web/src/app/providers/manage/bulk/bulk-remove.component.ts @@ -1,11 +1,11 @@ import { Component, Input } from "@angular/core"; import { ProviderUserBulkRequest } from "@bitwarden/common/models/request/provider/provider-user-bulk.request"; -import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from "@bitwarden/web-vault/app/organizations/manage/bulk/bulk-remove.component"; +import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-remove.component"; @Component({ templateUrl: - "../../../../../../../apps/web/src/app/organizations/manage/bulk/bulk-remove.component.html", + "../../../../../../../apps/web/src/app/organizations/members/components/bulk/bulk-remove.component.html", }) export class BulkRemoveComponent extends OrganizationBulkRemoveComponent { @Input() providerId: string; diff --git a/bitwarden_license/bit-web/src/app/providers/manage/people.component.ts b/bitwarden_license/bit-web/src/app/providers/manage/people.component.ts index b8ef60e8e4a..6029c6f2c3c 100644 --- a/bitwarden_license/bit-web/src/app/providers/manage/people.component.ts +++ b/bitwarden_license/bit-web/src/app/providers/manage/people.component.ts @@ -22,8 +22,8 @@ import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { ProviderUserBulkResponse } from "@bitwarden/common/models/response/provider/provider-user-bulk.response"; import { ProviderUserUserDetailsResponse } from "@bitwarden/common/models/response/provider/provider-user.response"; import { BasePeopleComponent } from "@bitwarden/web-vault/app/common/base.people.component"; -import { BulkStatusComponent } from "@bitwarden/web-vault/app/organizations/manage/bulk/bulk-status.component"; import { EntityEventsComponent } from "@bitwarden/web-vault/app/organizations/manage/entity-events.component"; +import { BulkStatusComponent } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-status.component"; import { BulkConfirmComponent } from "./bulk/bulk-confirm.component"; import { BulkRemoveComponent } from "./bulk/bulk-remove.component"; diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 1aca6f1d14b..6eb16fd0a16 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -354,7 +354,11 @@ export abstract class ApiService { ) => Promise; getOrganizationUserGroups: (organizationId: string, id: string) => Promise; getOrganizationUsers: ( - organizationId: string + organizationId: string, + options?: { + includeCollections?: boolean; + includeGroups?: boolean; + } ) => Promise>; getOrganizationUserResetPasswordDetails: ( organizationId: string, diff --git a/libs/common/src/models/response/organization-user.response.ts b/libs/common/src/models/response/organization-user.response.ts index 0a92a1fdce4..ddf7efed216 100644 --- a/libs/common/src/models/response/organization-user.response.ts +++ b/libs/common/src/models/response/organization-user.response.ts @@ -32,6 +32,8 @@ export class OrganizationUserUserDetailsResponse extends OrganizationUserRespons email: string; twoFactorEnabled: boolean; usesKeyConnector: boolean; + collections: SelectionReadOnlyResponse[] = []; + groups: string[] = []; constructor(response: any) { super(response); @@ -39,6 +41,14 @@ export class OrganizationUserUserDetailsResponse extends OrganizationUserRespons this.email = this.getResponseProperty("Email"); this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled"); this.usesKeyConnector = this.getResponseProperty("UsesKeyConnector") ?? false; + const collections = this.getResponseProperty("Collections"); + if (collections != null) { + this.collections = collections.map((c: any) => new SelectionReadOnlyResponse(c)); + } + const groups = this.getResponseProperty("Groups"); + if (groups != null) { + this.groups = groups; + } } } diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 4f66585d49a..985a6c46f34 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -984,11 +984,24 @@ export class ApiService implements ApiServiceAbstraction { } async getOrganizationUsers( - organizationId: string + organizationId: string, + options?: { + includeCollections?: boolean; + includeGroups?: boolean; + } ): Promise> { + const params = new URLSearchParams(); + + if (options?.includeCollections) { + params.set("includeCollections", "true"); + } + if (options?.includeGroups) { + params.set("includeGroups", "true"); + } + const r = await this.send( "GET", - "/organizations/" + organizationId + "/users", + `/organizations/${organizationId}/users?${params.toString()}`, null, true, true diff --git a/libs/components/src/toggle-group/toggle-group.component.ts b/libs/components/src/toggle-group/toggle-group.component.ts index adaed4bdb01..907b5c9c582 100644 --- a/libs/components/src/toggle-group/toggle-group.component.ts +++ b/libs/components/src/toggle-group/toggle-group.component.ts @@ -7,17 +7,17 @@ let nextId = 0; templateUrl: "./toggle-group.component.html", preserveWhitespaces: false, }) -export class ToggleGroupComponent { +export class ToggleGroupComponent { private id = nextId++; name = `bit-toggle-group-${this.id}`; - @Input() selected?: unknown; - @Output() selectedChange = new EventEmitter(); + @Input() selected?: TValue; + @Output() selectedChange = new EventEmitter(); @HostBinding("attr.role") role = "radiogroup"; @HostBinding("class") classList = ["tw-flex"]; - onInputInteraction(value: unknown) { + onInputInteraction(value: TValue) { this.selected = value; this.selectedChange.emit(value); } diff --git a/libs/components/src/toggle-group/toggle.component.ts b/libs/components/src/toggle-group/toggle.component.ts index 928b29c8818..76d845f1a2f 100644 --- a/libs/components/src/toggle-group/toggle.component.ts +++ b/libs/components/src/toggle-group/toggle.component.ts @@ -1,4 +1,4 @@ -import { HostBinding, Component, Input } from "@angular/core"; +import { Component, HostBinding, Input } from "@angular/core"; import { ToggleGroupComponent } from "./toggle-group.component"; @@ -9,12 +9,12 @@ let nextId = 0; templateUrl: "./toggle.component.html", preserveWhitespaces: false, }) -export class ToggleComponent { +export class ToggleComponent { id = nextId++; - @Input() value?: string; + @Input() value?: TValue; - constructor(private groupComponent: ToggleGroupComponent) {} + constructor(private groupComponent: ToggleGroupComponent) {} @HostBinding("tabIndex") tabIndex = "-1"; @HostBinding("class") classList = ["tw-group"]; @@ -67,6 +67,9 @@ export class ToggleComponent { "tw-py-1.5", "tw-px-3", + // Fix for bootstrap styles that add bottom margin + "!tw-mb-0", + // Fix for badge being pushed slightly lower when inside a button. // Insipired by bootstrap, which does the same. "[&>[bitBadge]]:tw-relative",