1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 00:03:56 +00:00

[PM-4222] Make importer UI reusable (#6504)

* Split up import/export into separate modules

* Fix routing and apply PR feedback

* Renamed OrganizationExport exports to OrganizationVaultExport

* Make import dialogs standalone and move them to libs/importer

* Make import.component re-usable

- Move functionality which was previously present on the org-import.component into import.component
- Move import.component into libs/importer
Make import.component standalone
Create import-web.component to represent Web UI
Fix module imports and routing
Remove unused org-import-files

* Renamed filenames according to export rename

* Make ImportWebComponent standalone, simplify routing

* Pass organizationId as Input to ImportComponent

* use formLoading and formDisabled outputs

* Emit an event when the import succeeds

Remove Angular router from base-component as other clients might not have routing (i.e. desktop)
Move logic that happened on web successful import into the import-web.component

* fix table themes on desktop & browser

* fix fileSelector button styles

* update selectors to use tools prefix; remove unused selectors

* Wall off UI components in libs/importer

Create barrel-file for libs/importer/components
Remove components and dialog exports from libs/importer/index.ts
Extend libs/shared/tsconfig.libs.json to include @bitwarden/importer/ui -> libs/importer/components
Extend apps/web/tsconfig.ts to include @bitwarden/importer/ui
Update all usages

* Rename @bitwarden/importer to @bitwarden/importer/core

Create more barrel files in libs/importer/*
Update imports within libs/importer
Extend tsconfig files
Update imports in web, desktop, browser and cli

* Lazy-load the ImportWebComponent via both routes

* Use SharedModule as import in import-web.component

* File selector should be displayed as secondary

* Use bitSubmit to override submit preventDefault (#6607)

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: William Martin <contact@willmartian.com>
This commit is contained in:
Daniel James Smith
2023-10-19 11:17:23 +02:00
committed by GitHub
parent d0e72f5554
commit 9e290a3fed
42 changed files with 299 additions and 328 deletions

View File

@@ -0,0 +1,32 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog>
<span bitDialogTitle>
{{ "confirmVaultImport" | i18n }}
</span>
<div bitDialogContent>
{{ "confirmVaultImportDesc" | i18n }}
<bit-form-field class="tw-mt-6">
<bit-label>{{ "confirmFilePassword" | i18n }}</bit-label>
<input
bitInput
type="password"
name="filePassword"
formControlName="filePassword"
appAutofocus
appInputVerbatim
/>
<button type="button" bitSuffix bitIconButton bitPasswordInputToggle></button>
</bit-form-field>
</div>
<ng-container bitDialogFooter>
<button bitButton buttonType="primary" type="submit">
<span>{{ "importData" | i18n }}</span>
</button>
<button bitButton bitDialogClose buttonType="secondary" type="button">
<span>{{ "cancel" | i18n }}</span>
</button>
</ng-container>
</bit-dialog>
</form>

View File

@@ -0,0 +1,43 @@
import { DialogRef } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
AsyncActionsModule,
ButtonModule,
DialogModule,
FormFieldModule,
IconButtonModule,
} from "@bitwarden/components";
@Component({
templateUrl: "file-password-prompt.component.html",
standalone: true,
imports: [
CommonModule,
JslibModule,
DialogModule,
FormFieldModule,
AsyncActionsModule,
ButtonModule,
IconButtonModule,
ReactiveFormsModule,
],
})
export class FilePasswordPromptComponent {
formGroup = this.formBuilder.group({
filePassword: ["", Validators.required],
});
constructor(public dialogRef: DialogRef, protected formBuilder: FormBuilder) {}
submit = () => {
this.formGroup.markAsTouched();
if (!this.formGroup.valid) {
return;
}
this.dialogRef.close(this.formGroup.value.filePassword);
};
}

View File

@@ -0,0 +1,29 @@
<bit-dialog>
<span bitDialogTitle>
{{ "importError" | i18n }}
</span>
<span bitDialogContent>
<div>{{ "resolveTheErrorsBelowAndTryAgain" | i18n }}</div>
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ "description" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async">
<td bitCell>{{ r.type }}</td>
<td bitCell>{{ r.message }}</td>
</tr>
</ng-template>
</bit-table>
</span>
<div bitDialogFooter>
<button bitButton bitDialogClose buttonType="primary" type="button">
{{ "ok" | i18n }}
</button>
</div>
</bit-dialog>

View File

@@ -0,0 +1,36 @@
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
import { Component, Inject, OnInit } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ButtonModule, DialogModule, TableDataSource, TableModule } from "@bitwarden/components";
export interface ErrorListItem {
type: string;
message: string;
}
@Component({
templateUrl: "./import-error-dialog.component.html",
standalone: true,
imports: [CommonModule, JslibModule, DialogModule, TableModule, ButtonModule],
})
export class ImportErrorDialogComponent implements OnInit {
protected dataSource = new TableDataSource<ErrorListItem>();
constructor(public dialogRef: DialogRef, @Inject(DIALOG_DATA) public data: Error) {}
ngOnInit(): void {
const split = this.data.message.split("\n\n");
if (split.length == 1) {
this.dataSource.data = [{ type: "", message: this.data.message }];
return;
}
const data: ErrorListItem[] = [];
split.forEach((line) => {
data.push({ type: "", message: line });
});
this.dataSource.data = data;
}
}

View File

@@ -0,0 +1,32 @@
<bit-dialog>
<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>
<ng-container bitDialogFooter>
<button bitButton bitDialogClose buttonType="primary" type="button">
{{ "ok" | i18n }}
</button>
</ng-container>
</bit-dialog>

View File

@@ -0,0 +1,82 @@
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
import { Component, Inject, OnInit } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { ButtonModule, DialogModule, TableDataSource, TableModule } from "@bitwarden/components";
import { ImportResult } from "../../models";
export interface ResultList {
icon: string;
type: string;
count: number;
}
@Component({
templateUrl: "./import-success-dialog.component.html",
standalone: true,
imports: [CommonModule, JslibModule, DialogModule, TableModule, ButtonModule],
})
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,3 @@
export * from "./import-error-dialog.component";
export * from "./import-success-dialog.component";
export * from "./file-password-prompt.component";

View File

@@ -0,0 +1,376 @@
<bit-callout type="info" *ngIf="importBlockedByPolicy">
{{ "personalOwnershipPolicyInEffectImports" | i18n }}
</bit-callout>
<form [formGroup]="formGroup" [bitSubmit]="submit" id="importForm">
<bit-form-field>
<bit-label
>{{ "importDestination" | i18n }}
<a
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnAboutImportOptions' | i18n }}"
href="https://bitwarden.com/help/import-data/"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</bit-label>
<bit-select formControlName="vaultSelector">
<bit-option
*ngIf="!importBlockedByPolicy"
[label]="'myVault' | i18n"
value="myVault"
icon="bwi-user"
/>
<bit-option
*ngFor="let o of organizations$ | async"
[value]="o.id"
[label]="o.name"
icon="bwi-business"
/>
</bit-select>
</bit-form-field>
<bit-form-field>
<bit-label>{{ organizationId ? ("collection" | i18n) : ("folder" | i18n) }}</bit-label>
<bit-select formControlName="targetSelector">
<ng-container *ngIf="!organizationId">
<bit-option [value]="null" label="-- {{ 'selectImportFolder' | i18n }} --" />
<bit-option
*ngFor="let f of folders$ | async"
[value]="f.id"
[label]="f.name"
icon="bwi-folder"
/>
</ng-container>
<ng-container *ngIf="organizationId">
<bit-option [value]="null" label="-- {{ 'selectImportCollection' | i18n }} --" />
<bit-option
*ngFor="let c of collections$ | async"
[value]="c.id"
[label]="c.name"
icon="bwi-collection"
/>
</ng-container>
</bit-select>
<bit-hint>{{
"importTargetHint"
| i18n
: (organizationId ? ("collection" | i18n | lowercase) : ("folder" | i18n | lowercase))
}}</bit-hint>
</bit-form-field>
<bit-form-field class="tw-w-1/2">
<bit-label>{{ "fileFormat" | i18n }}</bit-label>
<bit-select formControlName="format">
<bit-option *ngFor="let o of featuredImportOptions" [value]="o.id" [label]="o.name" />
<ng-container *ngIf="importOptions && importOptions.length">
<bit-option value="-" disabled />
<bit-option *ngFor="let o of importOptions" [value]="o.id" [label]="o.name" />
</ng-container>
</bit-select>
</bit-form-field>
<bit-callout type="info" title="{{ getFormatInstructionTitle() }}" *ngIf="format">
<ng-container *ngIf="format === 'bitwardencsv' || format === 'bitwardenjson'">
See detailed instructions on our help site at
<a target="_blank" rel="noopener" href="https://bitwarden.com/help/export-your-data/">
https://bitwarden.com/help/export-your-data/</a
>
</ng-container>
<ng-container *ngIf="format === 'lastpasscsv'">
See detailed instructions on our help site at
<a target="_blank" rel="noopener" href="https://bitwarden.com/help/import-from-lastpass/">
https://bitwarden.com/help/import-from-lastpass/</a
>
</ng-container>
<ng-container *ngIf="format === 'keepassxcsv'">
Using the KeePassX desktop application, navigate to "Database" &rarr; "Export to CSV file" and
save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'aviracsv'">
In the Avira web vault, go to "Settings" &rarr; "My Data" &rarr; "Export data" and save the
CSV file.
</ng-container>
<ng-container *ngIf="format === 'blurcsv'">
In the Blur web vault, click your username at the top and go to "Settings" &rarr; "Export
Data", then click "Export CSV" for your "Accounts".
</ng-container>
<ng-container *ngIf="format === 'safeincloudxml'">
Using the SaveInCloud desktop application, navigate to "File" &rarr; "Export" &rarr; "As XML"
and save the XML file.
</ng-container>
<ng-container *ngIf="format === 'padlockcsv'">
Using the Padlock desktop application, click the hamburger icon in the top left corner and
navigate to "Settings" &rarr; "Export" button and save the file "As CSV".
</ng-container>
<ng-container *ngIf="format === 'keepass2xml'">
Using the KeePass 2 desktop application, navigate to "File" &rarr; "Export" and select the
"KeePass XML (2.x)" option.
</ng-container>
<ng-container *ngIf="format === 'upmcsv'">
Using the Universal Password Manager desktop application, navigate to "Database" &rarr;
"Export" and save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'saferpasscsv'">
Using the SaferPass browser extension, click the hamburger icon in the top left corner and
navigate to "Settings". Click the "Export accounts" button to save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'meldiumcsv'">
Using the Meldium web vault, navigate to "Settings". Locate the "Export data" function and
click "Show me my data" to save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'keepercsv'">
Log into the Keeper web vault (keepersecurity.com/vault). Click on your "account email" (top
right) and select "Settings". Go to "Export" and find the "Export to .csv File" option. Click
"Export" to save the CSV file.
</ng-container>
<!--
<ng-container *ngIf="format === 'keeperjson'">
Log into the Keeper web vault (keepersecurity.com/vault). Click on your "account email" (top
right) and select "Settings". Go to "Export" and find the "Export to .json File" option. Click
"Export" to save the JSON file.
</ng-container>
-->
<ng-container
*ngIf="format === 'chromecsv' || format === 'operacsv' || format === 'vivaldicsv'"
>
<span *ngIf="format !== 'chromecsv'">
The process is exactly the same as importing from Google Chrome.
</span>
See detailed instructions on our help site at
<a target="_blank" rel="noopener" href="https://bitwarden.com/help/import-from-chrome/">
https://bitwarden.com/help/import-from-chrome/</a
>
</ng-container>
<ng-container *ngIf="format === 'firefoxcsv'">
See detailed instructions on our help site at
<a target="_blank" rel="noopener" href="https://bitwarden.com/help/import-from-firefox/">
https://bitwarden.com/help/import-from-firefox/</a
>.
</ng-container>
<ng-container *ngIf="format === 'safaricsv'">
See detailed instructions on our help site at
<a target="_blank" rel="noopener" href="https://bitwarden.com/help/import-from-safari/">
https://bitwarden.com/help/import-from-safari/</a
>.
</ng-container>
<ng-container
*ngIf="
format === '1password1pux' ||
format === '1password1pif' ||
format === '1passwordwincsv' ||
format === '1passwordmaccsv'
"
>
See detailed instructions on our help site at
<a target="_blank" rel="noopener" href="https://bitwarden.com/help/import-from-1password/">
https://bitwarden.com/help/import-from-1password/</a
>.
</ng-container>
<ng-container *ngIf="format === 'passworddragonxml'">
Using the Password Dragon desktop application, navigate to "File" &rarr; "Export" &rarr; "To
XML". In the dialog that pops up select "All Rows" and check all fields. Click the "Export"
button and save the XML file.
</ng-container>
<ng-container *ngIf="format === 'enpasscsv'">
Using the Enpass desktop application, navigate to "File" &rarr; "Export" &rarr; "As CSV".
Select "OK" to the warning alert and save the CSV file. Note that the importer only supports
files exported while Enpass is set to the English language, so adjust your settings
accordingly.
</ng-container>
<ng-container *ngIf="format === 'enpassjson'">
Using the Enpass 6 desktop application, click the menu button and navigate to "File" &rarr;
"Export". Select the ".json" file format option and save the JSON file.
</ng-container>
<ng-container *ngIf="format === 'pwsafexml'">
Using the Password Safe desktop application, navigate to "File" &rarr; "Export To" &rarr; "XML
format..." and save the XML file.
</ng-container>
<ng-container *ngIf="format === 'dashlanecsv'">
Log in to Dashlane, click on "My Account" &rarr; "Settings" &rarr; "Export file" and select
"Export as a CSV file". This will download a zip archive containing various CSV files. Unzip
the archive and import each CSV file individually.
</ng-container>
<ng-container *ngIf="format === 'dashlanejson'">
Dashlane no longer supports the JSON format. Only use this if you have an existing JSON for
import. Use the CSV importer when creating new exports.
</ng-container>
<ng-container *ngIf="format === 'msecurecsv'">
Using the mSecure desktop application, navigate to "File" &rarr; "Export" &rarr; "CSV File..."
and save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'stickypasswordxml'">
Using the Sticky Password desktop application, navigate to "Menu" (top right) &rarr; "Export"
&rarr; "Export all". Select the unencrypted format XML option and save the XML file.
</ng-container>
<ng-container *ngIf="format === 'truekeycsv'">
Using the True Key desktop application, click the gear icon (top right) and then navigate to
"App Settings". Click the "Export" button, enter your password and save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'clipperzhtml'">
Log into the Clipperz web application (clipperz.is/app). Click the hamburger menu icon in the
top right to expand the navigation bar. Navigate to "Data" &rarr; "Export". Click the
"download HTML+JSON" button to save the HTML file.
</ng-container>
<ng-container *ngIf="format === 'roboformcsv'">
Using the RoboForm Editor desktop application, navigate to "RoboForm" (top left) &rarr;
"Options" &rarr; "Account &amp; Data" and click the "Export" button. Select all of your data,
change the "Format" to "CSV file" and then click the "Export" button to save the CSV file.
Note: RoboForm only allows you to export Logins. Other items will not be exported.
</ng-container>
<ng-container *ngIf="format === 'passboltcsv'">
Log into the Passbolt web vault and navigate to the "Passwords" listing. Select all of the
passwords you would like to export and click the "Export" button at the top of the listing.
Choose the "csv (lastpass)" export format and click the "Export" button.
</ng-container>
<ng-container *ngIf="format === 'ascendocsv'">
Using the Ascendo DataVault desktop application, navigate to "Tools" &rarr; "Export". In the
dialog that pops up, select the "All Items (DVX, CSV)" option. Click the "Ok" button to save
the CSV file.
</ng-container>
<ng-container *ngIf="format === 'passwordbossjson'">
Using the Password Boss desktop application, navigate to "File" &rarr; "Export data" &rarr;
"Password Boss JSON - not encrypted" and save the JSON file.
</ng-container>
<ng-container *ngIf="format === 'zohovaultcsv'">
Log into the Zoho web vault (vault.zoho.com). Navigate to "Tools" &rarr; "Export Secrets".
Select "All Secrets" and click the "Zoho Vault Format CSV" button. Highlight and copy the data
from the textarea. Open a text editor like Notepad and paste the data. Save the data from the
text editor as
<code>zoho_export.csv</code>.
</ng-container>
<ng-container *ngIf="format === 'splashidcsv'">
Using the SplashID Safe desktop application, click on the SplashID blue lock logo in the top
right corner. Navigate to "Export" &rarr; "Export as CSV" and save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'passkeepcsv'">
Using the PassKeep mobile app, navigate to "Backup/Restore". Locate the "CSV Backup/Restore"
section and click "Backup to CSV" to save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'gnomejson'">
Make sure you have python-keyring and python-gnomekeyring installed. Save the
<a target="_blank" rel="noopener" href="https://bit.ly/2GpOMTg"
>GNOME Keyring Import/Export</a
>
python script to your desktop as <code>pw_helper.py</code>. Open terminal and run
<code>chmod +rx Desktop/pw_helper.py</code> and then
<code>python Desktop/pw_helper.py export Desktop/my_passwords.json</code>. Then upload the
resulting <code>my_passwords.json</code> file here to Bitwarden.
</ng-container>
<ng-container *ngIf="format === 'passwordagentcsv'">
Using the Password Agent desktop application navigate to "File" &rarr; "Export", select the
"Fields to export" button and check all of the fields, change the "Output format" to "CSV",
and then click the "Start" button to save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'passpackcsv'">
Log into the Passpack website vault and navigate to "Settings" &rarr; "Export", then click the
"Download" button to save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'passmanjson'">
Open your Passman vault and click on "Settings" in the bottom left corner. In the "Settings"
window switch to the "Export credentials" tab and choose "JSON" as the export type. Enter your
vault's passphrase and click the "Export" button to save the JSON file.
</ng-container>
<ng-container *ngIf="format === 'avastcsv'">
Open the Avast Passwords desktop application and navigate to "Settings" &rarr; "Import/export
data". Select the "Export" button for the "Export to CSV file" option to save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'avastjson'">
Open the Avast Passwords desktop application and navigate to "Settings" &rarr; "Import/export
data". Select the "Export" button for the "Export to JSON file" option to save the JSON file.
</ng-container>
<ng-container *ngIf="format === 'fsecurefsk'">
Open the F-Secure KEY desktop application and navigate to "Settings" &rarr; "Export
Passwords". Select the "Export" button, enter your master password, and save the FSK file.
</ng-container>
<ng-container *ngIf="format === 'kasperskytxt'">
Open the Kaspersky Password Manager desktop application and navigate to "Settings" &rarr;
"Import/Export". Locate the "Export to text file" section and select the "Export" button to
save the TXT file.
</ng-container>
<ng-container *ngIf="format === 'remembearcsv'">
Open the RememBear desktop application and navigate to "Settings" &rarr; "Account" &rarr;
"Export". Enter your master password and select the "Export Anyway" button to save the CSV
file.
</ng-container>
<ng-container *ngIf="format === 'passwordwallettxt'">
Open the PasswordWallet desktop application and navigate to "File" &rarr; "Export" &rarr;
"Visible entries to text file". Enter your password and select the "Ok" button to save the TXT
file.
</ng-container>
<ng-container *ngIf="format === 'mykicsv'">
Open the Myki desktop browser extension and navigate to "Advanced" &rarr; "Export Accounts"
and then scan the QR code with your mobile device. Various CSV files will then be saved to
your computer's downloads folder.
</ng-container>
<ng-container *ngIf="format === 'securesafecsv'">
Export your SecureSafe password safe to a CSV file with a comma delimiter.
</ng-container>
<ng-container *ngIf="format === 'logmeoncecsv'">
Open the LogMeOnce browser extension, then navigate to "Open Menu" &rarr; "Export To" and
select "CSV File" to save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'blackberrycsv'">
Open the BlackBerry Password Keeper application, then navigate to "Settings" &rarr;
"Import/Export". Select "Export Passwords" and follow the instructions on screen to save the
unencrypted CSV file.
</ng-container>
<ng-container *ngIf="format === 'buttercupcsv'">
Open the Buttercup desktop application and unlock your vault. Right click on your vault's icon
and select "Export" to save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'codebookcsv'">
Open the Codebook desktop application and log in. Navigate to "File" &rarr; "Export all", then
click "Yes" on the dialog and save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'encryptrcsv'">
Open the newest version of the Encryptr desktop application and allow all of your data to
sync. Once syncing of your data is complete, the download icon in the top right corner will
turn pink. Click the download icon and save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'yoticsv'">
From the Yoti browser extension, click on "Settings", then "Export Saved Logins" and save the
CSV file.
</ng-container>
<ng-container *ngIf="format === 'psonojson'">
Log in to the Psono web vault, click on the "Signed in as"-dropdown, select "Others". Go to
the "Export"-tab and select the json type export and then click on Export.
</ng-container>
<ng-container *ngIf="format === 'passkyjson'">
Log in to "https://vault.passky.org" &rarr; "Import & Export" &rarr; "Export" in the Passky
section. ("Backup" is unsupported as it is encrypted).
</ng-container>
<ng-container *ngIf="format === 'protonpass'">
In the ProtonPass browser extension, go to Settings > Export. Export without PGP encryption
and save the zip file.
</ng-container>
</bit-callout>
<bit-form-field>
<bit-label>{{ "selectImportFile" | i18n }}</bit-label>
<div class="file-selector">
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
{{ "chooseFile" | i18n }}
</button>
{{ this.fileSelected ? this.fileSelected.name : ("noFileChosen" | i18n) }}
</div>
<input
bitInput
#fileSelector
type="file"
id="file"
class="form-control-file"
name="file"
formControlName="file"
(change)="setSelectedFile($event)"
hidden
/>
</bit-form-field>
<bit-form-field>
<bit-label>{{ "orCopyPasteFileContents" | i18n }}</bit-label>
<textarea
id="fileContents"
bitInput
name="FileContents"
formControlName="fileContents"
></textarea>
</bit-form-field>
</form>

View File

@@ -0,0 +1,471 @@
import { CommonModule } from "@angular/common";
import {
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
} from "@angular/core";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import * as JSZip from "jszip";
import { concat, Observable, Subject, lastValueFrom, combineLatest } from "rxjs";
import { map, takeUntil } from "rxjs/operators";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import {
canAccessImportExport,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import {
AsyncActionsModule,
BitSubmitDirective,
ButtonModule,
CalloutModule,
DialogService,
FormFieldModule,
IconButtonModule,
SelectModule,
} from "@bitwarden/components";
import { ImportOption, ImportResult, ImportType } from "../models";
import {
ImportApiService,
ImportApiServiceAbstraction,
ImportService,
ImportServiceAbstraction,
} from "../services";
import {
FilePasswordPromptComponent,
ImportErrorDialogComponent,
ImportSuccessDialogComponent,
} from "./dialog";
@Component({
selector: "tools-import",
templateUrl: "import.component.html",
standalone: true,
imports: [
CommonModule,
JslibModule,
FormFieldModule,
AsyncActionsModule,
ButtonModule,
IconButtonModule,
SelectModule,
CalloutModule,
ReactiveFormsModule,
],
providers: [
{
provide: ImportApiServiceAbstraction,
useClass: ImportApiService,
deps: [ApiService],
},
{
provide: ImportServiceAbstraction,
useClass: ImportService,
deps: [
CipherService,
FolderService,
ImportApiServiceAbstraction,
I18nService,
CollectionService,
CryptoService,
],
},
],
})
export class ImportComponent implements OnInit, OnDestroy {
featuredImportOptions: ImportOption[];
importOptions: ImportOption[];
format: ImportType = null;
fileSelected: File;
folders$: Observable<FolderView[]>;
collections$: Observable<CollectionView[]>;
organizations$: Observable<Organization[]>;
private _organizationId: string;
get organizationId(): string {
return this._organizationId;
}
@Input() set organizationId(value: string) {
this._organizationId = value;
this.organizationService
.get$(this._organizationId)
.pipe(takeUntil(this.destroy$))
.subscribe((organization) => {
this._organizationId = organization?.id;
this.organization = organization;
});
}
protected organization: Organization;
protected destroy$ = new Subject<void>();
private _importBlockedByPolicy = false;
formGroup = this.formBuilder.group({
vaultSelector: [
"myVault",
{
nonNullable: true,
validators: [Validators.required],
},
],
targetSelector: [null],
format: [null as ImportType | null, [Validators.required]],
fileContents: [],
file: [],
});
@ViewChild(BitSubmitDirective)
private bitSubmit: BitSubmitDirective;
@Output()
formLoading = new EventEmitter<boolean>();
@Output()
formDisabled = new EventEmitter<boolean>();
@Output()
onSuccessfulImport = new EventEmitter<string>();
ngAfterViewInit(): void {
this.bitSubmit.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => {
this.formLoading.emit(loading);
});
this.bitSubmit.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => {
this.formDisabled.emit(disabled);
});
}
constructor(
protected i18nService: I18nService,
protected importService: ImportServiceAbstraction,
protected platformUtilsService: PlatformUtilsService,
protected policyService: PolicyService,
private logService: LogService,
protected syncService: SyncService,
protected dialogService: DialogService,
protected folderService: FolderService,
protected collectionService: CollectionService,
protected organizationService: OrganizationService,
protected formBuilder: FormBuilder
) {}
protected get importBlockedByPolicy(): boolean {
return this._importBlockedByPolicy;
}
ngOnInit() {
this.setImportOptions();
this.organizations$ = concat(
this.organizationService.memberOrganizations$.pipe(
canAccessImportExport(this.i18nService),
map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name")))
)
);
combineLatest([
this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership),
this.organizations$,
])
.pipe(takeUntil(this.destroy$))
.subscribe(([policyApplies, orgs]) => {
this._importBlockedByPolicy = policyApplies;
if (policyApplies && orgs.length == 0) {
this.formGroup.disable();
}
});
if (this.organizationId) {
this.formGroup.controls.vaultSelector.patchValue(this.organizationId);
this.formGroup.controls.vaultSelector.disable();
this.collections$ = Utils.asyncToObservable(() =>
this.collectionService
.getAllDecrypted()
.then((c) => c.filter((c2) => c2.organizationId === this.organizationId))
);
} else {
// Filter out the `no folder`-item from folderViews$
this.folders$ = this.folderService.folderViews$.pipe(
map((folders) => folders.filter((f) => f.id != null))
);
this.formGroup.controls.targetSelector.disable();
this.formGroup.controls.vaultSelector.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((value) => {
this.organizationId = value != "myVault" ? value : undefined;
if (!this._importBlockedByPolicy) {
this.formGroup.controls.targetSelector.enable();
}
if (value) {
this.collections$ = Utils.asyncToObservable(() =>
this.collectionService
.getAllDecrypted()
.then((c) => c.filter((c2) => c2.organizationId === value))
);
}
});
this.formGroup.controls.vaultSelector.setValue("myVault");
}
this.formGroup.controls.format.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((value) => {
this.format = value;
});
}
submit = async () => {
if (this.formGroup.invalid) {
this.formGroup.markAllAsTouched();
return;
}
await this.performImport();
};
protected async performImport() {
if (this.organization) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "warning" },
content: { key: "importWarning", placeholders: [this.organization.name] },
type: "warning",
});
if (!confirmed) {
return;
}
}
if (this.importBlockedByPolicy) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("personalOwnershipPolicyInEffectImports")
);
return;
}
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",
this.i18nService.t("errorOccurred"),
this.i18nService.t("selectFormat")
);
return;
}
const fileEl = document.getElementById("file") as HTMLInputElement;
const files = fileEl.files;
let fileContents = this.formGroup.controls.fileContents.value;
if ((files == null || files.length === 0) && (fileContents == null || fileContents === "")) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("selectFile")
);
return;
}
if (files != null && files.length > 0) {
try {
const content = await this.getFileContents(files[0]);
if (content != null) {
fileContents = content;
}
} catch (e) {
this.logService.error(e);
}
}
if (fileContents == null || fileContents === "") {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("selectFile")
);
return;
}
if (this.organizationId) {
await this.organizationService.get(this.organizationId)?.isAdmin;
}
try {
const result = await this.importService.import(
importer,
fileContents,
this.organizationId,
this.formGroup.controls.targetSelector.value,
this.isUserAdmin(this.organizationId)
);
//No errors, display success message
this.dialogService.open<unknown, ImportResult>(ImportSuccessDialogComponent, {
data: result,
});
this.syncService.fullSync(true);
this.onSuccessfulImport.emit(this._organizationId);
} catch (e) {
this.dialogService.open<unknown, Error>(ImportErrorDialogComponent, {
data: e,
});
this.logService.error(e);
}
}
private isUserAdmin(organizationId?: string): boolean {
if (!organizationId) {
return false;
}
return this.organizationService.get(this.organizationId)?.isAdmin;
}
getFormatInstructionTitle() {
if (this.format == null) {
return null;
}
const results = this.featuredImportOptions
.concat(this.importOptions)
.filter((o) => o.id === this.format);
if (results.length > 0) {
return this.i18nService.t("instructionsFor", results[0].name);
}
return null;
}
protected setImportOptions() {
this.featuredImportOptions = [
{
id: null,
name: "-- " + this.i18nService.t("select") + " --",
},
...this.importService.featuredImportOptions,
];
this.importOptions = [...this.importService.regularImportOptions].sort((a, b) => {
if (a.name == null && b.name != null) {
return -1;
}
if (a.name != null && b.name == null) {
return 1;
}
if (a.name == null && b.name == null) {
return 0;
}
return this.i18nService.collator
? this.i18nService.collator.compare(a.name, b.name)
: a.name.localeCompare(b.name);
});
}
setSelectedFile(event: Event) {
const fileInputEl = <HTMLInputElement>event.target;
this.fileSelected = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
}
private getFileContents(file: File): Promise<string> {
if (this.format === "1password1pux" && file.name.endsWith(".1pux")) {
return this.extractZipContent(file, "export.data");
}
if (
this.format === "protonpass" &&
(file.type === "application/zip" ||
file.type == "application/x-zip-compressed" ||
file.name.endsWith(".zip"))
) {
return this.extractZipContent(file, "Proton Pass/data.json");
}
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsText(file, "utf-8");
reader.onload = (evt) => {
if (this.format === "lastpasscsv" && file.type === "text/html") {
const parser = new DOMParser();
const doc = parser.parseFromString((evt.target as any).result, "text/html");
const pre = doc.querySelector("pre");
if (pre != null) {
resolve(pre.textContent);
return;
}
reject();
return;
}
resolve((evt.target as any).result);
};
reader.onerror = () => {
reject();
};
});
}
private extractZipContent(zipFile: File, contentFilePath: string): Promise<string> {
return new JSZip()
.loadAsync(zipFile)
.then((zip) => {
return zip.file(contentFilePath).async("string");
})
.then(
function success(content) {
return content;
},
function error(e) {
return "";
}
);
}
async getFilePassword(): Promise<string> {
const dialog = this.dialogService.open<string>(FilePasswordPromptComponent, {
ariaModal: true,
});
return await lastValueFrom(dialog.closed);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@@ -0,0 +1,3 @@
export * from "./dialog";
export { ImportComponent } from "./import.component";