mirror of https://github.com/bitwarden/web.git
Browse Source
* 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>pull/1022/head
11 changed files with 436 additions and 135 deletions
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue