From 273eb146bb8bfd2d33f80a59386293b2226c9d17 Mon Sep 17 00:00:00 2001 From: Tom Kirkpatrick Date: Mon, 9 Sep 2019 14:30:26 +0100 Subject: [PATCH] feat(app): basic password authentication fix #1387 --- renderer/components/Login/Login.js | 71 ++++++++++++++++++ renderer/components/Login/index.js | 3 + renderer/components/Login/messages.js | 6 ++ renderer/components/Settings/SettingsForm.js | 6 ++ renderer/components/Settings/SettingsPage.js | 20 ++++- renderer/components/UI/index.js | 1 + renderer/containers/Initializer.js | 45 +---------- renderer/containers/Login/Login.js | 35 +++++++++ renderer/containers/Login/index.js | 3 + renderer/containers/Root.js | 54 +++++++++++-- renderer/containers/Settings/SettingsPage.js | 2 + renderer/reducers/account.js | 79 +++++++++++++++++++- renderer/reducers/messages/messages.js | 1 + 13 files changed, 273 insertions(+), 53 deletions(-) create mode 100644 renderer/components/Login/Login.js create mode 100644 renderer/components/Login/index.js create mode 100644 renderer/components/Login/messages.js create mode 100644 renderer/containers/Login/Login.js create mode 100644 renderer/containers/Login/index.js diff --git a/renderer/components/Login/Login.js b/renderer/components/Login/Login.js new file mode 100644 index 00000000000..1c57b752c77 --- /dev/null +++ b/renderer/components/Login/Login.js @@ -0,0 +1,71 @@ +import React, { useEffect, useRef } from 'react' +import PropTypes from 'prop-types' +import { FormattedMessage } from 'react-intl' +import { Box, Flex } from 'rebass/styled-components' +import { Button, CenteredContent, Message, Text } from 'components/UI' +import { Form, PasswordInput } from 'components/Form' +import ArrowRight from 'components/Icon/ArrowRight' +import ZapLogo from 'components/Icon/ZapLogo' +import messages from './messages' + +const Login = ({ login, loginError, clearLoginError, ...rest }) => { + const formApiRef = useRef(null) + + useEffect(() => { + const { current: formApi } = formApiRef + loginError && formApi.setFormError(loginError) + clearLoginError() + }, [loginError, formApiRef, clearLoginError]) + + const handleSubmit = ({ password }) => login(password) + + return ( +
(formApiRef.current = api)} onSubmit={handleSubmit} width={1}> + {({ formState: { submits, error } }) => { + const willValidateInline = submits > 0 + return ( + + + + + + + + + + {error && ( + + + {error} + + + )} + + + + + + + + ) + }} +
+ ) +} + +Login.propTypes = { + clearLoginError: PropTypes.func.isRequired, + login: PropTypes.func.isRequired, + loginError: PropTypes.string, +} + +export default Login diff --git a/renderer/components/Login/index.js b/renderer/components/Login/index.js new file mode 100644 index 00000000000..393a6df4079 --- /dev/null +++ b/renderer/components/Login/index.js @@ -0,0 +1,3 @@ +import Login from './Login' + +export default Login diff --git a/renderer/components/Login/messages.js b/renderer/components/Login/messages.js new file mode 100644 index 00000000000..3b70a695083 --- /dev/null +++ b/renderer/components/Login/messages.js @@ -0,0 +1,6 @@ +import { defineMessages } from 'react-intl' + +/* eslint-disable max-len */ +export default defineMessages({ + intro: 'Enter your password to continue.', +}) diff --git a/renderer/components/Settings/SettingsForm.js b/renderer/components/Settings/SettingsForm.js index f5b7712cf57..2f6043c73db 100644 --- a/renderer/components/Settings/SettingsForm.js +++ b/renderer/components/Settings/SettingsForm.js @@ -1,5 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' +import get from 'lodash/get' import { injectIntl } from 'react-intl' import { intlShape } from '@zap/i18n' import { Form } from 'components/Form' @@ -18,6 +19,11 @@ const SettingsForm = ({ }) => { const handleSubmit = async values => { try { + // If password feature has been disabled, rest the password to null. + if (!get(values, 'password.value')) { + values.password.value = null + } + // Save the updated settings. await saveConfigOverrides(values) diff --git a/renderer/components/Settings/SettingsPage.js b/renderer/components/Settings/SettingsPage.js index 4473bfe921f..0272d2e5490 100644 --- a/renderer/components/Settings/SettingsPage.js +++ b/renderer/components/Settings/SettingsPage.js @@ -14,13 +14,18 @@ import SettingsFieldsGeneral from './SettingsFieldsGeneral' import SettingsFieldsSecurity from './SettingsFieldsSecurity' import messages from './messages' -const SettingsMenu = ({ group, setGroup, ...rest }) => { - const items = [ +const SettingsMenu = ({ group, setGroup, isLoggedIn, ...rest }) => { + // Items accessle to unauthenticated users. + const anonItems = [ { id: 'general', title: , onClick: () => setGroup('general'), }, + ] + + // Items only accessible to authenticated users. + const authItems = [ { id: 'wallet', title: , @@ -33,11 +38,17 @@ const SettingsMenu = ({ group, setGroup, ...rest }) => { }, ] + let items = [...anonItems] + if (isLoggedIn) { + items = [...items, ...authItems] + } + return } SettingsMenu.propTypes = { group: PropTypes.string, + isLoggedIn: PropTypes.bool, setGroup: PropTypes.func.isRequired, } @@ -91,7 +102,7 @@ SettingsActions.propTypes = { currentConfig: PropTypes.object.isRequired, } -const SettingsPage = ({ currentConfig, ...rest }) => { +const SettingsPage = ({ currentConfig, isLoggedIn, ...rest }) => { const [group, setGroup] = useState('general') return ( @@ -102,7 +113,7 @@ const SettingsPage = ({ currentConfig, ...rest }) => { - + @@ -124,6 +135,7 @@ const SettingsPage = ({ currentConfig, ...rest }) => { SettingsPage.propTypes = { currentConfig: PropTypes.object.isRequired, + isLoggedIn: PropTypes.bool, } export default SettingsPage diff --git a/renderer/components/UI/index.js b/renderer/components/UI/index.js index 71f5740f13b..a5bfb312237 100644 --- a/renderer/components/UI/index.js +++ b/renderer/components/UI/index.js @@ -6,6 +6,7 @@ export BackgroundTertiary from './BackgroundTertiary' export Bar from './Bar' export Button from './Button' export ButtonCreate from './ButtonCreate' +export CenteredContent from './CenteredContent' export ClippedText from './ClippedText' export CopyBox from './CopyBox' export CopyButton from './CopyButton' diff --git a/renderer/containers/Initializer.js b/renderer/containers/Initializer.js index 91a0bc2a19c..08bc2f5f9a8 100644 --- a/renderer/containers/Initializer.js +++ b/renderer/containers/Initializer.js @@ -2,13 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' import { Redirect } from 'react-router' -import { initLocale } from 'reducers/locale' -import { initCurrency } from 'reducers/ticker' -import { initWallets, walletSelectors } from 'reducers/wallet' -import { initNeutrino } from 'reducers/neutrino' -import { startActiveWallet } from 'reducers/lnd' -import { initAutopay } from 'reducers/autopay' -import { initChannels } from 'reducers/channels' +import { walletSelectors } from 'reducers/wallet' /** * Root component that deals with mounting the app and managing top level routing. @@ -18,36 +12,11 @@ class Initializer extends React.Component { activeWallet: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), activeWalletSettings: PropTypes.object, hasWallets: PropTypes.bool, - initAutopay: PropTypes.func.isRequired, - initChannels: PropTypes.func.isRequired, - initCurrency: PropTypes.func.isRequired, - initLocale: PropTypes.func.isRequired, - initNeutrino: PropTypes.func.isRequired, - initWallets: PropTypes.func.isRequired, isWalletOpen: PropTypes.bool, isWalletsLoaded: PropTypes.bool.isRequired, lndConnect: PropTypes.string, } - // Initialize app state. - componentDidMount() { - const { - initLocale, - initCurrency, - initWallets, - initChannels, - initAutopay, - initNeutrino, - } = this.props - initNeutrino() - - initLocale() - initCurrency() - initAutopay() - initWallets() - initChannels() - } - /** * getLocation - Returns current location based on app initialization state and referrer. * @@ -98,17 +67,7 @@ const mapStateToProps = state => ({ isWalletsLoaded: walletSelectors.isWalletsLoaded(state), }) -const mapDispatchToProps = { - startActiveWallet, - initNeutrino, - initCurrency, - initLocale, - initWallets, - initAutopay, - initChannels, -} - export default connect( mapStateToProps, - mapDispatchToProps + null )(Initializer) diff --git a/renderer/containers/Login/Login.js b/renderer/containers/Login/Login.js new file mode 100644 index 00000000000..306b33d6fd1 --- /dev/null +++ b/renderer/containers/Login/Login.js @@ -0,0 +1,35 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { Redirect } from 'react-router' +import { settingsSelectors } from 'reducers/settings' +import { login, clearLoginError, accountSelectors } from 'reducers/account' +import Login from 'components/Login' + +const WrappedLogin = ({ isPasswordEnabled, isLoggedIn, ...rest }) => { + if (isPasswordEnabled && !isLoggedIn) { + return + } + return +} + +WrappedLogin.propTypes = { + isLoggedIn: PropTypes.bool, + isPasswordEnabled: PropTypes.bool, +} + +const mapStateToProps = state => ({ + isPasswordEnabled: settingsSelectors.currentConfig(state).password.active, + isLoggedIn: accountSelectors.isLoggedIn(state), + loginError: accountSelectors.loginError(state), +}) + +const mapDispatchToProps = { + login, + clearLoginError, +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(WrappedLogin) diff --git a/renderer/containers/Login/index.js b/renderer/containers/Login/index.js new file mode 100644 index 00000000000..393a6df4079 --- /dev/null +++ b/renderer/containers/Login/index.js @@ -0,0 +1,3 @@ +import Login from './Login' + +export default Login diff --git a/renderer/containers/Root.js b/renderer/containers/Root.js index f0ee92492af..5152b4a33a1 100644 --- a/renderer/containers/Root.js +++ b/renderer/containers/Root.js @@ -10,7 +10,12 @@ import { initDatabase, setLoading, setMounted, appSelectors } from 'reducers/app import { initSettings } from 'reducers/settings' import { initTheme, themeSelectors } from 'reducers/theme' import { initAccount } from 'reducers/account' -import { walletSelectors } from 'reducers/wallet' +import { initLocale } from 'reducers/locale' +import { initCurrency } from 'reducers/ticker' +import { initWallets, walletSelectors } from 'reducers/wallet' +import { initNeutrino } from 'reducers/neutrino' +import { initAutopay } from 'reducers/autopay' +import { initChannels } from 'reducers/channels' import { isLoading, isLoadingPerPath, getLoadingMessage } from 'reducers/utils' import { Page, Titlebar, GlobalStyle } from 'components/UI' import GlobalNotification from 'components/GlobalNotification' @@ -18,6 +23,7 @@ import { withLoading } from 'hocs' import { DialogLndCrashed } from './Dialog' import Initializer from './Initializer' import Logout from './Logout' +import Login from './Login' import Home from './Home' import ModalStack from './RootModalStack' import Onboarding from './Onboarding/Onboarding' @@ -34,6 +40,12 @@ const Root = ({ initSettings, initTheme, initAccount, + initNeutrino, + initLocale, + initCurrency, + initAutopay, + initWallets, + initChannels, isMounted, setMounted, hasWallets, @@ -55,12 +67,31 @@ const Root = ({ setMounted(true) await initDatabase() await initSettings() - await initTheme() - await initAccount() + initTheme() + initAccount() + initNeutrino() + initLocale() + initCurrency() + initAutopay() + initWallets() + initChannels() } } init() - }, [initDatabase, initSettings, initTheme, initAccount, isMounted, setMounted]) + }, [ + initDatabase, + initSettings, + initTheme, + initAccount, + isMounted, + setMounted, + initNeutrino, + initLocale, + initCurrency, + initAutopay, + initWallets, + initChannels, + ]) const redirectToHome = () => history.push('/home') const redirectToLogout = () => history.push('/logout') @@ -85,7 +116,8 @@ const Root = ({ > {isRootReady && ( - + + ({ currentConfig: settingsSelectors.currentConfig(state), + isLoggedIn: accountSelectors.isLoggedIn(state), }) export default connect(mapStateToProps)(SettingsPage) diff --git a/renderer/reducers/account.js b/renderer/reducers/account.js index 9632a3464dd..63ff0a4e3da 100644 --- a/renderer/reducers/account.js +++ b/renderer/reducers/account.js @@ -1,4 +1,7 @@ +import { getIntl } from '@zap/i18n' import createReducer from './utils/createReducer' +import { settingsSelectors } from './settings' +import messages from './messages' // ------------------------------------ // Initial State @@ -10,6 +13,10 @@ export const initialState = { }, isAccountLoading: false, initAccountError: null, + + isLoggingIn: false, + loginError: null, + isLoggedIn: false, } // ------------------------------------ @@ -20,6 +27,11 @@ export const INIT_ACCOUNT = 'INIT_ACCOUNT' export const INIT_ACCOUNT_SUCCESS = 'INIT_ACCOUNT_SUCCESS' export const INIT_ACCOUNT_FAILURE = 'INIT_ACCOUNT_FAILURE' +export const LOGIN = 'LOGIN' +export const LOGIN_SUCCESS = 'LOGIN_SUCCESS' +export const LOGIN_FAILURE = 'LOGIN_FAILURE' +export const LOGIN_CLEAR_ERROR = 'LOGIN_CLEAR_ERROR' + // ------------------------------------ // Actions // ------------------------------------ @@ -30,7 +42,7 @@ export const INIT_ACCOUNT_FAILURE = 'INIT_ACCOUNT_FAILURE' * * @returns {Function} Thunk */ -export const initAccount = () => async dispatch => { +export const initAccount = () => async (dispatch, getState) => { dispatch({ type: INIT_ACCOUNT }) try { // Fetch existing account record. @@ -42,12 +54,53 @@ export const initAccount = () => async dispatch => { account.id = await window.db.accounts.add({}) } + // Auto login user of password feature is diabled. + const currentConfig = settingsSelectors.currentConfig(getState()) + const isAuthEnabled = currentConfig.password.active + if (!isAuthEnabled) { + dispatch({ type: LOGIN_SUCCESS }) + } + dispatch({ type: INIT_ACCOUNT_SUCCESS, account }) } catch (error) { dispatch({ type: INIT_ACCOUNT_FAILURE, error: error.message }) } } +/** + * login - Perform account login. + * + * @param {string} password Password + * @returns {Function} Thunk + */ +export const login = password => (dispatch, getState) => { + dispatch({ type: LOGIN, password }) + try { + const currentConfig = settingsSelectors.currentConfig(getState()) + const accountPassword = currentConfig.password.value + if (accountPassword === password) { + dispatch({ type: LOGIN_SUCCESS }) + } else { + throw new Error(getIntl().formatMessage(messages.account_invalid_password)) + } + } catch (error) { + dispatch({ type: LOGIN_FAILURE, error: error.message }) + } +} + +/** + * clearVerifyUserError - Clear verify user error. + * + * @returns {object} Action + */ +export const clearLoginError = () => (dispatch, getState) => { + if (accountSelectors.loginError(getState())) { + dispatch({ + type: 'LOGIN_CLEAR_ERROR', + }) + } +} + // ------------------------------------ // Action Handlers // ------------------------------------ @@ -65,6 +118,22 @@ const ACTION_HANDLERS = { state.isAccountLoading = false state.initAccountError = error }, + + [LOGIN]: state => { + state.isLoggingIn = true + }, + [LOGIN_SUCCESS]: state => { + state.isLoggingIn = false + state.loginError = null + state.isLoggedIn = true + }, + [LOGIN_FAILURE]: (state, { error }) => { + state.isLoggingIn = false + state.loginError = error + }, + [LOGIN_CLEAR_ERROR]: state => { + state.loginError = null + }, } // ------------------------------------ @@ -75,12 +144,20 @@ const accountSelector = state => state.account.account const isAccountLoadingSelector = state => state.account.isAccountLoading const initAccountErrorSelector = state => state.exchange.initAccountError +const isLoggingInSelector = state => state.account.isLoggingIn +const isLoggedInSelector = state => state.account.isLoggedIn +const loginErrorSelector = state => state.account.loginError + const accountSelectors = {} accountSelectors.account = accountSelector accountSelectors.isAccountLoading = isAccountLoadingSelector accountSelectors.initAccountError = initAccountErrorSelector +accountSelectors.isLoggingIn = isLoggingInSelector +accountSelectors.isLoggedIn = isLoggedInSelector +accountSelectors.loginError = loginErrorSelector + export { accountSelectors } // ------------------------------------ diff --git a/renderer/reducers/messages/messages.js b/renderer/reducers/messages/messages.js index d88d7039198..8fcb4e91b23 100644 --- a/renderer/reducers/messages/messages.js +++ b/renderer/reducers/messages/messages.js @@ -4,6 +4,7 @@ import { defineMessages } from 'react-intl' /* eslint-disable max-len */ export default defineMessages({ + account_invalid_password: 'Invalid password', backup_import_success: 'Wallet backup imported successfully', backup_import_error: 'Backup import has failed: {error} ', backup_not_found_error: 'Unable to find backup file ',