diff --git a/public/locales/en.json b/public/locales/en.json index b8d485e..7b23443 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -408,7 +408,6 @@ "autoRefreshNamePage": "Auto-refresh name page", "subMenuAdvanced": "Advanced settings", - "modalAuth": "Always prompt for master password with a modal dialog", "alwaysIncludeMined": "Always include mined transactions in transaction listings (may require refresh)", "copyNameSuffixes": "Include suffix when copying names", "addressCopyButtons": "Show copy buttons next to all addresses", diff --git a/src/App.tsx b/src/App.tsx index 547d1b0..7935a67 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,18 +5,19 @@ import { BrowserRouter as Router } from "react-router-dom"; import { Provider } from "react-redux"; -import { initStore } from "./store/init"; +import { initStore } from "@store/init"; // Set up localisation -import "./utils/i18n"; +import "@utils/i18n"; // FIXME: Apparently the import order of my CSS is important. Who knew! import "./App.less"; -import { AppLoading } from "./global/AppLoading"; -import { AppServices } from "./global/AppServices"; -import { WebsocketProvider } from "./global/ws/WebsocketProvider"; -import { LocaleContext } from "./global/LocaleContext"; +import { AppLoading } from "@global/AppLoading"; +import { AppServices } from "@global/AppServices"; +import { WebsocketProvider } from "@global/ws/WebsocketProvider"; +import { LocaleContext } from "@global/LocaleContext"; +import { AuthProvider } from "@comp/auth/AuthContext"; import { AppLayout } from "@layout/AppLayout"; @@ -36,14 +37,16 @@ return }> - - - + + + + - {/* Services, etc. */} - - - + {/* Services, etc. */} + + + + ; diff --git a/src/components/auth/AuthContext.tsx b/src/components/auth/AuthContext.tsx new file mode 100644 index 0000000..4be3007 --- /dev/null +++ b/src/components/auth/AuthContext.tsx @@ -0,0 +1,58 @@ +// 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, useCallback, createContext, FC } from "react"; + +import { useMasterPassword } from "@wallets"; + +import { AuthMasterPasswordModal } from "./AuthMasterPasswordModal"; +import { SetMasterPasswordModal } from "./SetMasterPasswordModal"; + +type PromptAuthFn = (encrypt: boolean | undefined, onAuthed?: () => void) => void; +export const AuthContext = createContext(undefined); + +interface ModalProps { + // Whether the modal text should say 'encrypt wallets' or 'decrypt wallets' + encrypt: boolean | undefined; + onAuthed?: () => void; +} + +export const AuthProvider: FC = ({ children }) => { + const { isAuthed, hasMasterPassword } = useMasterPassword(); + + // Don't render the modal unless we absolutely have to + const [clicked, setClicked] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + + const [modalProps, setModalProps] = useState({ encrypt: false }); + + const promptAuth: PromptAuthFn = useCallback((encrypt, onAuthed) => { + setModalProps({ encrypt, onAuthed }); + setModalVisible(true); + setClicked(true); + }, []); + + const submit = useCallback(() => { + setModalVisible(false); + modalProps.onAuthed?.(); + }, [modalProps]); + + return + {children} + + {clicked && !isAuthed && <> + {hasMasterPassword + ? setModalVisible(false)} + onSubmit={submit} + /> + : setModalVisible(false)} + onSubmit={submit} + />} + } + ; +}; diff --git a/src/components/auth/AuthForm.tsx b/src/components/auth/AuthForm.tsx new file mode 100644 index 0000000..b637628 --- /dev/null +++ b/src/components/auth/AuthForm.tsx @@ -0,0 +1,90 @@ +// 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, useMemo, useCallback, Ref } from "react"; +import { Button, Input, Form } from "antd"; + +import { useTFns, translateError } from "@utils/i18n"; + +import { FakeUsernameInput } from "./FakeUsernameInput"; +import { getMasterPasswordInput } from "./MasterPasswordInput"; + +import { authMasterPassword, useMasterPassword } from "@wallets"; + +interface FormValues { + masterPassword: string; +} + +interface AuthFormRes { + form: JSX.Element; + submit: () => Promise; + reset: () => void; +} + +export function useAuthForm( + encrypt: boolean | undefined, + onSubmit: () => void, + inputRef: Ref +): AuthFormRes { + const { t, tStr, tKey } = useTFns("masterPassword."); + + const { salt, tester } = useMasterPassword(); + + const [form] = Form.useForm(); + const [passwordError, setPasswordError] = useState(); + + const reset = useCallback(() => { + form.resetFields(); + }, [form]); + + const onFinish = useCallback(async function() { + const values = await form.validateFields(); + + try { + await authMasterPassword(salt, tester, values.masterPassword); + onSubmit(); + } catch (err) { + setPasswordError(translateError(t, err, tKey("errorUnknown"))); + } + }, [t, tKey, onSubmit, salt, tester, form]); + + const formEl = useMemo(() => <> +

{tStr(encrypt ? "popoverDescriptionEncrypt" : "popoverDescription")}

+ +
+ + + {/* Password input */} + + {getMasterPasswordInput({ + inputRef, + placeholder: tStr("passwordPlaceholder"), + autoFocus: true + })} + + + {/* Fake submit button to allow the enter key to submit in modal */} + - ; -} - -interface AuthFormRes { - form: JSX.Element; - submit: () => Promise; - reset: () => void; -} - -export function useAuthForm({ - encrypt, - onSubmit, - inputRef -}: AuthFormProps): AuthFormRes { - const { t, tStr, tKey } = useTFns("masterPassword."); - - const { salt, tester } = useMasterPassword(); - - const [form] = Form.useForm(); - const [passwordError, setPasswordError] = useState(); - - const reset = useCallback(() => { - form.resetFields(); - }, [form]); - - const onFinish = useCallback(async function() { - const values = await form.validateFields(); - - try { - await authMasterPassword(salt, tester, values.masterPassword); - onSubmit(); - } catch (err) { - setPasswordError(translateError(t, err, tKey("errorUnknown"))); - } - }, [t, tKey, onSubmit, salt, tester, form]); - - const formEl = useMemo(() => <> -

{tStr(encrypt ? "popoverDescriptionEncrypt" : "popoverDescription")}

- -
- - - {/* Password input */} - - {getMasterPasswordInput({ - inputRef, - placeholder: tStr("passwordPlaceholder"), - autoFocus: true - })} - - - {/* Fake submit button to allow the enter key to submit in modal */} -