From fd328eef2a79e78f2129638a775ef9db1d953492 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Wed, 9 Jun 2021 17:04:21 +0200 Subject: [PATCH] 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> --- src/app/app.module.ts | 8 +- .../manage/bulk/bulk-confirm.component.html | 100 ++++++++ .../manage/bulk/bulk-confirm.component.ts | 95 ++++++++ .../manage/bulk/bulk-remove.component.html | 81 +++++++ .../manage/bulk/bulk-remove.component.ts | 46 ++++ .../{ => bulk}/bulk-status.component.html | 0 .../{ => bulk}/bulk-status.component.ts | 0 .../manage/people.component.html | 4 +- .../organizations/manage/people.component.ts | 220 +++++++----------- .../manage/user-confirm.component.ts | 14 +- src/locales/en/messages.json | 13 +- 11 files changed, 441 insertions(+), 140 deletions(-) create mode 100644 src/app/organizations/manage/bulk/bulk-confirm.component.html create mode 100644 src/app/organizations/manage/bulk/bulk-confirm.component.ts create mode 100644 src/app/organizations/manage/bulk/bulk-remove.component.html create mode 100644 src/app/organizations/manage/bulk/bulk-remove.component.ts rename src/app/organizations/manage/{ => bulk}/bulk-status.component.html (100%) rename src/app/organizations/manage/{ => bulk}/bulk-status.component.ts (100%) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 75dce49c52..aee6e9c6e2 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -38,7 +38,9 @@ import { TwoFactorComponent } from './accounts/two-factor.component'; import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component'; import { VerifyRecoverDeleteComponent } from './accounts/verify-recover-delete.component'; -import { BulkStatusComponent as OrgBulkStatusComponent } from './organizations/manage/bulk-status.component'; +import { BulkConfirmComponent as OrgBulkConfirmComponent } from './organizations/manage/bulk/bulk-confirm.component'; +import { BulkRemoveComponent as OrgBulkRemoveComponent } from './organizations/manage/bulk/bulk-remove.component'; +import { BulkStatusComponent as OrgBulkStatusComponent } from './organizations/manage/bulk/bulk-status.component'; import { CollectionAddEditComponent as OrgCollectionAddEditComponent, } from './organizations/manage/collection-add-edit.component'; @@ -351,6 +353,8 @@ registerLocaleData(localeZhTw, 'zh-TW'); OrganizationSubscriptionComponent, OrgAttachmentsComponent, OrgBulkStatusComponent, + OrgBulkConfirmComponent, + OrgBulkRemoveComponent, OrgCiphersComponent, OrgCollectionAddEditComponent, OrgCollectionsComponent, @@ -452,6 +456,8 @@ registerLocaleData(localeZhTw, 'zh-TW'); OrgAddEditComponent, OrgAttachmentsComponent, OrgBulkStatusComponent, + OrgBulkConfirmComponent, + OrgBulkRemoveComponent, OrgCollectionAddEditComponent, OrgCollectionsComponent, OrgEntityEventsComponent, diff --git a/src/app/organizations/manage/bulk/bulk-confirm.component.html b/src/app/organizations/manage/bulk/bulk-confirm.component.html new file mode 100644 index 0000000000..17ecc5a587 --- /dev/null +++ b/src/app/organizations/manage/bulk/bulk-confirm.component.html @@ -0,0 +1,100 @@ + diff --git a/src/app/organizations/manage/bulk/bulk-confirm.component.ts b/src/app/organizations/manage/bulk/bulk-confirm.component.ts new file mode 100644 index 0000000000..d2ac452278 --- /dev/null +++ b/src/app/organizations/manage/bulk/bulk-confirm.component.ts @@ -0,0 +1,95 @@ +import { + Component, + Input, + OnInit, +} from '@angular/core'; + +import { ApiService } from 'jslib-common/abstractions/api.service'; +import { CryptoService } from 'jslib-common/abstractions/crypto.service'; +import { I18nService } from 'jslib-common/abstractions/i18n.service'; + +import { OrganizationUserBulkConfirmRequest } from 'jslib-common/models/request/organizationUserBulkConfirmRequest'; +import { OrganizationUserBulkRequest } from 'jslib-common/models/request/organizationUserBulkRequest'; + +import { OrganizationUserUserDetailsResponse } from 'jslib-common/models/response/organizationUserResponse'; + +import { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType'; + +import { Utils } from 'jslib-common/misc/utils'; + +@Component({ + selector: 'app-bulk-confirm', + templateUrl: 'bulk-confirm.component.html', +}) +export class BulkConfirmComponent implements OnInit { + + @Input() organizationId: string; + @Input() users: OrganizationUserUserDetailsResponse[]; + + excludedUsers: OrganizationUserUserDetailsResponse[]; + filteredUsers: OrganizationUserUserDetailsResponse[]; + publicKeys: Map = new Map(); + fingerprints: Map = new Map(); + statuses: Map = new Map(); + + loading: boolean = true; + done: boolean = false; + error: string; + + constructor(private cryptoService: CryptoService, private apiService: ApiService, + private i18nService: I18nService) { } + + async ngOnInit() { + this.excludedUsers = this.users.filter(user => user.status !== OrganizationUserStatusType.Accepted); + this.filteredUsers = this.users.filter(user => user.status === OrganizationUserStatusType.Accepted); + + if (this.filteredUsers.length <= 0) { + this.done = true; + } + + const request = new OrganizationUserBulkRequest(this.filteredUsers.map(user => user.id)); + const response = await this.apiService.postOrganizationUsersPublicKey(this.organizationId, request); + + for (const entry of response.data) { + const publicKey = Utils.fromB64ToArray(entry.key); + const fingerprint = await this.cryptoService.getFingerprint(entry.id, 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 orgKey = await this.cryptoService.getOrgKey(this.organizationId); + const userIdsWithKeys: any[] = []; + for (const user of this.filteredUsers) { + const publicKey = this.publicKeys.get(user.id); + if (publicKey == null) { + continue; + } + const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer); + userIdsWithKeys.push({ + id: user.id, + key: key.encryptedString, + }); + } + const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys); + const response = await this.apiService.postOrganizationUserBulkConfirm(this.organizationId, request); + + 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; + } +} diff --git a/src/app/organizations/manage/bulk/bulk-remove.component.html b/src/app/organizations/manage/bulk/bulk-remove.component.html new file mode 100644 index 0000000000..14e803c585 --- /dev/null +++ b/src/app/organizations/manage/bulk/bulk-remove.component.html @@ -0,0 +1,81 @@ + diff --git a/src/app/organizations/manage/bulk/bulk-remove.component.ts b/src/app/organizations/manage/bulk/bulk-remove.component.ts new file mode 100644 index 0000000000..8b59aee638 --- /dev/null +++ b/src/app/organizations/manage/bulk/bulk-remove.component.ts @@ -0,0 +1,46 @@ +import { + Component, + Input, +} from '@angular/core'; + +import { ApiService } from 'jslib-common/abstractions/api.service'; +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { OrganizationUserBulkRequest } from 'jslib-common/models/request/organizationUserBulkRequest'; + +import { OrganizationUserUserDetailsResponse } from 'jslib-common/models/response/organizationUserResponse'; + +@Component({ + selector: 'app-bulk-remove', + templateUrl: 'bulk-remove.component.html', +}) +export class BulkRemoveComponent { + + @Input() organizationId: string; + @Input() users: OrganizationUserUserDetailsResponse[]; + + statuses: Map = new Map(); + + loading: boolean = false; + done: boolean = false; + error: string; + + constructor(private apiService: ApiService, private i18nService: I18nService) { } + + async submit() { + this.loading = true; + try { + const request = new OrganizationUserBulkRequest(this.users.map(user => user.id)); + const response = await this.apiService.deleteManyOrganizationUsers(this.organizationId, request); + + 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; + } +} diff --git a/src/app/organizations/manage/bulk-status.component.html b/src/app/organizations/manage/bulk/bulk-status.component.html similarity index 100% rename from src/app/organizations/manage/bulk-status.component.html rename to src/app/organizations/manage/bulk/bulk-status.component.html diff --git a/src/app/organizations/manage/bulk-status.component.ts b/src/app/organizations/manage/bulk/bulk-status.component.ts similarity index 100% rename from src/app/organizations/manage/bulk-status.component.ts rename to src/app/organizations/manage/bulk/bulk-status.component.ts diff --git a/src/app/organizations/manage/people.component.html b/src/app/organizations/manage/people.component.html index 7bcdad67c1..7adb790871 100644 --- a/src/app/organizations/manage/people.component.html +++ b/src/app/organizations/manage/people.component.html @@ -36,7 +36,7 @@ {{'reinviteSelected' | i18n}} @@ -158,3 +158,5 @@ + + diff --git a/src/app/organizations/manage/people.component.ts b/src/app/organizations/manage/people.component.ts index 6edf91348e..755d4ef3f6 100644 --- a/src/app/organizations/manage/people.component.ts +++ b/src/app/organizations/manage/people.component.ts @@ -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(); - 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 { - 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(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, 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>, 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, 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 { - return new Promise(async (resolve, reject) => { - const autoConfirm = await this.storageService.get(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, 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); - }); - }); - } } diff --git a/src/app/organizations/manage/user-confirm.component.ts b/src/app/organizations/manage/user-confirm.component.ts index ae0ada435f..f55f8aaeea 100644 --- a/src/app/organizations/manage/user-confirm.component.ts +++ b/src/app/organizations/manage/user-confirm.component.ts @@ -8,9 +8,11 @@ import { import { ConstantsService } from 'jslib-common/services/constants.service'; +import { ApiService } from 'jslib-common/abstractions/api.service'; import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { StorageService } from 'jslib-common/abstractions/storage.service'; +import { Utils } from 'jslib-common/misc/utils'; @Component({ selector: 'app-user-confirm', @@ -21,18 +23,22 @@ export class UserConfirmComponent implements OnInit { @Input() userId: string; @Input() organizationUserId: string; @Input() organizationId: string; - @Input() publicKey: Uint8Array; @Output() onConfirmedUser = new EventEmitter(); dontAskAgain = false; loading = true; fingerprint: string; - constructor(private cryptoService: CryptoService, private storageService: StorageService) { } + private publicKey: Uint8Array = null; + + constructor(private apiService: ApiService, private cryptoService: CryptoService, + private storageService: StorageService) { } async ngOnInit() { try { - if (this.publicKey != null) { + const publicKeyResponse = await this.apiService.getUserPublicKey(this.userId); + if (publicKeyResponse != null) { + this.publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); const fingerprint = await this.cryptoService.getFingerprint(this.userId, this.publicKey.buffer); if (fingerprint != null) { this.fingerprint = fingerprint.join('-'); @@ -51,6 +57,6 @@ export class UserConfirmComponent implements OnInit { await this.storageService.save(ConstantsService.autoConfirmFingerprints, true); } - this.onConfirmedUser.emit(); + this.onConfirmedUser.emit(this.publicKey); } } diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 57147e0ae4..149a47cba4 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -3966,8 +3966,8 @@ "noSelectedUsersApplicable": { "message": "This action is not applicable to any of the selected users." }, - "removeSelectedUsersConfirmation": { - "message": "Are you sure you want to remove the selected users?" + "removeUsersWarning": { + "message": "Are you sure you want to remove the following users? The process may take a few seconds to complete and cannot be interrupted or canceled." }, "confirmSelected": { "message": "Confirm Selected" @@ -3986,5 +3986,14 @@ }, "bulkFilteredMessage": { "message": "Excluded, not applicable for this action." + }, + "fingerprint": { + "message": "Fingerprint" + }, + "removeUsers": { + "message": "Remove Users" + }, + "error": { + "message": "Error" } }