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:
committed by
GitHub
parent
19626a7837
commit
cf2d8b266a
@@ -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,
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
2
apps/web/src/app/tools/import-export/dialog/index.ts
Normal file
2
apps/web/src/app/tools/import-export/dialog/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./import-success-dialog.component";
|
||||
export * from "./file-password-prompt.component";
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user