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:
committed by
GitHub
parent
d0e72f5554
commit
9e290a3fed
@@ -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>
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
3
libs/importer/src/components/dialog/index.ts
Normal file
3
libs/importer/src/components/dialog/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./import-error-dialog.component";
|
||||
export * from "./import-success-dialog.component";
|
||||
export * from "./file-password-prompt.component";
|
||||
376
libs/importer/src/components/import.component.html
Normal file
376
libs/importer/src/components/import.component.html
Normal 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" → "Export to CSV file" and
|
||||
save the CSV file.
|
||||
</ng-container>
|
||||
<ng-container *ngIf="format === 'aviracsv'">
|
||||
In the Avira web vault, go to "Settings" → "My Data" → "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" → "Export
|
||||
Data", then click "Export CSV" for your "Accounts".
|
||||
</ng-container>
|
||||
<ng-container *ngIf="format === 'safeincloudxml'">
|
||||
Using the SaveInCloud desktop application, navigate to "File" → "Export" → "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" → "Export" button and save the file "As CSV".
|
||||
</ng-container>
|
||||
<ng-container *ngIf="format === 'keepass2xml'">
|
||||
Using the KeePass 2 desktop application, navigate to "File" → "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" →
|
||||
"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" → "Export" → "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" → "Export" → "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" →
|
||||
"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" → "Export To" → "XML
|
||||
format..." and save the XML file.
|
||||
</ng-container>
|
||||
<ng-container *ngIf="format === 'dashlanecsv'">
|
||||
Log in to Dashlane, click on "My Account" → "Settings" → "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" → "Export" → "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) → "Export"
|
||||
→ "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" → "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) →
|
||||
"Options" → "Account & 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" → "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" → "Export data" →
|
||||
"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" → "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" → "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" → "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" → "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" → "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" → "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" → "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" →
|
||||
"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" → "Account" →
|
||||
"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" → "Export" →
|
||||
"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" → "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" → "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" →
|
||||
"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" → "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" → "Import & Export" → "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>
|
||||
471
libs/importer/src/components/import.component.ts
Normal file
471
libs/importer/src/components/import.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
3
libs/importer/src/components/index.ts
Normal file
3
libs/importer/src/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./dialog";
|
||||
|
||||
export { ImportComponent } from "./import.component";
|
||||
Reference in New Issue
Block a user