1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +00:00

[PM-1071] Display import-details-dialog on successful import (#4817)

* Prefer callback over error-flow to prompt for password

Remove error-flow to request file password
Prefer callback, which has to be provided when retrieving/creating an instance.
Delete ImportError
Call BitwardenPasswordProtector for all Bitwarden json imports, as it extends BitwardenJsonImporter
Throw errors instead of returning
Return ImportResult
Fix and extend tests import.service
Replace "@fluffy-spoon/substitute" with "jest-mock-extended"

* Fix up test cases

Delete bitwarden-json-importer.spec.ts
Add test case to ensure bitwarden-json-importer.ts is called given unencrypted or account-protected files

* Move file-password-prompt into dialog-folder

* Add import success dialog

* Fix typo

* Only list the type when at least one got imported

* update copy based on design feedback

* Remove unnecessary /index import

* Remove promptForPassword_callback from interface

PR feedback from @MGibson1 that giving every importer the ability to request a password is unnecessary. Instead, we can pass the callback into the constructor for every importer that needs this functionality

* Remove unneeded import of BitwardenJsonImporter

* Fix spec constructor

* Fixed organizational import

Added an else statement, or else we'd import into an org and then also import into an individual vault
This commit is contained in:
Daniel James Smith
2023-04-06 22:41:09 +02:00
committed by GitHub
parent 19626a7837
commit cf2d8b266a
24 changed files with 397 additions and 287 deletions

View File

@@ -2,7 +2,7 @@ import * as program from "commander";
import * as inquirer from "inquirer";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { ImportServiceAbstraction, Importer, ImportType } from "@bitwarden/importer";
import { ImportServiceAbstraction, ImportType } from "@bitwarden/importer";
import { Response } from "../models/response";
import { MessageResponse } from "../models/response/message.response";
@@ -50,8 +50,14 @@ export class ImportCommand {
if (filepath == null || filepath === "") {
return Response.badRequest("`filepath` was not provided.");
}
const importer = await this.importService.getImporter(format, organizationId);
const promptForPassword_callback = async () => {
return await this.promptPassword();
};
const importer = await this.importService.getImporter(
format,
promptForPassword_callback,
organizationId
);
if (importer === null) {
return Response.badRequest("Proper importer type required.");
}
@@ -68,12 +74,14 @@ export class ImportCommand {
return Response.badRequest("Import file was empty.");
}
const response = await this.doImport(importer, contents, organizationId);
const response = await this.importService.import(importer, contents, organizationId);
if (response.success) {
response.data = new MessageResponse("Imported " + filepath, null);
return Response.success(new MessageResponse("Imported " + filepath, null));
}
return response;
} catch (err) {
if (err.message) {
return Response.badRequest(err.message);
}
return Response.badRequest(err);
}
}
@@ -91,27 +99,6 @@ export class ImportCommand {
return Response.success(res);
}
private async doImport(
importer: Importer,
contents: string,
organizationId?: string
): Promise<Response> {
const err = await this.importService.import(importer, contents, organizationId);
if (err != null) {
if (err.passwordRequired) {
importer = this.importService.getImporter(
"bitwardenpasswordprotected",
organizationId,
await this.promptPassword()
);
return this.doImport(importer, contents, organizationId);
}
return Response.badRequest(err.message);
}
return Response.success();
}
private async promptPassword() {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,

View File

@@ -8,6 +8,7 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components";
import { ImportServiceAbstraction } from "@bitwarden/importer";
import { ImportComponent } from "../../../../tools/import-export/import.component";
@@ -30,7 +31,8 @@ export class OrganizationImportComponent extends ImportComponent {
private organizationService: OrganizationService,
logService: LogService,
modalService: ModalService,
syncService: SyncService
syncService: SyncService,
dialogService: DialogService
) {
super(
i18nService,
@@ -40,7 +42,8 @@ export class OrganizationImportComponent extends ImportComponent {
policyService,
logService,
modalService,
syncService
syncService,
dialogService
);
}

View File

@@ -0,0 +1,32 @@
<bit-dialog dialogSize="small">
<span bitDialogTitle>
{{ "importSuccess" | i18n }}
</span>
<div bitDialogContent>
<span>{{ "importSuccessNumberOfItems" | i18n : this.data.ciphers.length }}</span>
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th bitCell>{{ "type" | i18n }}</th>
<th bitCell>{{ "total" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async">
<td bitCell>
<i class="bwi bwi-fw bwi-{{ r.icon }}" aria-hidden="true"></i>
{{ r.type | i18n }}
</td>
<td bitCell>{{ r.count }}</td>
</tr>
</ng-template>
</bit-table>
</div>
<div bitDialogFooter>
<button bitButton bitDialogClose buttonType="primary" type="button">
{{ "ok" | i18n }}
</button>
</div>
</bit-dialog>

View File

@@ -0,0 +1,78 @@
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { TableDataSource } from "@bitwarden/components";
import { ImportResult } from "@bitwarden/importer";
export interface ResultList {
icon: string;
type: string;
count: number;
}
@Component({
selector: "app-import-success-dialog",
templateUrl: "./import-success-dialog.component.html",
})
export class ImportSuccessDialogComponent implements OnInit {
protected dataSource = new TableDataSource<ResultList>();
constructor(public dialogRef: DialogRef, @Inject(DIALOG_DATA) public data: ImportResult) {}
ngOnInit(): void {
if (this.data != null) {
this.dataSource.data = this.buildResultList();
}
}
private buildResultList(): ResultList[] {
let logins = 0;
let cards = 0;
let identities = 0;
let secureNotes = 0;
this.data.ciphers.map((c) => {
switch (c.type) {
case CipherType.Login:
logins++;
break;
case CipherType.Card:
cards++;
break;
case CipherType.SecureNote:
secureNotes++;
break;
case CipherType.Identity:
identities++;
break;
default:
break;
}
});
const list: ResultList[] = [];
if (logins > 0) {
list.push({ icon: "globe", type: "typeLogin", count: logins });
}
if (cards > 0) {
list.push({ icon: "credit-card", type: "typeCard", count: cards });
}
if (identities > 0) {
list.push({ icon: "id-card", type: "typeIdentity", count: identities });
}
if (secureNotes > 0) {
list.push({ icon: "sticky-note", type: "typeSecureNote", count: secureNotes });
}
if (this.data.folders.length > 0) {
list.push({ icon: "folder", type: "folders", count: this.data.folders.length });
}
if (this.data.collections.length > 0) {
list.push({
icon: "collection",
type: "collections",
count: this.data.collections.length,
});
}
return list;
}
}

View File

@@ -0,0 +1,2 @@
export * from "./import-success-dialog.component";
export * from "./file-password-prompt.component";

View File

@@ -15,14 +15,19 @@ import {
import { LooseComponentsModule, SharedModule } from "../../shared";
import { ImportSuccessDialogComponent, FilePasswordPromptComponent } from "./dialog";
import { ExportComponent } from "./export.component";
import { FilePasswordPromptComponent } from "./file-password-prompt.component";
import { ImportExportRoutingModule } from "./import-export-routing.module";
import { ImportComponent } from "./import.component";
@NgModule({
imports: [SharedModule, LooseComponentsModule, ImportExportRoutingModule],
declarations: [ImportComponent, ExportComponent, FilePasswordPromptComponent],
declarations: [
ImportComponent,
ExportComponent,
FilePasswordPromptComponent,
ImportSuccessDialogComponent,
],
providers: [
{
provide: ImportApiServiceAbstraction,

View File

@@ -11,14 +11,15 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components";
import {
ImportOption,
ImportType,
ImportError,
ImportResult,
ImportServiceAbstraction,
} from "@bitwarden/importer";
import { FilePasswordPromptComponent } from "./file-password-prompt.component";
import { ImportSuccessDialogComponent, FilePasswordPromptComponent } from "./dialog";
@Component({
selector: "app-import",
@@ -30,7 +31,6 @@ export class ImportComponent implements OnInit {
format: ImportType = null;
fileContents: string;
fileSelected: File;
formPromise: Promise<ImportError>;
loading = false;
importBlockedByPolicy = false;
@@ -45,7 +45,8 @@ export class ImportComponent implements OnInit {
protected policyService: PolicyService,
private logService: LogService,
protected modalService: ModalService,
protected syncService: SyncService
protected syncService: SyncService,
protected dialogService: DialogService
) {}
async ngOnInit() {
@@ -68,7 +69,15 @@ export class ImportComponent implements OnInit {
this.loading = true;
const importer = this.importService.getImporter(this.format, this.organizationId);
const promptForPassword_callback = async () => {
return await this.getFilePassword();
};
const importer = this.importService.getImporter(
this.format,
promptForPassword_callback,
this.organizationId
);
if (importer === null) {
this.platformUtilsService.showToast(
"error",
@@ -117,30 +126,17 @@ export class ImportComponent implements OnInit {
}
try {
this.formPromise = this.importService.import(importer, fileContents, this.organizationId);
let error = await this.formPromise;
if (error?.passwordRequired) {
const filePassword = await this.getFilePassword();
if (filePassword == null) {
this.loading = false;
return;
}
error = await this.doPasswordProtectedImport(filePassword, fileContents);
}
if (error != null) {
this.error(error);
this.loading = false;
return;
}
const result = await this.importService.import(importer, fileContents, this.organizationId);
//No errors, display success message
this.platformUtilsService.showToast("success", null, this.i18nService.t("importSuccess"));
this.dialogService.open<unknown, ImportResult>(ImportSuccessDialogComponent, {
data: result,
});
this.syncService.fullSync(true);
this.router.navigate(this.successNavigate);
} catch (e) {
this.error(e);
this.logService.error(e);
}
@@ -268,17 +264,4 @@ export class ImportComponent implements OnInit {
return await ref.onClosedPromise();
}
async doPasswordProtectedImport(
filePassword: string,
fileContents: string
): Promise<ImportError> {
const passwordProtectedImporter = this.importService.getImporter(
"bitwardenpasswordprotected",
this.organizationId,
filePassword
);
return this.importService.import(passwordProtectedImporter, fileContents, this.organizationId);
}
}

View File

@@ -1256,6 +1256,15 @@
"importSuccess": {
"message": "Data successfully imported"
},
"importSuccessNumberOfItems": {
"message": "A total of $AMOUNT$ items were imported.",
"placeholders": {
"amount": {
"content": "$1",
"example": "2"
}
}
},
"dataExportSuccess": {
"message": "Data successfully exported"
},