mirror of
https://github.com/bitwarden/browser
synced 2025-12-22 19:23:52 +00:00
[AC-1383] Move collections to Vault (#5424)
* Move CollectionDialog to Vault * Fix CollectionDialogModule imports * Move CollectionAdminService and View to Vault * Move CollectionService to Vault * Split GroupService into internal and public facing classes * Move collection models to vault * lint spacing fix * Move collection spec file * Fix spec import * Update apps/web/src/app/core/core.module.ts Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * Remove CoreOrganizationModule from CollectionDialogModule --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
@@ -1,126 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.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 { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncString } from "@bitwarden/common/platform/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;
|
||||
}
|
||||
@@ -11,29 +11,11 @@ import { GroupRequest } from "./requests/group.request";
|
||||
import { OrganizationGroupBulkRequest } from "./requests/organization-group-bulk.request";
|
||||
import { GroupDetailsResponse, GroupResponse } from "./responses/group.response";
|
||||
|
||||
@Injectable({ providedIn: CoreOrganizationModule })
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
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
|
||||
);
|
||||
}
|
||||
constructor(protected apiService: ApiService) {}
|
||||
|
||||
async get(orgId: string, groupId: string): Promise<GroupView> {
|
||||
const r = await this.apiService.send(
|
||||
@@ -60,6 +42,33 @@ export class GroupService {
|
||||
|
||||
return listResponse.data?.map((gr) => GroupView.fromResponse(gr)) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: CoreOrganizationModule })
|
||||
export class InternalGroupService extends GroupService {
|
||||
constructor(protected apiService: ApiService) {
|
||||
super(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 save(group: GroupView): Promise<GroupView> {
|
||||
const request = new GroupRequest();
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./group/group.service";
|
||||
export * from "./collection-admin.service";
|
||||
export * from "./user-admin.service";
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
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";
|
||||
|
||||
@@ -6,16 +6,16 @@ import { catchError, combineLatest, from, map, of, Subject, switchMap, takeUntil
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||
import { CollectionService } from "@bitwarden/common/admin-console/abstractions/collection.service";
|
||||
import { CollectionData } from "@bitwarden/common/admin-console/models/data/collection.data";
|
||||
import { Collection } from "@bitwarden/common/admin-console/models/domain/collection";
|
||||
import { CollectionDetailsResponse } from "@bitwarden/common/admin-console/models/response/collection.response";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
|
||||
import { Collection } from "@bitwarden/common/vault/models/domain/collection";
|
||||
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
||||
|
||||
import { GroupService, GroupView } from "../core";
|
||||
import { InternalGroupService as GroupService, GroupView } from "../core";
|
||||
import {
|
||||
AccessItemType,
|
||||
AccessItemValue,
|
||||
|
||||
@@ -19,21 +19,21 @@ import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/s
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { CollectionService } from "@bitwarden/common/admin-console/abstractions/collection.service";
|
||||
import { CollectionData } from "@bitwarden/common/admin-console/models/data/collection.data";
|
||||
import { Collection } from "@bitwarden/common/admin-console/models/domain/collection";
|
||||
import {
|
||||
CollectionDetailsResponse,
|
||||
CollectionResponse,
|
||||
} from "@bitwarden/common/admin-console/models/response/collection.response";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
|
||||
import { Collection } from "@bitwarden/common/vault/models/domain/collection";
|
||||
import {
|
||||
CollectionDetailsResponse,
|
||||
CollectionResponse,
|
||||
} from "@bitwarden/common/vault/models/response/collection.response";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
|
||||
import { GroupService, GroupView } from "../core";
|
||||
import { InternalGroupService as GroupService, GroupView } from "../core";
|
||||
|
||||
import {
|
||||
GroupAddEditDialogResultType,
|
||||
|
||||
@@ -12,14 +12,14 @@ import {
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
|
||||
import { flagEnabled } from "../../../../../../utils/flags";
|
||||
import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service";
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
CollectionAdminService,
|
||||
GroupService,
|
||||
GroupView,
|
||||
OrganizationUserAdminView,
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
OrganizationUserUserDetailsResponse,
|
||||
} from "@bitwarden/common/abstractions/organization-user/responses";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { CollectionService } from "@bitwarden/common/admin-console/abstractions/collection.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction as PolicyApiService } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
@@ -40,11 +39,8 @@ import {
|
||||
OrganizationUserType,
|
||||
PolicyType,
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { CollectionData } from "@bitwarden/common/admin-console/models/data/collection.data";
|
||||
import { Collection } from "@bitwarden/common/admin-console/models/domain/collection";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
||||
import { CollectionDetailsResponse } from "@bitwarden/common/admin-console/models/response/collection.response";
|
||||
import { ProductType } from "@bitwarden/common/enums";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
@@ -53,7 +49,11 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
|
||||
import { Collection } from "@bitwarden/common/vault/models/domain/collection";
|
||||
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
||||
|
||||
import { EntityEventsComponent } from "../../../admin-console/organizations/manage/entity-events.component";
|
||||
import { BasePeopleComponent } from "../../../common/base.people.component";
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog [disablePadding]="!loading" dialogSize="large">
|
||||
<span bitDialogTitle>
|
||||
<ng-container *ngIf="editMode">
|
||||
{{ "editCollection" | i18n }}
|
||||
<span class="tw-text-sm tw-normal-case tw-text-muted" *ngIf="!loading">{{
|
||||
collection.name
|
||||
}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!editMode">
|
||||
{{ "newCollection" | i18n }}
|
||||
</ng-container>
|
||||
</span>
|
||||
<div bitDialogContent>
|
||||
<ng-container *ngIf="loading" #spinner>
|
||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" aria-hidden="true"></i>
|
||||
</ng-container>
|
||||
<bit-tab-group *ngIf="!loading" [(selectedIndex)]="tabIndex">
|
||||
<bit-tab label="{{ 'collectionInfo' | i18n }}">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "name" | i18n }}</bit-label>
|
||||
<input bitInput appAutofocus formControlName="name" />
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "externalId" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="externalId" />
|
||||
<bit-hint>{{ "externalIdDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "nestCollectionUnder" | i18n }}</bit-label>
|
||||
<bit-select bitInput formControlName="parent">
|
||||
<bit-option [value]="undefined" [label]="'noCollection' | i18n"> </bit-option>
|
||||
<bit-option
|
||||
*ngIf="deletedParentName"
|
||||
disabled
|
||||
icon="bwi-collection"
|
||||
[value]="deletedParentName"
|
||||
label="{{ deletedParentName }} ({{ 'deleted' | i18n }})"
|
||||
>
|
||||
</bit-option>
|
||||
<bit-option
|
||||
*ngFor="let collection of nestOptions"
|
||||
icon="bwi-collection"
|
||||
[value]="collection.name"
|
||||
[label]="collection.name"
|
||||
>
|
||||
</bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</bit-tab>
|
||||
<bit-tab label="{{ 'access' | i18n }}">
|
||||
<bit-access-selector
|
||||
*ngIf="organization.useGroups"
|
||||
[permissionMode]="PermissionMode.Edit"
|
||||
formControlName="access"
|
||||
[items]="accessItems"
|
||||
[columnHeader]="'groupAndMemberColumnHeader' | i18n"
|
||||
[selectorLabelText]="'selectGroupsAndMembers' | i18n"
|
||||
[selectorHelpText]="'userPermissionOverrideHelper' | i18n"
|
||||
[emptySelectionText]="'noMembersOrGroupsAdded' | i18n"
|
||||
></bit-access-selector>
|
||||
<bit-access-selector
|
||||
*ngIf="!organization.useGroups"
|
||||
[permissionMode]="PermissionMode.Edit"
|
||||
formControlName="access"
|
||||
[items]="accessItems"
|
||||
[columnHeader]="'memberColumnHeader' | i18n"
|
||||
[selectorLabelText]="'selectMembers' | i18n"
|
||||
[emptySelectionText]="'noMembersAdded' | i18n"
|
||||
></bit-access-selector>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" [disabled]="loading">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
(click)="cancel()"
|
||||
[disabled]="loading"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="editMode && organization?.canDeleteAssignedCollections"
|
||||
type="button"
|
||||
bitIconButton="bwi-trash"
|
||||
buttonType="danger"
|
||||
class="tw-ml-auto"
|
||||
bitFormButton
|
||||
[appA11yTitle]="'delete' | i18n"
|
||||
[bitAction]="delete"
|
||||
[disabled]="loading"
|
||||
></button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -1,301 +0,0 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { combineLatest, of, shareReplay, Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "@bitwarden/angular/services/dialog";
|
||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/abstractions/organization-user/responses";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { BitValidators } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
CollectionAdminService,
|
||||
CollectionAdminView,
|
||||
GroupService,
|
||||
GroupView,
|
||||
} from "../../../core";
|
||||
import {
|
||||
AccessItemType,
|
||||
AccessItemValue,
|
||||
AccessItemView,
|
||||
convertToPermission,
|
||||
convertToSelectionView,
|
||||
PermissionMode,
|
||||
} from "../access-selector";
|
||||
|
||||
export enum CollectionDialogTabType {
|
||||
Info = 0,
|
||||
Access = 1,
|
||||
}
|
||||
|
||||
export interface CollectionDialogParams {
|
||||
collectionId?: string;
|
||||
organizationId: string;
|
||||
initialTab?: CollectionDialogTabType;
|
||||
parentCollectionId?: string;
|
||||
}
|
||||
|
||||
export enum CollectionDialogResult {
|
||||
Saved = "saved",
|
||||
Canceled = "canceled",
|
||||
Deleted = "deleted",
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "collection-dialog.component.html",
|
||||
})
|
||||
export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected tabIndex: CollectionDialogTabType;
|
||||
protected loading = true;
|
||||
protected organization?: Organization;
|
||||
protected collection?: CollectionView;
|
||||
protected nestOptions: CollectionView[] = [];
|
||||
protected accessItems: AccessItemView[] = [];
|
||||
protected deletedParentName: string | undefined;
|
||||
protected formGroup = this.formBuilder.group({
|
||||
name: ["", [Validators.required, BitValidators.forbiddenCharacters(["/"])]],
|
||||
externalId: "",
|
||||
parent: undefined as string | undefined,
|
||||
access: [[] as AccessItemValue[]],
|
||||
});
|
||||
protected PermissionMode = PermissionMode;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) private params: CollectionDialogParams,
|
||||
private formBuilder: FormBuilder,
|
||||
private dialogRef: DialogRef<CollectionDialogResult>,
|
||||
private organizationService: OrganizationService,
|
||||
private groupService: GroupService,
|
||||
private collectionService: CollectionAdminService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
private dialogService: DialogServiceAbstraction
|
||||
) {
|
||||
this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const organization$ = of(this.organizationService.get(this.params.organizationId)).pipe(
|
||||
shareReplay({ refCount: true, bufferSize: 1 })
|
||||
);
|
||||
const groups$ = organization$.pipe(
|
||||
switchMap((organization) => {
|
||||
if (!organization.useGroups) {
|
||||
return of([] as GroupView[]);
|
||||
}
|
||||
|
||||
return this.groupService.getAll(this.params.organizationId);
|
||||
})
|
||||
);
|
||||
|
||||
combineLatest({
|
||||
organization: organization$,
|
||||
collections: this.collectionService.getAll(this.params.organizationId),
|
||||
collectionDetails: this.params.collectionId
|
||||
? this.collectionService.get(this.params.organizationId, this.params.collectionId)
|
||||
: of(null),
|
||||
groups: groups$,
|
||||
users: this.organizationUserService.getAllUsers(this.params.organizationId),
|
||||
})
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(({ organization, collections, collectionDetails, groups, users }) => {
|
||||
this.organization = organization;
|
||||
this.accessItems = [].concat(
|
||||
groups.map(mapGroupToAccessItemView),
|
||||
users.data.map(mapUserToAccessItemView)
|
||||
);
|
||||
|
||||
if (this.params.collectionId) {
|
||||
this.collection = collections.find((c) => c.id === this.collectionId);
|
||||
this.nestOptions = collections.filter((c) => c.id !== this.collectionId);
|
||||
|
||||
if (!this.collection) {
|
||||
throw new Error("Could not find collection to edit.");
|
||||
}
|
||||
|
||||
const { name, parent } = parseName(this.collection);
|
||||
if (parent !== undefined && !this.nestOptions.find((c) => c.name === parent)) {
|
||||
this.deletedParentName = parent;
|
||||
}
|
||||
|
||||
const accessSelections = mapToAccessSelections(collectionDetails);
|
||||
this.formGroup.patchValue({
|
||||
name,
|
||||
externalId: this.collection.externalId,
|
||||
parent,
|
||||
access: accessSelections,
|
||||
});
|
||||
} else {
|
||||
this.nestOptions = collections;
|
||||
const parent = collections.find((c) => c.id === this.params.parentCollectionId);
|
||||
this.formGroup.patchValue({ parent: parent?.name ?? undefined });
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
protected get collectionId() {
|
||||
return this.params.collectionId;
|
||||
}
|
||||
|
||||
protected get editMode() {
|
||||
return this.params.collectionId != undefined;
|
||||
}
|
||||
|
||||
protected async cancel() {
|
||||
this.close(CollectionDialogResult.Canceled);
|
||||
}
|
||||
|
||||
protected submit = async () => {
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
if (this.formGroup.invalid) {
|
||||
if (this.tabIndex === CollectionDialogTabType.Access) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("collectionInfo"))
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const collectionView = new CollectionAdminView();
|
||||
collectionView.id = this.params.collectionId;
|
||||
collectionView.organizationId = this.params.organizationId;
|
||||
collectionView.externalId = this.formGroup.controls.externalId.value;
|
||||
collectionView.groups = this.formGroup.controls.access.value
|
||||
.filter((v) => v.type === AccessItemType.Group)
|
||||
.map(convertToSelectionView);
|
||||
collectionView.users = this.formGroup.controls.access.value
|
||||
.filter((v) => v.type === AccessItemType.Member)
|
||||
.map(convertToSelectionView);
|
||||
|
||||
const parent = this.formGroup.controls.parent.value;
|
||||
if (parent) {
|
||||
collectionView.name = `${parent}/${this.formGroup.controls.name.value}`;
|
||||
} else {
|
||||
collectionView.name = this.formGroup.controls.name.value;
|
||||
}
|
||||
|
||||
await this.collectionService.save(collectionView);
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t(
|
||||
this.editMode ? "editedCollectionId" : "createdCollectionId",
|
||||
collectionView.name
|
||||
)
|
||||
);
|
||||
|
||||
this.close(CollectionDialogResult.Saved);
|
||||
};
|
||||
|
||||
protected delete = async () => {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: this.collection?.name,
|
||||
content: { key: "deleteCollectionConfirmation" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
});
|
||||
|
||||
if (!confirmed && this.params.collectionId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.collectionService.delete(this.params.organizationId, this.params.collectionId);
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("deletedCollectionId", this.collection?.name)
|
||||
);
|
||||
|
||||
this.close(CollectionDialogResult.Deleted);
|
||||
};
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
private close(result: CollectionDialogResult) {
|
||||
this.dialogRef.close(result);
|
||||
}
|
||||
}
|
||||
|
||||
function parseName(collection: CollectionView) {
|
||||
const nameParts = collection.name?.split("/");
|
||||
const name = nameParts[nameParts.length - 1];
|
||||
const parent = nameParts.length > 1 ? nameParts.slice(0, -1).join("/") : undefined;
|
||||
|
||||
return { name, parent };
|
||||
}
|
||||
|
||||
function mapGroupToAccessItemView(group: GroupView): AccessItemView {
|
||||
return {
|
||||
id: group.id,
|
||||
type: AccessItemType.Group,
|
||||
listName: group.name,
|
||||
labelName: group.name,
|
||||
accessAllItems: group.accessAll,
|
||||
readonly: group.accessAll,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Use view when user apis are migrated to a service
|
||||
function mapUserToAccessItemView(user: OrganizationUserUserDetailsResponse): AccessItemView {
|
||||
return {
|
||||
id: user.id,
|
||||
type: AccessItemType.Member,
|
||||
email: user.email,
|
||||
role: user.type,
|
||||
listName: user.name?.length > 0 ? `${user.name} (${user.email})` : user.email,
|
||||
labelName: user.name ?? user.email,
|
||||
status: user.status,
|
||||
accessAllItems: user.accessAll,
|
||||
readonly: user.accessAll,
|
||||
};
|
||||
}
|
||||
|
||||
function mapToAccessSelections(collectionDetails: CollectionAdminView): AccessItemValue[] {
|
||||
if (collectionDetails == undefined) {
|
||||
return [];
|
||||
}
|
||||
return [].concat(
|
||||
collectionDetails.groups.map<AccessItemValue>((selection) => ({
|
||||
id: selection.id,
|
||||
type: AccessItemType.Group,
|
||||
permission: convertToPermission(selection),
|
||||
})),
|
||||
collectionDetails.users.map<AccessItemValue>((selection) => ({
|
||||
id: selection.id,
|
||||
type: AccessItemType.Member,
|
||||
permission: convertToPermission(selection),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open a CollectionDialog
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Configuration for the dialog
|
||||
*/
|
||||
export function openCollectionDialog(
|
||||
dialogService: DialogServiceAbstraction,
|
||||
config: DialogConfig<CollectionDialogParams>
|
||||
) {
|
||||
return dialogService.open<CollectionDialogResult, CollectionDialogParams>(
|
||||
CollectionDialogComponent,
|
||||
config
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SelectModule } from "@bitwarden/components";
|
||||
|
||||
import { AccessSelectorModule } from "../../../../../admin-console/organizations/shared/components/access-selector/access-selector.module";
|
||||
import { SharedModule } from "../../../../../shared";
|
||||
|
||||
import { CollectionDialogComponent } from "./collection-dialog.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, AccessSelectorModule, SelectModule],
|
||||
declarations: [CollectionDialogComponent],
|
||||
exports: [CollectionDialogComponent],
|
||||
})
|
||||
export class CollectionDialogModule {}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./collection-dialog.component";
|
||||
export * from "./collection-dialog.module";
|
||||
@@ -1,2 +1 @@
|
||||
export * from "./shared-organization.module";
|
||||
export * from "./components/collection-dialog";
|
||||
|
||||
@@ -5,11 +5,10 @@ import { SearchModule } from "@bitwarden/components";
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
|
||||
import { AccessSelectorModule } from "./components/access-selector/access-selector.module";
|
||||
import { CollectionDialogModule } from "./components/collection-dialog";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, CollectionDialogModule, AccessSelectorModule, SearchModule],
|
||||
imports: [SharedModule, AccessSelectorModule, SearchModule],
|
||||
declarations: [],
|
||||
exports: [SharedModule, CollectionDialogModule, AccessSelectorModule, SearchModule],
|
||||
exports: [SharedModule, AccessSelectorModule, SearchModule],
|
||||
})
|
||||
export class SharedOrganizationModule {}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CollectionService } from "@bitwarden/common/admin-console/abstractions/collection.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import {
|
||||
ImportService,
|
||||
|
||||
Reference in New Issue
Block a user