mirror of
https://github.com/bitwarden/browser
synced 2025-12-22 19:23:52 +00:00
refactor: move organizations folder to admin-console, refs AC-1202 (#5118)
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
@NgModule({})
|
||||
export class CoreOrganizationModule {}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./core-organization.module";
|
||||
export * from "./services";
|
||||
export * from "./views";
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { CollectionRequest } from "@bitwarden/common/admin-console/models/request/collection.request";
|
||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
|
||||
import {
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionResponse,
|
||||
} from "@bitwarden/common/admin-console/models/response/collection.response";
|
||||
import { EncString } from "@bitwarden/common/models/domain/enc-string";
|
||||
|
||||
import { CoreOrganizationModule } from "../core-organization.module";
|
||||
import { CollectionAdminView } from "../views/collection-admin.view";
|
||||
|
||||
@Injectable({ providedIn: CoreOrganizationModule })
|
||||
export class CollectionAdminService {
|
||||
constructor(private apiService: ApiService, private cryptoService: CryptoService) {}
|
||||
|
||||
async getAll(organizationId: string): Promise<CollectionAdminView[]> {
|
||||
const collectionResponse = await this.apiService.getManyCollectionsWithAccessDetails(
|
||||
organizationId
|
||||
);
|
||||
|
||||
if (collectionResponse?.data == null || collectionResponse.data.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await this.decryptMany(organizationId, collectionResponse.data);
|
||||
}
|
||||
|
||||
async get(
|
||||
organizationId: string,
|
||||
collectionId: string
|
||||
): Promise<CollectionAdminView | undefined> {
|
||||
const collectionResponse = await this.apiService.getCollectionAccessDetails(
|
||||
organizationId,
|
||||
collectionId
|
||||
);
|
||||
|
||||
if (collectionResponse == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [view] = await this.decryptMany(organizationId, [collectionResponse]);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
async save(collection: CollectionAdminView): Promise<unknown> {
|
||||
const request = await this.encrypt(collection);
|
||||
|
||||
let response: CollectionResponse;
|
||||
if (collection.id == null) {
|
||||
response = await this.apiService.postCollection(collection.organizationId, request);
|
||||
collection.id = response.id;
|
||||
} else {
|
||||
response = await this.apiService.putCollection(
|
||||
collection.organizationId,
|
||||
collection.id,
|
||||
request
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Implement upsert when in PS-1083: Collection Service refactors
|
||||
// await this.collectionService.upsert(data);
|
||||
return;
|
||||
}
|
||||
|
||||
async delete(organizationId: string, collectionId: string): Promise<void> {
|
||||
await this.apiService.deleteCollection(organizationId, collectionId);
|
||||
}
|
||||
|
||||
private async decryptMany(
|
||||
organizationId: string,
|
||||
collections: CollectionResponse[] | CollectionAccessDetailsResponse[]
|
||||
): Promise<CollectionAdminView[]> {
|
||||
const orgKey = await this.cryptoService.getOrgKey(organizationId);
|
||||
|
||||
const promises = collections.map(async (c) => {
|
||||
const view = new CollectionAdminView();
|
||||
view.id = c.id;
|
||||
view.name = await this.cryptoService.decryptToUtf8(new EncString(c.name), orgKey);
|
||||
view.externalId = c.externalId;
|
||||
view.organizationId = c.organizationId;
|
||||
|
||||
if (isCollectionAccessDetailsResponse(c)) {
|
||||
view.groups = c.groups;
|
||||
view.users = c.users;
|
||||
view.assigned = c.assigned;
|
||||
}
|
||||
|
||||
return view;
|
||||
});
|
||||
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
private async encrypt(model: CollectionAdminView): Promise<CollectionRequest> {
|
||||
if (model.organizationId == null) {
|
||||
throw new Error("Collection has no organization id.");
|
||||
}
|
||||
const key = await this.cryptoService.getOrgKey(model.organizationId);
|
||||
if (key == null) {
|
||||
throw new Error("No key for this collection's organization.");
|
||||
}
|
||||
const collection = new CollectionRequest();
|
||||
collection.externalId = model.externalId;
|
||||
collection.name = (await this.cryptoService.encrypt(model.name, key)).encryptedString;
|
||||
collection.groups = model.groups.map(
|
||||
(group) => new SelectionReadOnlyRequest(group.id, group.readOnly, group.hidePasswords)
|
||||
);
|
||||
collection.users = model.users.map(
|
||||
(user) => new SelectionReadOnlyRequest(user.id, user.readOnly, user.hidePasswords)
|
||||
);
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
|
||||
function isCollectionAccessDetailsResponse(
|
||||
response: CollectionResponse | CollectionAccessDetailsResponse
|
||||
): response is CollectionAccessDetailsResponse {
|
||||
const anyResponse = response as any;
|
||||
|
||||
return anyResponse?.groups instanceof Array && anyResponse?.users instanceof Array;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
|
||||
import { CoreOrganizationModule } from "../../core-organization.module";
|
||||
import { GroupView } from "../../views/group.view";
|
||||
|
||||
import { GroupRequest } from "./requests/group.request";
|
||||
import { OrganizationGroupBulkRequest } from "./requests/organization-group-bulk.request";
|
||||
import { GroupDetailsResponse, GroupResponse } from "./responses/group.response";
|
||||
|
||||
@Injectable({ providedIn: CoreOrganizationModule })
|
||||
export class GroupService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
async delete(orgId: string, groupId: string): Promise<void> {
|
||||
await this.apiService.send(
|
||||
"DELETE",
|
||||
"/organizations/" + orgId + "/groups/" + groupId,
|
||||
null,
|
||||
true,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
async deleteMany(orgId: string, groupIds: string[]): Promise<void> {
|
||||
await this.apiService.send(
|
||||
"DELETE",
|
||||
"/organizations/" + orgId + "/groups",
|
||||
new OrganizationGroupBulkRequest(groupIds),
|
||||
true,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
async get(orgId: string, groupId: string): Promise<GroupView> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
"/organizations/" + orgId + "/groups/" + groupId + "/details",
|
||||
null,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
return GroupView.fromResponse(new GroupDetailsResponse(r));
|
||||
}
|
||||
|
||||
async getAll(orgId: string): Promise<GroupView[]> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
"/organizations/" + orgId + "/groups",
|
||||
null,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
const listResponse = new ListResponse(r, GroupDetailsResponse);
|
||||
|
||||
return listResponse.data?.map((gr) => GroupView.fromResponse(gr)) ?? [];
|
||||
}
|
||||
|
||||
async save(group: GroupView): Promise<GroupView> {
|
||||
const request = new GroupRequest();
|
||||
request.name = group.name;
|
||||
request.accessAll = group.accessAll;
|
||||
request.users = group.members;
|
||||
request.collections = group.collections.map(
|
||||
(c) => new SelectionReadOnlyRequest(c.id, c.readOnly, c.hidePasswords)
|
||||
);
|
||||
|
||||
if (group.id == undefined) {
|
||||
return await this.postGroup(group.organizationId, request);
|
||||
} else {
|
||||
return await this.putGroup(group.organizationId, group.id, request);
|
||||
}
|
||||
}
|
||||
|
||||
private async postGroup(organizationId: string, request: GroupRequest): Promise<GroupView> {
|
||||
const r = await this.apiService.send(
|
||||
"POST",
|
||||
"/organizations/" + organizationId + "/groups",
|
||||
request,
|
||||
true,
|
||||
true
|
||||
);
|
||||
return GroupView.fromResponse(new GroupResponse(r));
|
||||
}
|
||||
|
||||
private async putGroup(
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: GroupRequest
|
||||
): Promise<GroupView> {
|
||||
const r = await this.apiService.send(
|
||||
"PUT",
|
||||
"/organizations/" + organizationId + "/groups/" + id,
|
||||
request,
|
||||
true,
|
||||
true
|
||||
);
|
||||
return GroupView.fromResponse(new GroupResponse(r));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
|
||||
|
||||
export class GroupRequest {
|
||||
name: string;
|
||||
accessAll: boolean;
|
||||
collections: SelectionReadOnlyRequest[] = [];
|
||||
users: string[] = [];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export class OrganizationGroupBulkRequest {
|
||||
ids: string[];
|
||||
|
||||
constructor(ids: string[]) {
|
||||
this.ids = ids == null ? [] : ids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { SelectionReadOnlyResponse } from "@bitwarden/common/admin-console/models/response/selection-read-only.response";
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
|
||||
export class GroupResponse extends BaseResponse {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
name: string;
|
||||
accessAll: boolean;
|
||||
externalId: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.id = this.getResponseProperty("Id");
|
||||
this.organizationId = this.getResponseProperty("OrganizationId");
|
||||
this.name = this.getResponseProperty("Name");
|
||||
this.accessAll = this.getResponseProperty("AccessAll");
|
||||
this.externalId = this.getResponseProperty("ExternalId");
|
||||
}
|
||||
}
|
||||
|
||||
export class GroupDetailsResponse extends GroupResponse {
|
||||
collections: SelectionReadOnlyResponse[] = [];
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
const collections = this.getResponseProperty("Collections");
|
||||
if (collections != null) {
|
||||
this.collections = collections.map((c: any) => new SelectionReadOnlyResponse(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./group/group.service";
|
||||
export * from "./collection-admin.service";
|
||||
export * from "./user-admin.service";
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||
import {
|
||||
OrganizationUserInviteRequest,
|
||||
OrganizationUserUpdateRequest,
|
||||
} from "@bitwarden/common/abstractions/organization-user/requests";
|
||||
import { OrganizationUserDetailsResponse } from "@bitwarden/common/abstractions/organization-user/responses";
|
||||
|
||||
import { CoreOrganizationModule } from "../core-organization.module";
|
||||
import { OrganizationUserAdminView } from "../views/organization-user-admin-view";
|
||||
|
||||
@Injectable({ providedIn: CoreOrganizationModule })
|
||||
export class UserAdminService {
|
||||
constructor(private organizationUserService: OrganizationUserService) {}
|
||||
|
||||
async get(
|
||||
organizationId: string,
|
||||
organizationUserId: string
|
||||
): Promise<OrganizationUserAdminView | undefined> {
|
||||
const userResponse = await this.organizationUserService.getOrganizationUser(
|
||||
organizationId,
|
||||
organizationUserId,
|
||||
{
|
||||
includeGroups: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (userResponse == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [view] = await this.decryptMany(organizationId, [userResponse]);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
async save(user: OrganizationUserAdminView): Promise<void> {
|
||||
const request = new OrganizationUserUpdateRequest();
|
||||
request.accessAll = user.accessAll;
|
||||
request.permissions = user.permissions;
|
||||
request.type = user.type;
|
||||
request.collections = user.collections;
|
||||
request.groups = user.groups;
|
||||
request.accessSecretsManager = user.accessSecretsManager;
|
||||
|
||||
await this.organizationUserService.putOrganizationUser(user.organizationId, user.id, request);
|
||||
}
|
||||
|
||||
async invite(emails: string[], user: OrganizationUserAdminView): Promise<void> {
|
||||
const request = new OrganizationUserInviteRequest();
|
||||
request.emails = emails;
|
||||
request.accessAll = user.accessAll;
|
||||
request.permissions = user.permissions;
|
||||
request.type = user.type;
|
||||
request.collections = user.collections;
|
||||
request.groups = user.groups;
|
||||
request.accessSecretsManager = user.accessSecretsManager;
|
||||
|
||||
await this.organizationUserService.postOrganizationUserInvite(user.organizationId, request);
|
||||
}
|
||||
|
||||
private async decryptMany(
|
||||
organizationId: string,
|
||||
users: OrganizationUserDetailsResponse[]
|
||||
): Promise<OrganizationUserAdminView[]> {
|
||||
const promises = users.map(async (u) => {
|
||||
const view = new OrganizationUserAdminView();
|
||||
|
||||
view.id = u.id;
|
||||
view.organizationId = organizationId;
|
||||
view.userId = u.userId;
|
||||
view.type = u.type;
|
||||
view.status = u.status;
|
||||
view.externalId = u.externalId;
|
||||
view.accessAll = u.accessAll;
|
||||
view.permissions = u.permissions;
|
||||
view.resetPasswordEnrolled = u.resetPasswordEnrolled;
|
||||
view.collections = u.collections.map((c) => ({
|
||||
id: c.id,
|
||||
hidePasswords: c.hidePasswords,
|
||||
readOnly: c.readOnly,
|
||||
}));
|
||||
view.groups = u.groups;
|
||||
view.accessSecretsManager = u.accessSecretsManager;
|
||||
|
||||
return view;
|
||||
});
|
||||
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { View } from "@bitwarden/common/models/view/view";
|
||||
|
||||
interface SelectionResponseLike {
|
||||
id: string;
|
||||
readOnly: boolean;
|
||||
hidePasswords: boolean;
|
||||
}
|
||||
|
||||
export class CollectionAccessSelectionView extends View {
|
||||
readonly id: string;
|
||||
readonly readOnly: boolean;
|
||||
readonly hidePasswords: boolean;
|
||||
|
||||
constructor(response?: SelectionResponseLike) {
|
||||
super();
|
||||
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.id = response.id;
|
||||
this.readOnly = response.readOnly;
|
||||
this.hidePasswords = response.hidePasswords;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
||||
import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/admin-console/models/response/collection.response";
|
||||
|
||||
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
|
||||
|
||||
export class CollectionAdminView extends CollectionView {
|
||||
groups: CollectionAccessSelectionView[] = [];
|
||||
users: CollectionAccessSelectionView[] = [];
|
||||
|
||||
/**
|
||||
* Flag indicating the user has been explicitly assigned to this Collection
|
||||
*/
|
||||
assigned: boolean;
|
||||
|
||||
constructor(response?: CollectionAccessDetailsResponse) {
|
||||
super(response);
|
||||
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.groups = response.groups
|
||||
? response.groups.map((g) => new CollectionAccessSelectionView(g))
|
||||
: [];
|
||||
|
||||
this.users = response.users
|
||||
? response.users.map((g) => new CollectionAccessSelectionView(g))
|
||||
: [];
|
||||
|
||||
this.assigned = response.assigned;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { View } from "@bitwarden/common/src/models/view/view";
|
||||
|
||||
import { GroupDetailsResponse, GroupResponse } from "../services/group/responses/group.response";
|
||||
|
||||
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
|
||||
|
||||
export class GroupView implements View {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
name: string;
|
||||
accessAll: boolean;
|
||||
externalId: string;
|
||||
collections: CollectionAccessSelectionView[] = [];
|
||||
members: string[] = [];
|
||||
|
||||
static fromResponse(response: GroupResponse): GroupView {
|
||||
const view: GroupView = Object.assign(new GroupView(), response) as GroupView;
|
||||
|
||||
if (response instanceof GroupDetailsResponse && response.collections != undefined) {
|
||||
view.collections = response.collections.map((c) => new CollectionAccessSelectionView(c));
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./collection-access-selection.view";
|
||||
export * from "./collection-admin.view";
|
||||
export * from "./group.view";
|
||||
export * from "./organization-user.view";
|
||||
export * from "./organization-user-admin-view";
|
||||
@@ -0,0 +1,22 @@
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums/organization-user-status-type";
|
||||
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums/organization-user-type";
|
||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||
|
||||
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
|
||||
|
||||
export class OrganizationUserAdminView {
|
||||
id: string;
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
type: OrganizationUserType;
|
||||
status: OrganizationUserStatusType;
|
||||
externalId: string;
|
||||
accessAll: boolean;
|
||||
permissions: PermissionsApi;
|
||||
resetPasswordEnrolled: boolean;
|
||||
|
||||
collections: CollectionAccessSelectionView[] = [];
|
||||
groups: string[] = [];
|
||||
|
||||
accessSecretsManager: boolean;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/abstractions/organization-user/responses";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums/organization-user-status-type";
|
||||
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums/organization-user-type";
|
||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||
|
||||
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;
|
||||
avatarColor: 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;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import { CollectionDetailsResponse } from "@bitwarden/common/admin-console/model
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { GroupService, GroupView } from "../../../organizations/core";
|
||||
import { GroupService, GroupView } from "../core";
|
||||
import {
|
||||
AccessItemType,
|
||||
AccessItemValue,
|
||||
|
||||
@@ -33,7 +33,7 @@ import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { GroupService, GroupView } from "../../../organizations/core";
|
||||
import { GroupService, GroupView } from "../core";
|
||||
|
||||
import {
|
||||
GroupAddEditDialogResultType,
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="bulkTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="bulkTitle">
|
||||
{{ "confirmUsers" | i18n }}
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="card-body text-center" *ngIf="loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
{{ "loading" | i18n }}
|
||||
</div>
|
||||
<app-callout type="danger" *ngIf="filteredUsers.length <= 0">
|
||||
{{ "noSelectedUsersApplicable" | i18n }}
|
||||
</app-callout>
|
||||
<app-callout type="error" *ngIf="error">
|
||||
{{ error }}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="!loading && !done">
|
||||
<p>
|
||||
{{ "fingerprintEnsureIntegrityVerify" | i18n }}
|
||||
<a href="https://bitwarden.com/help/fingerprint-phrase/" target="_blank" rel="noopener">
|
||||
{{ "learnMore" | i18n }}</a
|
||||
>
|
||||
</p>
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
<th>{{ "fingerprint" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let user of filteredUsers">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ fingerprints.get(user.id) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngFor="let user of excludedUsers">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ "bulkFilteredMessage" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!loading && done">
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
<th>{{ "status" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let user of filteredUsers">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td *ngIf="statuses.has(user.id)">
|
||||
{{ statuses.get(user.id) }}
|
||||
</td>
|
||||
<td *ngIf="!statuses.has(user.id)">
|
||||
{{ "bulkFilteredMessage" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-submit"
|
||||
*ngIf="!done"
|
||||
[disabled]="loading"
|
||||
(click)="submit()"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "confirm" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||
import { OrganizationUserBulkConfirmRequest } from "@bitwarden/common/abstractions/organization-user/requests";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums/organization-user-status-type";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
|
||||
import { BulkUserDetails } from "./bulk-status.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-bulk-confirm",
|
||||
templateUrl: "bulk-confirm.component.html",
|
||||
})
|
||||
export class BulkConfirmComponent implements OnInit {
|
||||
@Input() organizationId: string;
|
||||
@Input() users: BulkUserDetails[];
|
||||
|
||||
excludedUsers: BulkUserDetails[];
|
||||
filteredUsers: BulkUserDetails[];
|
||||
publicKeys: Map<string, Uint8Array> = new Map();
|
||||
fingerprints: Map<string, string> = new Map();
|
||||
statuses: Map<string, string> = new Map();
|
||||
|
||||
loading = true;
|
||||
done = false;
|
||||
error: string;
|
||||
|
||||
constructor(
|
||||
protected cryptoService: CryptoService,
|
||||
protected apiService: ApiService,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
private i18nService: I18nService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.excludedUsers = this.users.filter((u) => !this.isAccepted(u));
|
||||
this.filteredUsers = this.users.filter((u) => this.isAccepted(u));
|
||||
|
||||
if (this.filteredUsers.length <= 0) {
|
||||
this.done = true;
|
||||
}
|
||||
|
||||
const response = await this.getPublicKeys();
|
||||
|
||||
for (const entry of response.data) {
|
||||
const publicKey = Utils.fromB64ToArray(entry.key);
|
||||
const fingerprint = await this.cryptoService.getFingerprint(entry.userId, publicKey.buffer);
|
||||
if (fingerprint != null) {
|
||||
this.publicKeys.set(entry.id, publicKey);
|
||||
this.fingerprints.set(entry.id, fingerprint.join("-"));
|
||||
}
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const key = await this.getCryptoKey();
|
||||
const userIdsWithKeys: any[] = [];
|
||||
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.buffer);
|
||||
userIdsWithKeys.push({
|
||||
id: user.id,
|
||||
key: encryptedKey.encryptedString,
|
||||
});
|
||||
}
|
||||
const response = await this.postConfirmRequest(userIdsWithKeys);
|
||||
|
||||
response.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 isAccepted(user: BulkUserDetails) {
|
||||
return user.status === OrganizationUserStatusType.Accepted;
|
||||
}
|
||||
|
||||
protected async getPublicKeys() {
|
||||
return await this.organizationUserService.postOrganizationUsersPublicKey(
|
||||
this.organizationId,
|
||||
this.filteredUsers.map((user) => user.id)
|
||||
);
|
||||
}
|
||||
|
||||
protected getCryptoKey() {
|
||||
return this.cryptoService.getOrgKey(this.organizationId);
|
||||
}
|
||||
|
||||
protected async postConfirmRequest(userIdsWithKeys: any[]) {
|
||||
const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys);
|
||||
return await this.organizationUserService.postOrganizationUserBulkConfirm(
|
||||
this.organizationId,
|
||||
request
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="bulkTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="bulkTitle">
|
||||
{{ "removeUsers" | i18n }}
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<app-callout type="danger" *ngIf="users.length <= 0">
|
||||
{{ "noSelectedUsersApplicable" | i18n }}
|
||||
</app-callout>
|
||||
<app-callout type="error" *ngIf="error">
|
||||
{{ error }}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="!done">
|
||||
<app-callout type="warning" *ngIf="users.length > 0 && !error">
|
||||
{{ removeUsersWarning }}
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let user of users">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="done">
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
<th>{{ "status" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let user of users">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td *ngIf="statuses.has(user.id)">
|
||||
{{ statuses.get(user.id) }}
|
||||
</td>
|
||||
<td *ngIf="!statuses.has(user.id)">
|
||||
{{ "bulkFilteredMessage" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-submit"
|
||||
*ngIf="!done && users.length > 0"
|
||||
[disabled]="loading"
|
||||
(click)="submit()"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "removeUsers" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||
|
||||
import { BulkUserDetails } from "./bulk-status.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-bulk-remove",
|
||||
templateUrl: "bulk-remove.component.html",
|
||||
})
|
||||
export class BulkRemoveComponent {
|
||||
@Input() organizationId: string;
|
||||
@Input() users: BulkUserDetails[];
|
||||
|
||||
statuses: Map<string, string> = new Map();
|
||||
|
||||
loading = false;
|
||||
done = false;
|
||||
error: string;
|
||||
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
protected i18nService: I18nService,
|
||||
private organizationUserService: OrganizationUserService
|
||||
) {}
|
||||
|
||||
async submit() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await this.deleteUsers();
|
||||
|
||||
response.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 async deleteUsers() {
|
||||
return await this.organizationUserService.deleteManyOrganizationUsers(
|
||||
this.organizationId,
|
||||
this.users.map((user) => user.id)
|
||||
);
|
||||
}
|
||||
|
||||
protected get removeUsersWarning() {
|
||||
return this.i18nService.t("removeOrgUsersConfirmation");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="bulkTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="bulkTitle">
|
||||
{{ bulkTitle }}
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<app-callout type="danger" *ngIf="users.length <= 0">
|
||||
{{ "noSelectedUsersApplicable" | i18n }}
|
||||
</app-callout>
|
||||
<app-callout type="error" *ngIf="error">
|
||||
{{ error }}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="!done">
|
||||
<app-callout type="warning" *ngIf="users.length > 0 && !error && isRevoking">
|
||||
{{ "revokeUsersWarning" | i18n }}
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let user of users">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="done">
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
<th>{{ "status" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let user of users">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td *ngIf="statuses.has(user.id)">
|
||||
{{ statuses.get(user.id) }}
|
||||
</td>
|
||||
<td *ngIf="!statuses.has(user.id)">
|
||||
{{ "bulkFilteredMessage" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-submit"
|
||||
*ngIf="!done && users.length > 0"
|
||||
[disabled]="loading"
|
||||
(click)="submit()"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ bulkTitle }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { ModalConfig } from "@bitwarden/angular/services/modal.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||
|
||||
import { BulkUserDetails } from "./bulk-status.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-bulk-restore-revoke",
|
||||
templateUrl: "bulk-restore-revoke.component.html",
|
||||
})
|
||||
export class BulkRestoreRevokeComponent {
|
||||
isRevoking: boolean;
|
||||
organizationId: string;
|
||||
users: BulkUserDetails[];
|
||||
|
||||
statuses: Map<string, string> = new Map();
|
||||
|
||||
loading = false;
|
||||
done = false;
|
||||
error: string;
|
||||
|
||||
constructor(
|
||||
protected i18nService: I18nService,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
config: ModalConfig
|
||||
) {
|
||||
this.isRevoking = config.data.isRevoking;
|
||||
this.organizationId = config.data.organizationId;
|
||||
this.users = config.data.users;
|
||||
}
|
||||
|
||||
get bulkTitle() {
|
||||
const titleKey = this.isRevoking ? "revokeUsers" : "restoreUsers";
|
||||
return this.i18nService.t(titleKey);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await this.performBulkUserAction();
|
||||
|
||||
const bulkMessage = this.isRevoking ? "bulkRevokedMessage" : "bulkRestoredMessage";
|
||||
response.data.forEach((entry) => {
|
||||
const error = entry.error !== "" ? entry.error : this.i18nService.t(bulkMessage);
|
||||
this.statuses.set(entry.id, error);
|
||||
});
|
||||
this.done = true;
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
protected async performBulkUserAction() {
|
||||
const userIds = this.users.map((user) => user.id);
|
||||
if (this.isRevoking) {
|
||||
return await this.organizationUserService.revokeManyOrganizationUsers(
|
||||
this.organizationId,
|
||||
userIds
|
||||
);
|
||||
} else {
|
||||
return await this.organizationUserService.restoreManyOrganizationUsers(
|
||||
this.organizationId,
|
||||
userIds
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="bulkTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="bulkTitle">
|
||||
{{ "bulkConfirmStatus" | i18n }}
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="card-body text-center" *ngIf="loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
{{ "loading" | i18n }}
|
||||
</div>
|
||||
<table class="table table-hover table-list" *ngIf="!loading">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
<th>{{ "status" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let item of users">
|
||||
<td width="30">
|
||||
<bit-avatar
|
||||
[text]="item.user | userName"
|
||||
[id]="item.user.id"
|
||||
size="small"
|
||||
></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ item.user.email }}
|
||||
<small class="text-muted d-block" *ngIf="item.user.name">{{ item.user.name }}</small>
|
||||
</td>
|
||||
<td class="text-danger" *ngIf="item.error">
|
||||
{{ item.message }}
|
||||
</td>
|
||||
<td *ngIf="!item.error">
|
||||
{{ item.message }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums/organization-user-status-type";
|
||||
import { ProviderUserStatusType } from "@bitwarden/common/admin-console/enums/provider-user-status-type";
|
||||
|
||||
export interface BulkUserDetails {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
status: OrganizationUserStatusType | ProviderUserStatusType;
|
||||
}
|
||||
|
||||
type BulkStatusEntry = {
|
||||
user: BulkUserDetails;
|
||||
error: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-bulk-status",
|
||||
templateUrl: "bulk-status.component.html",
|
||||
})
|
||||
export class BulkStatusComponent {
|
||||
users: BulkStatusEntry[];
|
||||
loading = false;
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
GroupView,
|
||||
OrganizationUserAdminView,
|
||||
UserAdminService,
|
||||
} from "../../../../../organizations/core";
|
||||
} from "../../../core";
|
||||
import {
|
||||
AccessItemType,
|
||||
AccessItemValue,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./members.module";
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { canAccessMembersTab } from "@bitwarden/common/admin-console/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 {}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedOrganizationModule } from "../../../organizations/shared";
|
||||
import { LooseComponentsModule } 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 { MembersRoutingModule } from "./members-routing.module";
|
||||
import { PeopleComponent } from "./people.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedOrganizationModule,
|
||||
LooseComponentsModule,
|
||||
MembersRoutingModule,
|
||||
UserDialogModule,
|
||||
],
|
||||
declarations: [
|
||||
BulkConfirmComponent,
|
||||
BulkRemoveComponent,
|
||||
BulkRestoreRevokeComponent,
|
||||
BulkStatusComponent,
|
||||
PeopleComponent,
|
||||
ResetPasswordComponent,
|
||||
],
|
||||
})
|
||||
export class MembersModule {}
|
||||
@@ -55,13 +55,13 @@ import {
|
||||
|
||||
import { EntityEventsComponent } from "../../../admin-console/organizations/manage/entity-events.component";
|
||||
import { BasePeopleComponent } from "../../../common/base.people.component";
|
||||
import { GroupService } from "../../../organizations/core";
|
||||
import { OrganizationUserView } from "../../../organizations/core/views/organization-user.view";
|
||||
import { BulkConfirmComponent } from "../../../organizations/members/components/bulk/bulk-confirm.component";
|
||||
import { BulkRemoveComponent } from "../../../organizations/members/components/bulk/bulk-remove.component";
|
||||
import { BulkRestoreRevokeComponent } from "../../../organizations/members/components/bulk/bulk-restore-revoke.component";
|
||||
import { BulkStatusComponent } from "../../../organizations/members/components/bulk/bulk-status.component";
|
||||
import { GroupService } from "../core";
|
||||
import { OrganizationUserView } from "../core/views/organization-user.view";
|
||||
|
||||
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,
|
||||
MemberDialogTab,
|
||||
|
||||
@@ -51,7 +51,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "members",
|
||||
loadChildren: () => import("../../organizations/members").then((m) => m.MembersModule),
|
||||
loadChildren: () => import("./members").then((m) => m.MembersModule),
|
||||
},
|
||||
{
|
||||
path: "groups",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { CoreOrganizationModule } from "../../organizations/core";
|
||||
import { SharedOrganizationModule } from "../../organizations/shared";
|
||||
|
||||
import { CoreOrganizationModule } from "./core";
|
||||
import { GroupAddEditComponent } from "./manage/group-add-edit.component";
|
||||
import { GroupsComponent } from "./manage/groups.component";
|
||||
import { OrganizationsRoutingModule } from "./organization-routing.module";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enum
|
||||
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums/organization-user-type";
|
||||
import { SelectItemView } from "@bitwarden/components";
|
||||
|
||||
import { CollectionAccessSelectionView } from "../../../../../organizations/core";
|
||||
import { CollectionAccessSelectionView } from "../../../core";
|
||||
|
||||
/**
|
||||
* Permission options that replace/correspond with readOnly and hidePassword server fields.
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
CollectionAdminView,
|
||||
GroupService,
|
||||
GroupView,
|
||||
} from "../../../../../organizations/core";
|
||||
} from "../../../core";
|
||||
import {
|
||||
AccessItemType,
|
||||
AccessItemValue,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<label class="tw-sr-only" [for]="id">{{ "search" | i18n }}</label>
|
||||
<div class="tw-relative tw-flex tw-items-center">
|
||||
<label
|
||||
[for]="id"
|
||||
aria-hidden="true"
|
||||
class="tw-absolute tw-left-2 tw-z-20 !tw-mb-0 tw-cursor-text"
|
||||
>
|
||||
<i class="bwi bwi-search bwi-fw tw-text-muted"></i>
|
||||
</label>
|
||||
<input
|
||||
bitInput
|
||||
type="search"
|
||||
[id]="id"
|
||||
[placeholder]="placeholder ?? ('search' | i18n)"
|
||||
class="tw-rounded-l tw-pl-9"
|
||||
[ngModel]="searchText"
|
||||
(ngModelChange)="onChange($event)"
|
||||
(blur)="onTouch()"
|
||||
[disabled]="disabled"
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||
|
||||
let nextId = 0;
|
||||
|
||||
@Component({
|
||||
selector: "app-search-input",
|
||||
templateUrl: "./search-input.component.html",
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
multi: true,
|
||||
useExisting: SearchInputComponent,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class SearchInputComponent implements ControlValueAccessor {
|
||||
private notifyOnChange: (v: string) => void;
|
||||
private notifyOnTouch: () => void;
|
||||
|
||||
protected id = `search-id-${nextId++}`;
|
||||
protected searchText: string;
|
||||
|
||||
@Input() disabled: boolean;
|
||||
@Input() placeholder: string;
|
||||
|
||||
onChange(searchText: string) {
|
||||
if (this.notifyOnChange != undefined) {
|
||||
this.notifyOnChange(searchText);
|
||||
}
|
||||
}
|
||||
|
||||
onTouch() {
|
||||
if (this.notifyOnTouch != undefined) {
|
||||
this.notifyOnTouch();
|
||||
}
|
||||
}
|
||||
|
||||
registerOnChange(fn: (v: string) => void): void {
|
||||
this.notifyOnChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: () => void): void {
|
||||
this.notifyOnTouch = fn;
|
||||
}
|
||||
|
||||
writeValue(searchText: string): void {
|
||||
this.searchText = searchText;
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean) {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { InputModule } from "@bitwarden/components/src/input/input.module";
|
||||
|
||||
import { PreloadedEnglishI18nModule } from "../../../../../tests/preloaded-english-i18n.module";
|
||||
|
||||
import { SearchInputComponent } from "./search-input.component";
|
||||
|
||||
export default {
|
||||
title: "Web/Organizations/Search Input",
|
||||
component: SearchInputComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [
|
||||
InputModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
PreloadedEnglishI18nModule,
|
||||
JslibModule,
|
||||
],
|
||||
providers: [],
|
||||
}),
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<SearchInputComponent> = (args: SearchInputComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<app-search-input [(ngModel)]="searchText" [placeholder]="placeholder" [disabled]="disabled"></app-search-input>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
|
||||
import { AccessSelectorModule } from "./components/access-selector/access-selector.module";
|
||||
import { CollectionDialogModule } from "./components/collection-dialog";
|
||||
import { SearchInputComponent } from "./components/search-input/search-input.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, CollectionDialogModule, AccessSelectorModule],
|
||||
declarations: [SearchInputComponent],
|
||||
exports: [SharedModule, CollectionDialogModule, AccessSelectorModule, SearchInputComponent],
|
||||
})
|
||||
export class SharedOrganizationModule {}
|
||||
Reference in New Issue
Block a user