diff --git a/public/locales/en.json b/public/locales/en.json index b47691a..0d4e6f0 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -745,6 +745,9 @@ "detectedFormatKristWebV2": "KristWeb v2", "detectedFormatInvalid": "Invalid!", + "progress": "Importing <1>{{count, number}} item...", + "progress_plural": "Importing <1>{{count, number}} items...", + "decodeErrors": { "atob": "The backup could not be decoded as it is not valid base64!", "json": "The backup could not be decoded as it is not valid JSON!", diff --git a/src/index.tsx b/src/index.tsx index c0ceb8b..233b61e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,14 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under AGPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import "./utils/setup"; -import { i18nLoader } from "./utils/i18n"; +import "@utils/setup"; +import { i18nLoader } from "@utils/i18n"; +import { isLocalhost } from "@utils"; import ReactDOM from "react-dom"; import "./index.css"; -import App from "./App"; +import App from "@app"; import Debug from "debug"; const debug = Debug("kristweb:index"); @@ -15,7 +16,14 @@ // import reportWebVitals from "./reportWebVitals"; debug("============================ APP STARTING ============================"); -debug("waiting for i18n first"); +if (isLocalhost && !localStorage.getItem("status")) { + // Automatically enable debug logging on localhost + localStorage.setItem("debug", "kristweb:*"); + localStorage.setItem("status", "LOCAL"); + location.reload(); +} + +debug("waiting for i18n"); i18nLoader.then(() => { debug("performing initial render"); ReactDOM.render( diff --git a/src/pages/CheckStatus.tsx b/src/pages/CheckStatus.tsx index db5910c..630cca9 100644 --- a/src/pages/CheckStatus.tsx +++ b/src/pages/CheckStatus.tsx @@ -6,6 +6,6 @@ import { StatusPage } from "./StatusPage"; export function CheckStatus(): JSX.Element { - const ok = localStorage.getItem("status") === "Ok"; + const ok = /^.?O[kC]/.test(localStorage.getItem("status") || "offline"); return ok ? : ; } diff --git a/src/pages/backup/ImportBackupForm.tsx b/src/pages/backup/ImportBackupForm.tsx new file mode 100644 index 0000000..adfac8e --- /dev/null +++ b/src/pages/backup/ImportBackupForm.tsx @@ -0,0 +1,201 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under AGPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +import { useState, Dispatch, SetStateAction } from "react"; +import { Form, Input, Checkbox, Typography } from "antd"; + +import { useTFns, translateError } from "@utils/i18n"; + +import { getMasterPasswordInput } from "@comp/auth/MasterPasswordInput"; + +import { useBooleanSetting, setBooleanSetting } from "@utils/settings"; + +import { ImportDetectFormat } from "./ImportDetectFormat"; +import { IncrProgressFn, InitProgressFn } from "./ImportProgress"; +import { decodeBackup } from "./backupParser"; +import { backupVerifyPassword, backupImport } from "./backupImport"; +import { BackupResults } from "./backupResults"; + +import Debug from "debug"; +const debug = Debug("kristweb:import-backup-modal"); + +const { Paragraph } = Typography; +const { TextArea } = Input; + +interface FormValues { + masterPassword: string; + code: string; + overwrite: boolean; +} + +interface ImportBackupFormHookRes { + form: JSX.Element; + + resetForm: () => void; + triggerSubmit: () => Promise; + + setCode: (code: string) => void; +} + +export function useImportBackupForm( + setLoading: Dispatch>, + setResults: Dispatch>, + + onProgress: IncrProgressFn, + initProgress: InitProgressFn +): ImportBackupFormHookRes { + const { t, tStr, tKey } = useTFns("import."); + + const [form] = Form.useForm(); + + const [code, setCode] = useState(""); + const [decodeError, setDecodeError] = useState(); + const [masterPasswordError, setMasterPasswordError] = useState(); + + const importOverwrite = useBooleanSetting("importOverwrite"); + + function resetForm() { + form.resetFields(); + setCode(""); + setDecodeError(""); + setMasterPasswordError(""); + } + + function onValuesChange(changed: Partial) { + if (changed.code) setCode(changed.code); + + // Remember the value of the 'overwrite' checkbox + if (changed.overwrite !== undefined) { + debug("updating importOverwrite to %b", changed.overwrite); + setBooleanSetting("importOverwrite", changed.overwrite, false); + } + } + + // Detect the backup format for the final time, validate the password, and + // if all is well, begin the import + async function onFinish() { + const values = await form.validateFields(); + + const { masterPassword, code, overwrite } = values; + if (!masterPassword || !code) return; + + setLoading(true); + + try { + // Decode first + 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, !overwrite, + onProgress, initProgress + ); + + setResults(results); + } catch (err) { + 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, tKey("decodeErrors.unknown"))); + } + } finally { + setLoading(false); + } + } + + const formEl =
+ {/* Import lead */} + {tStr("description")} + + {/* Detected format information */} + + + {/* Password input */} + + {getMasterPasswordInput({ + placeholder: tStr("masterPasswordPlaceholder"), + autoFocus: true + })} + + + {/* Code textarea */} + +