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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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