From aaad7aa86f61a8c86b271ce6ec63f9488c02ab8e Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 7 Feb 2018 22:10:05 +0100 Subject: [PATCH] Reimplement recaptcha --- client/coral-admin/src/actions/config.js | 7 --- client/coral-admin/src/components/SignIn.js | 27 ++++++++-- client/coral-admin/src/containers/Layout.js | 10 ---- client/coral-admin/src/containers/SignIn.js | 12 ++++- client/coral-admin/src/reducers/config.js | 17 ------ client/coral-admin/src/reducers/index.js | 2 - .../coral-framework/components/Recaptcha.js | 52 +++++++++++++++++++ client/coral-framework/components/Slot.js | 3 +- client/coral-framework/hocs/withSignIn.js | 10 +++- client/coral-framework/services/bootstrap.js | 2 +- client/coral-framework/services/plugins.js | 5 +- 11 files changed, 100 insertions(+), 47 deletions(-) delete mode 100644 client/coral-admin/src/actions/config.js delete mode 100644 client/coral-admin/src/reducers/config.js create mode 100644 client/coral-framework/components/Recaptcha.js diff --git a/client/coral-admin/src/actions/config.js b/client/coral-admin/src/actions/config.js deleted file mode 100644 index 5e2995e85..000000000 --- a/client/coral-admin/src/actions/config.js +++ /dev/null @@ -1,7 +0,0 @@ -export const CONFIG_UPDATED = 'CONFIG_UPDATED'; - -export const fetchConfig = () => dispatch => { - let json = document.getElementById('data'); - let data = JSON.parse(json.textContent); - dispatch({ type: CONFIG_UPDATED, data }); -}; diff --git a/client/coral-admin/src/components/SignIn.js b/client/coral-admin/src/components/SignIn.js index 71f14d12a..85bfb9810 100644 --- a/client/coral-admin/src/components/SignIn.js +++ b/client/coral-admin/src/components/SignIn.js @@ -2,11 +2,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import styles from './SignIn.css'; import { Button, TextField, Alert } from 'coral-ui'; +import Recaptcha from 'coral-framework/components/Recaptcha'; class SignIn extends React.Component { - constructor(props) { - super(props); - } + recaptcha = null; handleForgotPasswordLink = e => { e.preventDefault(); @@ -18,10 +17,23 @@ class SignIn extends React.Component { handleSubmit = e => { e.preventDefault(); this.props.onSubmit(); + + // Reset recaptcha because each response can only + // be used once. + if (this.recaptcha) { + this.recaptcha.reset(); + } + }; + + handleRecaptchaRef = ref => { + this.recaptcha = ref; + setTimeout(() => { + console.log(ref); + }, 1000) }; render() { - const { email, password, errorMessage } = this.props; + const { email, password, errorMessage, requireRecaptcha } = this.props; return (
{errorMessage && {errorMessage}} @@ -57,6 +69,12 @@ class SignIn extends React.Component { Request a new one.

+ {requireRecaptcha && ( + + )} ); } @@ -68,6 +86,7 @@ SignIn.propTypes = { onEmailChange: PropTypes.func.isRequired, onPasswordChange: PropTypes.func.isRequired, onForgotPasswordLink: PropTypes.func.isRequired, + onRecaptchaVerify: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired, errorMessage: PropTypes.string.isRequired, requireRecaptcha: PropTypes.bool.isRequired, diff --git a/client/coral-admin/src/containers/Layout.js b/client/coral-admin/src/containers/Layout.js index ea98000b8..484467936 100644 --- a/client/coral-admin/src/containers/Layout.js +++ b/client/coral-admin/src/containers/Layout.js @@ -2,7 +2,6 @@ import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import Layout from '../components/Layout'; -import { fetchConfig } from '../actions/config'; import Login from '../containers/Login'; import { FullLoading } from '../components/FullLoading'; import BanUserDialog from './BanUserDialog'; @@ -14,12 +13,6 @@ import UserDetail from 'coral-admin/src/containers/UserDetail'; import PropTypes from 'prop-types'; class LayoutContainer extends React.Component { - componentWillMount() { - const { fetchConfig } = this.props; - - fetchConfig(); - } - render() { const { currentUser, @@ -72,8 +65,6 @@ LayoutContainer.propTypes = { checkedInitialLogin: PropTypes.bool, logout: PropTypes.func, toggleShortcutModal: PropTypes.func, - TALK_RECAPTCHA_PUBLIC: PropTypes.string, - fetchConfig: PropTypes.func, }; const mapStateToProps = state => ({ @@ -84,7 +75,6 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => bindActionCreators( { - fetchConfig, toggleShortcutModal, logout, }, diff --git a/client/coral-admin/src/containers/SignIn.js b/client/coral-admin/src/containers/SignIn.js index ab7886c3a..02dc8ba01 100644 --- a/client/coral-admin/src/containers/SignIn.js +++ b/client/coral-admin/src/containers/SignIn.js @@ -8,10 +8,15 @@ class SignInContainer extends Component { state = { email: '', password: '', + recaptchaResponse: '', }; handleSubmit = () => { - this.props.signIn(this.state.email, this.state.password); + this.props.signIn( + this.state.email, + this.state.password, + this.state.recaptchaResponse + ); }; handleEmailChange = email => { @@ -22,6 +27,10 @@ class SignInContainer extends Component { this.setState({ password }); }; + handleRecaptchaVerify = recaptchaResponse => { + this.setState({ recaptchaResponse }); + }; + render() { return ( ); diff --git a/client/coral-admin/src/reducers/config.js b/client/coral-admin/src/reducers/config.js deleted file mode 100644 index 0588f0a13..000000000 --- a/client/coral-admin/src/reducers/config.js +++ /dev/null @@ -1,17 +0,0 @@ -import * as actions from '../actions/config'; - -const initialState = { - data: {}, -}; - -export default function config(state = initialState, action) { - switch (action.type) { - case actions.CONFIG_UPDATED: - return { - ...state, - data: action.data, - }; - default: - return state; - } -} diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js index 11801a379..9175698cf 100644 --- a/client/coral-admin/src/reducers/index.js +++ b/client/coral-admin/src/reducers/index.js @@ -4,7 +4,6 @@ import configure from './configure'; import community from './community'; import moderation from './moderation'; import install from './install'; -import config from './config'; import banUserDialog from './banUserDialog'; import suspendUserDialog from './suspendUserDialog'; import userDetail from './userDetail'; @@ -20,6 +19,5 @@ export default { community, moderation, install, - config, ui, }; diff --git a/client/coral-framework/components/Recaptcha.js b/client/coral-framework/components/Recaptcha.js new file mode 100644 index 000000000..f0825df30 --- /dev/null +++ b/client/coral-framework/components/Recaptcha.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactRecaptcha from 'react-recaptcha'; + +class Recaptcha extends React.Component { + static contextTypes = { + store: PropTypes.object, + }; + + ref = null; + + handleRef = ref => { + this.ref = ref; + }; + + reset = () => this.ref.reset(); + + getSiteKey() { + // This should be fine because it's static and will never change. + // Prefer this to connect HOC because wie expose the instance method + // `reset` + return this.context.store.getState().static.TALK_RECAPTCHA_PUBLIC; + } + + render() { + return ( + + ); + } +} + +Recaptcha.defaultProps = { + render: 'explicit', + theme: 'dark', +}; + +Recaptcha.propTypes = { + onLoad: PropTypes.func, + onVerify: PropTypes.func.isRequired, + theme: PropTypes.string, + render: PropTypes.string, + sitekey: PropTypes.string.isRequired, +}; + +export default Recaptcha; diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index af524b21d..fe63b4f30 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import kebabCase from 'lodash/kebabCase'; import PropTypes from 'prop-types'; import isEqual from 'lodash/isEqual'; +import get from 'lodash/get'; import { getShallowChanges } from 'coral-framework/utils'; const emptyConfig = {}; @@ -68,7 +69,7 @@ class Slot extends React.Component { } = this.props; const { plugins } = this.context; let children = this.getChildren(); - const pluginConfig = reduxState.config.pluginConfig || emptyConfig; + const pluginConfig = get(reduxState, 'config.pluginConfig') || emptyConfig; if (children.length === 0 && DefaultComponent) { const props = plugins.getSlotComponentProps( DefaultComponent, diff --git a/client/coral-framework/hocs/withSignIn.js b/client/coral-framework/hocs/withSignIn.js index a725f01e9..02c631068 100644 --- a/client/coral-framework/hocs/withSignIn.js +++ b/client/coral-framework/hocs/withSignIn.js @@ -18,6 +18,7 @@ export default hoistStatics(WrappedComponent => { state = { error: null, loading: false, + requireRecaptcha: false, }; signIn = (email, password, recaptchaResponse) => { @@ -45,7 +46,12 @@ export default hoistStatics(WrappedComponent => { if (!error.status || error.status !== 401) { console.error(error); } - this.setState({ loading: false, error }); + const changeSet = { loading: false, error }; + if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') { + changeSet.requireRecaptcha = !!this.context.store.getState().static + .TALK_RECAPTCHA_PUBLIC; + } + this.setState(changeSet); }); }; @@ -65,7 +71,7 @@ export default hoistStatics(WrappedComponent => { signIn={this.signIn} loading={this.state.loading} errorMessage={this.getErrorMessage()} - requireRecaptcha={false} + requireRecaptcha={this.state.requireRecaptcha} /> ); } diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index 9b5982edc..fa1f907af 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -35,7 +35,7 @@ import { setStaticConfiguration } from '../actions/static'; const getAuthToken = (store, storage) => { let state = store.getState(); - if (state.config.auth_token) { + if (state.config && state.config.auth_token) { // if an auth_token exists in config, use it. return state.config.auth_token; } else if (!bowser.safari && !bowser.ios && storage) { diff --git a/client/coral-framework/services/plugins.js b/client/coral-framework/services/plugins.js index 8f9fa8080..6f99a2291 100644 --- a/client/coral-framework/services/plugins.js +++ b/client/coral-framework/services/plugins.js @@ -6,6 +6,7 @@ import flattenDeep from 'lodash/flattenDeep'; import isEmpty from 'lodash/isEmpty'; import flatten from 'lodash/flatten'; import mapValues from 'lodash/mapValues'; +import get from 'lodash/get'; import { getDisplayName } from 'coral-framework/helpers/hoc'; import camelize from '../helpers/camelize'; @@ -83,7 +84,7 @@ class PluginsService { * query datas are only passed to the component if it is defined in `component.fragments`. */ getSlotComponentProps(component, reduxState, props, queryData) { - const pluginConfig = reduxState.config.plugin_config || emptyConfig; + const pluginConfig = get(reduxState, 'config.plugin_config') || emptyConfig; return { ...props, config: pluginConfig, @@ -97,7 +98,7 @@ class PluginsService { * Returns React Elements for given slot. */ getSlotElements(slot, reduxState, props = {}, queryData = {}) { - const pluginConfig = reduxState.config.plugin_config || emptyConfig; + const pluginConfig = get(reduxState, 'config.plugin_config') || emptyConfig; const isDisabled = component => { if (