diff --git a/public/locales/en.json b/public/locales/en.json index ef2c0c9..f5fe247 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -588,6 +588,8 @@ "fromFileButton": "Import from file", "textareaPlaceholder": "Paste backup code here", "textareaRequired": "Backup code is required.", + "fileErrorTitle": "Import error", + "fileErrorNotText": "The imported file must be a text file.", "modalTitle": "Import backup", "modalButton": "Import", @@ -608,6 +610,8 @@ "json": "The backup could not be decoded as it is not valid JSON!", "missingTester": "The backup could not be decoded as it is missing a tester key!", "missingSalt": "The backup could not be decoded as it is missing a salt key!", + "invalidTester": "The backup could not be decoded as the 'tester' key is the wrong type!", + "invalidSalt": "The backup could not be decoded as the 'salt' key is the wrong type!", "invalidWallets": "The backup could not be decoded as the 'wallets' key is the wrong type!", "invalidFriends": "The backup could not be decoded as the 'wallets' key is the wrong type!", "unknown": "The backup could not be decoded due to an unknown error. See console for details." diff --git a/src/pages/backup/BackupResultsTree.tsx b/src/pages/backup/BackupResultsTree.tsx new file mode 100644 index 0000000..9b2cf78 --- /dev/null +++ b/src/pages/backup/BackupResultsTree.tsx @@ -0,0 +1,29 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under GPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +import React from "react"; +import { Tree, Typography } from "antd"; +import { CheckCircleOutlined, WarningOutlined, ExclamationCircleOutlined } from "@ant-design/icons"; + +import { BackupResults, ResultType } from "./backupResults"; + +const { Paragraph } = Typography; + +interface Props { + results: BackupResults; +} + +export function BackupResultsTree({ results }: Props): JSX.Element { + return <> + {/* Results summary */} + + + {/* Results tree */} + + ; +} diff --git a/src/pages/backup/ImportBackupModal.tsx b/src/pages/backup/ImportBackupModal.tsx index 386e269..bd321e7 100644 --- a/src/pages/backup/ImportBackupModal.tsx +++ b/src/pages/backup/ImportBackupModal.tsx @@ -1,22 +1,24 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under GPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import React, { useState, Dispatch, SetStateAction} from "react"; -import { Modal, Form, Input, Button, Typography } from "antd"; +import React, { useState, Dispatch, SetStateAction } from "react"; +import { Modal, Form, FormInstance, Input, Button, Typography, Upload, UploadProps, notification } from "antd"; -import { useTranslation, Trans } from "react-i18next"; +import { useTranslation } from "react-i18next"; import { translateError } from "@utils/i18n"; -import { FakeUsernameInput } from "@comp/auth/FakeUsernameInput"; import { getMasterPasswordInput } from "@comp/auth/MasterPasswordInput"; import { ImportDetectFormat } from "./ImportDetectFormat"; -import { detectBackupFormat } from "../backup/backupParser"; +import { decodeBackup } from "./backupParser"; +import { backupVerifyPassword, backupImport } from "./backupImport"; +import { BackupResults } from "./backupResults"; +import { BackupResultsTree } from "./BackupResultsTree"; import Debug from "debug"; const debug = Debug("kristweb:import-backup-modal"); -const { Text, Paragraph } = Typography; +const { Paragraph } = Typography; const { TextArea } = Input; interface FormValues { @@ -34,12 +36,60 @@ const [form] = Form.useForm(); const [code, setCode] = useState(""); - const [decodeError, setDecodeError] = useState(); + const [decodeError, setDecodeError] = useState(); + const [masterPasswordError, setMasterPasswordError] = useState(); + const [results, setResults] = useState(); + + const uploadProps: UploadProps = { + showUploadList: false, + + /** Updates the contents of the 'code' field with the given file. */ + beforeUpload(file) { + debug("importing file %s: %o", file.name, file); + + // Disallow non-plaintext files + if (file.type !== "text/plain") { + notification.error({ + message: t("import.fileErrorTitle"), + description: t("import.fileErrorNotText") + }); + return false; + } + + // Read the file and update the contents of the code field + const reader = new FileReader(); + reader.readAsText(file, "UTF-8"); + reader.onload = e => { + if (!e.target || !e.target.result) { + debug("reader.onload target was null?!", e); + return; + } + + const contents = e.target.result.toString(); + debug("got file contents: %s", contents); + + // Update the form + setCode(contents); // Triggers a format re-detection + form.setFieldsValue({ code: contents }); + }; + + // Don't actually upload the file + return false; + } + }; + + /** Resets all the state when the modal is closed. */ + function resetState() { + form.resetFields(); + setCode(""); + setDecodeError(""); + setMasterPasswordError(""); + setResults(undefined); + } function closeModal() { debug("closing modal and resetting fields"); - form.resetFields(); - setCode(""); + resetState(); setVisible(false); } @@ -50,89 +100,170 @@ // Detect the backup format for the final time, validate the password, and // if all is well, begin the import async function onFinish() { + // If we're already on the results screen, close the modal instead + if (results) return closeModal(); + const values = await form.validateFields(); - if (!values.masterPassword || !values.code) return; + + const { masterPassword, code } = values; + if (!masterPassword || !code) return; try { // Decode first - const format = detectBackupFormat(code); - debug("detected format: %s", format.type); - + const backup = decodeBackup(code); + debug("detected format: %s", backup.type); setDecodeError(undefined); + + // Attempt to verify the master password + await backupVerifyPassword(backup, masterPassword); + setMasterPasswordError(undefined); + + // Perform the import + const results = await backupImport(backup, masterPassword); + setResults(results); } catch (err) { - console.error(err); - setDecodeError(translateError(t, err, "import.decodeErrors.unknown")); + if (err.message === "import.masterPasswordRequired" + || err.message === "import.masterPasswordIncorrect") { + // Master password incorrect error + setMasterPasswordError(translateError(t, err)); + } else { + // Any other decoding error + console.error(err); + setDecodeError(translateError(t, err, "import.decodeErrors.unknown")); + } } } return + {t("dialog.close")} + + ] + : [ // Import screen + // "Import from file" button for import screen +
+ + + +
, + + // "Cancel" button for import screen + , + + // "Import" button for import screen + + ]} > -
+ ) + : ( + // No results - show the import form + - {/* Import lead */} - {t("import.description")} - - {/* Detected format information */} - - - {/* Password input */} - - {getMasterPasswordInput({ - placeholder: t("import.masterPasswordPlaceholder"), - autoFocus: true - })} - - - - {/* Code textarea */} - -