diff --git a/public/locales/en.json b/public/locales/en.json index 72c8489..c2d9102 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -131,7 +131,14 @@ "earlyAuthError": "The app has not loaded fully yet, please try again.", "reset": { - "modalTitle": "Reset master password" + "modalTitle": "Reset master password", + "description": "Are you sure you want to reset your master password? All your wallets will be deleted. Make sure to <3>export a backup first!", + "buttonConfirm": "Reset & delete", + + "modalTitle2": "DELETE ALL WALLETS", + "description2": "Are you REALLY sure you want to DELETE ALL YOUR WALLETS?", + "buttonConfirm2": "Yes, I'm sure [{{n}}]", + "buttonConfirmFinal": "Yes, I'm sure!" }, "change": { @@ -424,6 +431,10 @@ "importBackup": "Import wallets", "exportBackup": "Export wallets", + "subMenuMasterPassword": "Master password", + "changeMasterPassword": "Change master password", + "resetMasterPassword": "Reset master password", + "subMenuAutoRefresh": "Auto-refresh", "autoRefreshTables": "Auto-refresh tables", "autoRefreshTablesDescription": "Whether or not large table listings (e.g. transactions, names) should automatically refresh when a change is detected on the network.", diff --git a/src/krist/wallets/functions/resetMasterPassword.ts b/src/krist/wallets/functions/resetMasterPassword.ts new file mode 100644 index 0000000..3cd4fd6 --- /dev/null +++ b/src/krist/wallets/functions/resetMasterPassword.ts @@ -0,0 +1,22 @@ +// 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 { store } from "@app"; + +import { getWalletKey } from "@wallets"; + +export function resetMasterPassword(): void { + // Remove the master password from local storage + localStorage.removeItem("salt2"); + localStorage.removeItem("tester2"); + + // Find and remove all the wallets + const wallets = store.getState().wallets.wallets; + for (const id in wallets) { + const key = getWalletKey(id); + localStorage.removeItem(key); + } + + // Reload the page and return to the dashboard + location.href = "/"; +} diff --git a/src/krist/wallets/index.ts b/src/krist/wallets/index.ts index 52ac798..3f00305 100644 --- a/src/krist/wallets/index.ts +++ b/src/krist/wallets/index.ts @@ -8,6 +8,7 @@ export * from "./functions/syncWallets"; export * from "./functions/decryptWallet"; export * from "./functions/recalculateWallets"; +export * from "./functions/resetMasterPassword"; export * from "./masterPassword"; export * from "./walletStorage"; export * from "./utils"; diff --git a/src/pages/settings/ResetMasterPassword.tsx b/src/pages/settings/ResetMasterPassword.tsx deleted file mode 100644 index 50a4cf8..0000000 --- a/src/pages/settings/ResetMasterPassword.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// 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 { useCallback } from "react"; -import { Modal } from "antd"; -import { ExclamationCircleOutlined } from "@ant-design/icons"; - -import { useTFns } from "@utils/i18n"; - -interface HookRes { - resetMasterPasswordCtx: JSX.Element; - openResetMasterPassword: () => void; -} - -export function useResetMasterPasswordModal(): HookRes { - const { tStr } = useTFns("masterPassword.reset."); - const [modal, contextHolder] = Modal.useModal(); - - const openResetMasterPassword = useCallback(() => modal.confirm({ - icon: , - title: tStr("modalTitle"), - }), [tStr, modal]); - - return { - resetMasterPasswordCtx: contextHolder, - openResetMasterPassword - }; -} diff --git a/src/pages/settings/SettingsBackups.tsx b/src/pages/settings/SettingsBackups.tsx deleted file mode 100644 index 4465a1b..0000000 --- a/src/pages/settings/SettingsBackups.tsx +++ /dev/null @@ -1,48 +0,0 @@ -// 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 } from "react"; -import { Menu } from "antd"; -import { - DatabaseOutlined, ImportOutlined, ExportOutlined -} from "@ant-design/icons"; - -import { useTFns } from "@utils/i18n"; - -import { AuthorisedAction } from "@comp/auth/AuthorisedAction"; -import { useMasterPassword } from "@wallets"; -import { ImportBackupModal } from "../backup/ImportBackupModal"; -import { ExportBackupModal } from "../backup/ExportBackupModal"; - -export function SettingsBackups({ ...props }: any): JSX.Element { - const { tStr } = useTFns("settings."); - - const [importVisible, setImportVisible] = useState(false); - const [exportVisible, setExportVisible] = useState(false); - - // Used to disable the export button if a master password hasn't been set up - const { hasMasterPassword, salt, tester } = useMasterPassword(); - const allowExport = !!hasMasterPassword && !!salt && !!tester; - - return <> - } - title={tStr("subMenuBackups")} - {...props} - > - - setImportVisible(true)}> -
{tStr("importBackup")}
-
-
- - setExportVisible(true)}> -
{tStr("exportBackup")}
-
-
- - - ; -} diff --git a/src/pages/settings/SettingsPage.tsx b/src/pages/settings/SettingsPage.tsx index c34b331..455af6b 100644 --- a/src/pages/settings/SettingsPage.tsx +++ b/src/pages/settings/SettingsPage.tsx @@ -15,7 +15,7 @@ import { SettingBoolean } from "./SettingBoolean"; import { getLanguageItems } from "./translations/LanguageItem"; -import { SettingsBackups } from "./SettingsBackups"; +import { useSettingsManage } from "./manage/SettingsManage"; import "./SettingsPage.less"; @@ -37,6 +37,8 @@ export function SettingsPage(): JSX.Element { const { t } = useTranslation(); + const [manageSettings, manageSettingsCtx] = useSettingsManage(); + return {/* Language selector */} @@ -44,8 +46,9 @@ {getLanguageItems()} - {/* Backups */} - + {/* Backups, master password */} + {manageSettings} + {manageSettingsCtx} {/* Auto-refresh settings */} void; +} + +export function useResetMasterPasswordModal( + openExportBackup: () => void +): HookRes { + const { t, tStr, tKey } = useTFns("masterPassword.reset."); + const [modal, contextHolder] = Modal.useModal(); + + const countUp = useRef(1); + const countUpMax = useRef(5); + const confirmModalRef = useRef>(); + + const confirm2 = useCallback(() => modal.confirm({ + icon: , + + width: 800, + + title: tStr("modalTitle2"), + content: + Are you REALLY sure you want to DELETE ALL YOUR WALLETS? + , + + okButtonProps: { danger: true, disabled: true }, + okText: t(tKey("buttonConfirm2"), { n: countUp.current }), + onOk: resetMasterPassword, + + cancelText: t("dialog.no"), + autoFocusButton: "cancel", + }), [t, tStr, tKey, modal]); + + // Make the final confirmation button count-up in a really annoying way, so + // if a user still ignores this and deletes their passwords, they're just an + // idiot. + const incrementCountUp = useCallback(() => { + setTimeout(() => { + const confirmModal = confirmModalRef.current; + if (!confirmModal) return; + + if (countUp.current++ < countUpMax.current) { + // Update the count-up button text + confirmModal.update({ + okText: t(tKey("buttonConfirm2"), { n: countUp.current }) + }); + incrementCountUp(); + } else { + // Count-up complete, enable the button + confirmModal.update({ + okButtonProps: { danger: true }, + okText: tStr("buttonConfirmFinal") + }); + } + }, random(1300, 1800)); + }, [t, tStr, tKey]); + + // First confirmation dialog + const openResetMasterPassword = useCallback(() => modal.confirm({ + icon: , + + title: tStr("modalTitle"), + content: + Are you sure you want to reset your master password? + All your wallets will be deleted. + Make sure to + export a backup + first! + , + + okButtonProps: { danger: true }, + okText: tStr("buttonConfirm"), + onOk: () => { + // Open the modal, initially with the confirm button disabled + countUp.current = 1; + countUpMax.current = random(5, 7); + confirmModalRef.current = confirm2(); + incrementCountUp(); + }, + + autoFocusButton: "cancel" + }), [tStr, tKey, modal, openExportBackup, confirm2, incrementCountUp]); + + return { + resetMasterPasswordCtx: contextHolder, + openResetMasterPassword + }; +} diff --git a/src/pages/settings/manage/SettingsManage.tsx b/src/pages/settings/manage/SettingsManage.tsx new file mode 100644 index 0000000..da85214 --- /dev/null +++ b/src/pages/settings/manage/SettingsManage.tsx @@ -0,0 +1,79 @@ +// 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 } from "react"; +import { Menu } from "antd"; +import { + DatabaseOutlined, ImportOutlined, ExportOutlined, KeyOutlined +} from "@ant-design/icons"; + +import { useTFns } from "@utils/i18n"; + +import { AuthorisedAction } from "@comp/auth/AuthorisedAction"; +import { useMasterPassword } from "@wallets"; + +import { ImportBackupModal } from "../../backup/ImportBackupModal"; +import { ExportBackupModal } from "../../backup/ExportBackupModal"; +import { useResetMasterPasswordModal } from "./ResetMasterPassword"; + +export function useSettingsManage(): [JSX.Element[], JSX.Element] { + const { tStr } = useTFns("settings."); + + const [importVisible, setImportVisible] = useState(false); + const [exportVisible, setExportVisible] = useState(false); + + const { resetMasterPasswordCtx, openResetMasterPassword } = + useResetMasterPasswordModal(() => setExportVisible(true)); + + // Used to disable the export button if a master password hasn't been set up + const { hasMasterPassword, salt, tester } = useMasterPassword(); + const allowExport = !!hasMasterPassword && !!salt && !!tester; + + const settings = [ + // Manage backups sub-menu + } + title={tStr("subMenuBackups")} + > + {/* Import backup */} + + setImportVisible(true)}> +
{tStr("importBackup")}
+
+
+ + {/* Export backup */} + setExportVisible(true)}> +
{tStr("exportBackup")}
+
+
, + + // Master password sub-menu + } + title={tStr("subMenuMasterPassword")} + > + {/* Change master password */} + + {tStr("changeMasterPassword")} + + + {/* Reset master password */} + + {tStr("resetMasterPassword")} + + + ]; + + const ctx = <> + + + {resetMasterPasswordCtx} + ; + + return [settings, ctx]; +}