1
0
mirror of https://github.com/bitwarden/web synced 2025-12-11 05:43:16 +00:00

Refactor bulk delete and confirm (#1013)

* Prevent confirm dialog from showing when autoConfirm is enabled

* Fix bulk confirm not showing if more than 3 confirmed users in org.

* Refactor bulk confirm to show a single dialog with all fingerprints

* Move bulk status dialog to bulk folder

* Refactor bulk delete to use a custom modal

* Update src/locales/en/messages.json

Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>

Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
This commit is contained in:
Oscar Hinton
2021-06-09 17:04:21 +02:00
committed by GitHub
parent b20206d350
commit fd328eef2a
11 changed files with 441 additions and 140 deletions

View File

@@ -26,7 +26,6 @@ import { StorageService } from 'jslib-common/abstractions/storage.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { OrganizationKeysRequest } from 'jslib-common/models/request/organizationKeysRequest';
import { OrganizationUserBulkConfirmRequest } from 'jslib-common/models/request/organizationUserBulkConfirmRequest';
import { OrganizationUserBulkRequest } from 'jslib-common/models/request/organizationUserBulkRequest';
import { OrganizationUserConfirmRequest } from 'jslib-common/models/request/organizationUserConfirmRequest';
@@ -41,7 +40,9 @@ import { PolicyType } from 'jslib-common/enums/policyType';
import { Utils } from 'jslib-common/misc/utils';
import { ModalComponent } from '../../modal.component';
import { BulkStatusComponent } from './bulk-status.component';
import { BulkConfirmComponent } from './bulk/bulk-confirm.component';
import { BulkRemoveComponent } from './bulk/bulk-remove.component';
import { BulkStatusComponent } from './bulk/bulk-status.component';
import { EntityEventsComponent } from './entity-events.component';
import { ResetPasswordComponent } from './reset-password.component';
import { UserAddEditComponent } from './user-add-edit.component';
@@ -61,6 +62,8 @@ export class PeopleComponent implements OnInit {
@ViewChild('confirmTemplate', { read: ViewContainerRef, static: true }) confirmModalRef: ViewContainerRef;
@ViewChild('resetPasswordTemplate', { read: ViewContainerRef, static: true }) resetPasswordModalRef: ViewContainerRef;
@ViewChild('bulkStatusTemplate', { read: ViewContainerRef, static: true }) bulkStatusModalRef: ViewContainerRef;
@ViewChild('bulkConfirmTemplate', { read: ViewContainerRef, static: true }) bulkConfirmModalRef: ViewContainerRef;
@ViewChild('bulkRemoveTemplate', { read: ViewContainerRef, static: true }) bulkRemoveModalRef: ViewContainerRef;
loading = true;
organizationId: string;
@@ -237,6 +240,10 @@ export class PeopleComponent implements OnInit {
this.confirmedCount > 0 && this.confirmedCount < 3 && this.acceptedCount > 0;
}
get showBulkConfirmUsers(): boolean {
return this.acceptedCount > 0;
}
edit(user: OrganizationUserUserDetailsResponse) {
if (this.modal != null) {
this.modal.close();
@@ -329,30 +336,21 @@ export class PeopleComponent implements OnInit {
return;
}
const users = this.getCheckedUsers();
if (users.length <= 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('noSelectedUsersApplicable'));
return;
if (this.modal != null) {
this.modal.close();
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('removeSelectedUsersConfirmation'), this.i18nService.t('remove'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkRemoveModalRef.createComponent(factory).instance;
const childComponent = this.modal.show(BulkRemoveComponent, this.bulkRemoveModalRef);
try {
const request = new OrganizationUserBulkRequest(users.map(user => user.id));
const response = this.apiService.deleteManyOrganizationUsers(this.organizationId, request);
this.showBulkStatus(users, users, response, this.i18nService.t('bulkRemovedMessage'));
await response;
childComponent.organizationId = this.organizationId;
childComponent.users = this.getCheckedUsers();
this.modal.onClosed.subscribe(async () => {
await this.load();
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
this.modal = null;
});
}
async bulkReinvite() {
@@ -385,83 +383,38 @@ export class PeopleComponent implements OnInit {
return;
}
const users = this.getCheckedUsers();
const filteredUsers = users.filter(u => u.status === OrganizationUserStatusType.Accepted);
if (filteredUsers.length <= 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('noSelectedUsersApplicable'));
return;
if (this.modal != null) {
this.modal.close();
}
const publicKeyRequest = new OrganizationUserBulkRequest(filteredUsers.map(user => user.id));
const publicKeyResponse = await this.apiService.postOrganizationUsersPublicKey(this.organizationId, publicKeyRequest);
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkConfirmModalRef.createComponent(factory).instance;
const childComponent = this.modal.show(BulkConfirmComponent, this.bulkConfirmModalRef);
const keyMap = new Map<string, Uint8Array>();
publicKeyResponse.data.forEach(entry => {
keyMap.set(entry.id, Utils.fromB64ToArray(entry.key));
});
childComponent.organizationId = this.organizationId;
childComponent.users = this.getCheckedUsers();
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
const userIdsWithKeys: any[] = [];
const approvedUsers = [];
for (const user of filteredUsers) {
const publicKey = keyMap.get(user.id);
if (publicKey == null) {
continue;
}
if (await this.promptConfirmUser(user, publicKey)) {
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
approvedUsers.push(user);
userIdsWithKeys.push({
id: user.id,
key: key.encryptedString,
});
}
}
if (userIdsWithKeys.length <= 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('noSelectedUsersApplicable'));
return;
}
try {
const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys);
const response = this.apiService.postOrganizationUserBulkConfirm(this.organizationId, request);
this.showBulkStatus(users, approvedUsers, response, this.i18nService.t('bulkConfirmMessage'));
await response;
this.modal.onClosed.subscribe(async () => {
await this.load();
} catch (e) {
this.validationService.showError(e);
}
this.modal = null;
});
}
async confirm(user: OrganizationUserUserDetailsResponse): Promise<void> {
if (this.actionPromise != null) {
return;
async confirm(user: OrganizationUserUserDetailsResponse) {
function updateUser(self: PeopleComponent) {
user.status = OrganizationUserStatusType.Confirmed;
const mapIndex = self.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);
if (mapIndex > -1) {
self.statusMap.get(OrganizationUserStatusType.Accepted).splice(mapIndex, 1);
self.statusMap.get(OrganizationUserStatusType.Confirmed).push(user);
}
}
try {
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
const confirmed = await this.promptConfirmUser(user, publicKey);
if (!confirmed) {
return;
}
try {
// tslint:disable-next-line
console.log('User\'s fingerprint: ' +
(await this.cryptoService.getFingerprint(user.userId, publicKey.buffer)).join('-'));
} catch { }
const confirmUser = async (publicKey: Uint8Array) => {
try {
this.actionPromise = this.doConfirmation(user, publicKey);
await this.actionPromise;
this.confirmUser(user);
updateUser(this);
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', user.name || user.email));
} catch (e) {
this.validationService.showError(e);
@@ -469,6 +422,52 @@ export class PeopleComponent implements OnInit {
} finally {
this.actionPromise = null;
}
};
if (this.actionPromise != null) {
return;
}
const autoConfirm = await this.storageService.get<boolean>(ConstantsService.autoConfirmFingerprints);
if (autoConfirm == null || !autoConfirm) {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.confirmModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<UserConfirmComponent>(
UserConfirmComponent, this.confirmModalRef);
childComponent.name = user != null ? user.name || user.email : null;
childComponent.organizationId = this.organizationId;
childComponent.organizationUserId = user != null ? user.id : null;
childComponent.userId = user != null ? user.userId : null;
childComponent.onConfirmedUser.subscribe(async (publicKey: Uint8Array) => {
try {
await confirmUser(publicKey);
this.modal.close();
} catch (e) {
// tslint:disable-next-line
console.error('Handled exception:', e);
}
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
return;
}
try {
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
try {
// tslint:disable-next-line
console.log('User\'s fingerprint: ' +
(await this.cryptoService.getFingerprint(user.userId, publicKey.buffer)).join('-'));
} catch { }
await confirmUser(publicKey);
} catch (e) {
// tslint:disable-next-line
console.error('Handled exception:', e);
@@ -558,9 +557,9 @@ export class PeopleComponent implements OnInit {
request: Promise<ListResponse<OrganizationUserBulkResponse>>, successfullMessage: string) {
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.eventsModalRef.createComponent(factory).instance;
this.modal = this.bulkStatusModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<BulkStatusComponent>(
BulkStatusComponent, this.eventsModalRef);
BulkStatusComponent, this.bulkStatusModalRef);
childComponent.loading = true;
@@ -639,50 +638,7 @@ export class PeopleComponent implements OnInit {
}
}
private confirmUser(user: OrganizationUserUserDetailsResponse) {
user.status = OrganizationUserStatusType.Confirmed;
const mapIndex = this.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);
if (mapIndex > -1) {
this.statusMap.get(OrganizationUserStatusType.Accepted).splice(mapIndex, 1);
this.statusMap.get(OrganizationUserStatusType.Confirmed).push(user);
}
}
private getCheckedUsers() {
return this.users.filter(u => (u as any).checked);
}
private promptConfirmUser(user: OrganizationUserUserDetailsResponse, publicKey: Uint8Array): Promise<boolean> {
return new Promise(async (resolve, reject) => {
const autoConfirm = await this.storageService.get<boolean>(ConstantsService.autoConfirmFingerprints);
if (autoConfirm ?? false) {
resolve(true);
}
let success = false;
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.confirmModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<UserConfirmComponent>(
UserConfirmComponent, this.confirmModalRef);
childComponent.name = user != null ? user.name || user.email : null;
childComponent.organizationId = this.organizationId;
childComponent.organizationUserId = user != null ? user.id : null;
childComponent.userId = user != null ? user.userId : null;
childComponent.publicKey = publicKey;
childComponent.onConfirmedUser.subscribe(() => {
success = true;
this.modal.close();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
setTimeout(() => resolve(success), 10);
});
});
}
}