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:
parent
b20206d350
commit
fd328eef2a
|
@ -38,7 +38,9 @@ import { TwoFactorComponent } from './accounts/two-factor.component';
|
||||||
import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component';
|
import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component';
|
||||||
import { VerifyRecoverDeleteComponent } from './accounts/verify-recover-delete.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 {
|
import {
|
||||||
CollectionAddEditComponent as OrgCollectionAddEditComponent,
|
CollectionAddEditComponent as OrgCollectionAddEditComponent,
|
||||||
} from './organizations/manage/collection-add-edit.component';
|
} from './organizations/manage/collection-add-edit.component';
|
||||||
|
@ -351,6 +353,8 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||||
OrganizationSubscriptionComponent,
|
OrganizationSubscriptionComponent,
|
||||||
OrgAttachmentsComponent,
|
OrgAttachmentsComponent,
|
||||||
OrgBulkStatusComponent,
|
OrgBulkStatusComponent,
|
||||||
|
OrgBulkConfirmComponent,
|
||||||
|
OrgBulkRemoveComponent,
|
||||||
OrgCiphersComponent,
|
OrgCiphersComponent,
|
||||||
OrgCollectionAddEditComponent,
|
OrgCollectionAddEditComponent,
|
||||||
OrgCollectionsComponent,
|
OrgCollectionsComponent,
|
||||||
|
@ -452,6 +456,8 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||||
OrgAddEditComponent,
|
OrgAddEditComponent,
|
||||||
OrgAttachmentsComponent,
|
OrgAttachmentsComponent,
|
||||||
OrgBulkStatusComponent,
|
OrgBulkStatusComponent,
|
||||||
|
OrgBulkConfirmComponent,
|
||||||
|
OrgBulkRemoveComponent,
|
||||||
OrgCollectionAddEditComponent,
|
OrgCollectionAddEditComponent,
|
||||||
OrgCollectionsComponent,
|
OrgCollectionsComponent,
|
||||||
OrgEntityEventsComponent,
|
OrgEntityEventsComponent,
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
<div class="modal fade" tabindex="-1" 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">
|
||||||
|
<h2 class="modal-title" id="bulkTitle">
|
||||||
|
{{'confirmUsers' | i18n}}
|
||||||
|
</h2>
|
||||||
|
<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="fa fa-spinner fa-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://help.bitwarden.com/article/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">
|
||||||
|
<app-avatar [data]="user.name || user.email" [email]="user.email" size="25" [circle]="true"
|
||||||
|
[fontSize]="14"></app-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">
|
||||||
|
<app-avatar [data]="user.name || user.email" [email]="user.email" size="25" [circle]="true"
|
||||||
|
[fontSize]="14"></app-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">
|
||||||
|
<app-avatar [data]="user.name || user.email" [email]="user.email" size="25" [circle]="true"
|
||||||
|
[fontSize]="14"></app-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="fa fa-spinner fa-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,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<string, Uint8Array> = new Map();
|
||||||
|
fingerprints: Map<string, string> = new Map();
|
||||||
|
statuses: Map<string, string> = 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
<div class="modal fade" tabindex="-1" 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">
|
||||||
|
<h2 class="modal-title" id="bulkTitle">
|
||||||
|
{{'removeUsers' | i18n}}
|
||||||
|
</h2>
|
||||||
|
<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="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
|
{{'loading' | i18n}}
|
||||||
|
</div>
|
||||||
|
<app-callout type="danger" *ngIf="users.length <= 0">
|
||||||
|
{{'noSelectedUsersApplicable' | i18n}}
|
||||||
|
</app-callout>
|
||||||
|
<app-callout type="error" *ngIf="error">
|
||||||
|
{{error}}
|
||||||
|
</app-callout>
|
||||||
|
<ng-container *ngIf="!loading && !done">
|
||||||
|
<app-callout type="warning" *ngIf="users.length > 0 && !error">
|
||||||
|
{{'removeUsersWarning' | 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">
|
||||||
|
<app-avatar [data]="user.name || user.email" [email]="user.email" size="25" [circle]="true"
|
||||||
|
[fontSize]="14"></app-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="!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 users">
|
||||||
|
<td width="30">
|
||||||
|
<app-avatar [data]="user.name || user.email" [email]="user.email" size="25" [circle]="true"
|
||||||
|
[fontSize]="14"></app-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="fa fa-spinner fa-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,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<string, string> = 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,7 +36,7 @@
|
||||||
{{'reinviteSelected' | i18n}}
|
{{'reinviteSelected' | i18n}}
|
||||||
</button>
|
</button>
|
||||||
<button class="dropdown-item text-success" appStopClick (click)="bulkConfirm()"
|
<button class="dropdown-item text-success" appStopClick (click)="bulkConfirm()"
|
||||||
*ngIf="showConfirmUsers">
|
*ngIf="showBulkConfirmUsers">
|
||||||
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
|
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
|
||||||
{{'confirmSelected' | i18n}}
|
{{'confirmSelected' | i18n}}
|
||||||
</button>
|
</button>
|
||||||
|
@ -158,3 +158,5 @@
|
||||||
<ng-template #confirmTemplate></ng-template>
|
<ng-template #confirmTemplate></ng-template>
|
||||||
<ng-template #resetPasswordTemplate></ng-template>
|
<ng-template #resetPasswordTemplate></ng-template>
|
||||||
<ng-template #bulkStatusTemplate></ng-template>
|
<ng-template #bulkStatusTemplate></ng-template>
|
||||||
|
<ng-template #bulkConfirmTemplate></ng-template>
|
||||||
|
<ng-template #bulkRemoveTemplate></ng-template>
|
||||||
|
|
|
@ -26,7 +26,6 @@ import { StorageService } from 'jslib-common/abstractions/storage.service';
|
||||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||||
|
|
||||||
import { OrganizationKeysRequest } from 'jslib-common/models/request/organizationKeysRequest';
|
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 { OrganizationUserBulkRequest } from 'jslib-common/models/request/organizationUserBulkRequest';
|
||||||
import { OrganizationUserConfirmRequest } from 'jslib-common/models/request/organizationUserConfirmRequest';
|
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 { Utils } from 'jslib-common/misc/utils';
|
||||||
|
|
||||||
import { ModalComponent } from '../../modal.component';
|
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 { EntityEventsComponent } from './entity-events.component';
|
||||||
import { ResetPasswordComponent } from './reset-password.component';
|
import { ResetPasswordComponent } from './reset-password.component';
|
||||||
import { UserAddEditComponent } from './user-add-edit.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('confirmTemplate', { read: ViewContainerRef, static: true }) confirmModalRef: ViewContainerRef;
|
||||||
@ViewChild('resetPasswordTemplate', { read: ViewContainerRef, static: true }) resetPasswordModalRef: ViewContainerRef;
|
@ViewChild('resetPasswordTemplate', { read: ViewContainerRef, static: true }) resetPasswordModalRef: ViewContainerRef;
|
||||||
@ViewChild('bulkStatusTemplate', { read: ViewContainerRef, static: true }) bulkStatusModalRef: 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;
|
loading = true;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
|
@ -237,6 +240,10 @@ export class PeopleComponent implements OnInit {
|
||||||
this.confirmedCount > 0 && this.confirmedCount < 3 && this.acceptedCount > 0;
|
this.confirmedCount > 0 && this.confirmedCount < 3 && this.acceptedCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get showBulkConfirmUsers(): boolean {
|
||||||
|
return this.acceptedCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
edit(user: OrganizationUserUserDetailsResponse) {
|
edit(user: OrganizationUserUserDetailsResponse) {
|
||||||
if (this.modal != null) {
|
if (this.modal != null) {
|
||||||
this.modal.close();
|
this.modal.close();
|
||||||
|
@ -329,30 +336,21 @@ export class PeopleComponent implements OnInit {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = this.getCheckedUsers();
|
if (this.modal != null) {
|
||||||
if (users.length <= 0) {
|
this.modal.close();
|
||||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
|
||||||
this.i18nService.t('noSelectedUsersApplicable'));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmed = await this.platformUtilsService.showDialog(
|
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||||
this.i18nService.t('removeSelectedUsersConfirmation'), this.i18nService.t('remove'),
|
this.modal = this.bulkRemoveModalRef.createComponent(factory).instance;
|
||||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
const childComponent = this.modal.show(BulkRemoveComponent, this.bulkRemoveModalRef);
|
||||||
if (!confirmed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
childComponent.organizationId = this.organizationId;
|
||||||
const request = new OrganizationUserBulkRequest(users.map(user => user.id));
|
childComponent.users = this.getCheckedUsers();
|
||||||
const response = this.apiService.deleteManyOrganizationUsers(this.organizationId, request);
|
|
||||||
this.showBulkStatus(users, users, response, this.i18nService.t('bulkRemovedMessage'));
|
this.modal.onClosed.subscribe(async () => {
|
||||||
await response;
|
|
||||||
await this.load();
|
await this.load();
|
||||||
} catch (e) {
|
this.modal = null;
|
||||||
this.validationService.showError(e);
|
});
|
||||||
}
|
|
||||||
this.actionPromise = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkReinvite() {
|
async bulkReinvite() {
|
||||||
|
@ -385,83 +383,38 @@ export class PeopleComponent implements OnInit {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = this.getCheckedUsers();
|
if (this.modal != null) {
|
||||||
const filteredUsers = users.filter(u => u.status === OrganizationUserStatusType.Accepted);
|
this.modal.close();
|
||||||
|
|
||||||
if (filteredUsers.length <= 0) {
|
|
||||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
|
||||||
this.i18nService.t('noSelectedUsersApplicable'));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicKeyRequest = new OrganizationUserBulkRequest(filteredUsers.map(user => user.id));
|
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||||
const publicKeyResponse = await this.apiService.postOrganizationUsersPublicKey(this.organizationId, publicKeyRequest);
|
this.modal = this.bulkConfirmModalRef.createComponent(factory).instance;
|
||||||
|
const childComponent = this.modal.show(BulkConfirmComponent, this.bulkConfirmModalRef);
|
||||||
|
|
||||||
const keyMap = new Map<string, Uint8Array>();
|
childComponent.organizationId = this.organizationId;
|
||||||
publicKeyResponse.data.forEach(entry => {
|
childComponent.users = this.getCheckedUsers();
|
||||||
keyMap.set(entry.id, Utils.fromB64ToArray(entry.key));
|
|
||||||
});
|
|
||||||
|
|
||||||
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
|
this.modal.onClosed.subscribe(async () => {
|
||||||
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;
|
|
||||||
await this.load();
|
await this.load();
|
||||||
} catch (e) {
|
this.modal = null;
|
||||||
this.validationService.showError(e);
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirm(user: OrganizationUserUserDetailsResponse): Promise<void> {
|
const confirmUser = async (publicKey: Uint8Array) => {
|
||||||
if (this.actionPromise != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 { }
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.actionPromise = this.doConfirmation(user, publicKey);
|
this.actionPromise = this.doConfirmation(user, publicKey);
|
||||||
await this.actionPromise;
|
await this.actionPromise;
|
||||||
this.confirmUser(user);
|
updateUser(this);
|
||||||
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', user.name || user.email));
|
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', user.name || user.email));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.validationService.showError(e);
|
this.validationService.showError(e);
|
||||||
|
@ -469,6 +422,52 @@ export class PeopleComponent implements OnInit {
|
||||||
} finally {
|
} finally {
|
||||||
this.actionPromise = null;
|
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) {
|
} catch (e) {
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
console.error('Handled exception:', e);
|
console.error('Handled exception:', e);
|
||||||
|
@ -558,9 +557,9 @@ export class PeopleComponent implements OnInit {
|
||||||
request: Promise<ListResponse<OrganizationUserBulkResponse>>, successfullMessage: string) {
|
request: Promise<ListResponse<OrganizationUserBulkResponse>>, successfullMessage: string) {
|
||||||
|
|
||||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
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>(
|
const childComponent = this.modal.show<BulkStatusComponent>(
|
||||||
BulkStatusComponent, this.eventsModalRef);
|
BulkStatusComponent, this.bulkStatusModalRef);
|
||||||
|
|
||||||
childComponent.loading = true;
|
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() {
|
private getCheckedUsers() {
|
||||||
return this.users.filter(u => (u as any).checked);
|
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,11 @@ import {
|
||||||
|
|
||||||
import { ConstantsService } from 'jslib-common/services/constants.service';
|
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 { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
||||||
import { StorageService } from 'jslib-common/abstractions/storage.service';
|
import { StorageService } from 'jslib-common/abstractions/storage.service';
|
||||||
|
|
||||||
|
import { Utils } from 'jslib-common/misc/utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user-confirm',
|
selector: 'app-user-confirm',
|
||||||
|
@ -21,18 +23,22 @@ export class UserConfirmComponent implements OnInit {
|
||||||
@Input() userId: string;
|
@Input() userId: string;
|
||||||
@Input() organizationUserId: string;
|
@Input() organizationUserId: string;
|
||||||
@Input() organizationId: string;
|
@Input() organizationId: string;
|
||||||
@Input() publicKey: Uint8Array;
|
|
||||||
@Output() onConfirmedUser = new EventEmitter();
|
@Output() onConfirmedUser = new EventEmitter();
|
||||||
|
|
||||||
dontAskAgain = false;
|
dontAskAgain = false;
|
||||||
loading = true;
|
loading = true;
|
||||||
fingerprint: string;
|
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() {
|
async ngOnInit() {
|
||||||
try {
|
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);
|
const fingerprint = await this.cryptoService.getFingerprint(this.userId, this.publicKey.buffer);
|
||||||
if (fingerprint != null) {
|
if (fingerprint != null) {
|
||||||
this.fingerprint = fingerprint.join('-');
|
this.fingerprint = fingerprint.join('-');
|
||||||
|
@ -51,6 +57,6 @@ export class UserConfirmComponent implements OnInit {
|
||||||
await this.storageService.save(ConstantsService.autoConfirmFingerprints, true);
|
await this.storageService.save(ConstantsService.autoConfirmFingerprints, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onConfirmedUser.emit();
|
this.onConfirmedUser.emit(this.publicKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3966,8 +3966,8 @@
|
||||||
"noSelectedUsersApplicable": {
|
"noSelectedUsersApplicable": {
|
||||||
"message": "This action is not applicable to any of the selected users."
|
"message": "This action is not applicable to any of the selected users."
|
||||||
},
|
},
|
||||||
"removeSelectedUsersConfirmation": {
|
"removeUsersWarning": {
|
||||||
"message": "Are you sure you want to remove the selected users?"
|
"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": {
|
"confirmSelected": {
|
||||||
"message": "Confirm Selected"
|
"message": "Confirm Selected"
|
||||||
|
@ -3986,5 +3986,14 @@
|
||||||
},
|
},
|
||||||
"bulkFilteredMessage": {
|
"bulkFilteredMessage": {
|
||||||
"message": "Excluded, not applicable for this action."
|
"message": "Excluded, not applicable for this action."
|
||||||
|
},
|
||||||
|
"fingerprint": {
|
||||||
|
"message": "Fingerprint"
|
||||||
|
},
|
||||||
|
"removeUsers": {
|
||||||
|
"message": "Remove Users"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"message": "Error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue