Fix recaptcha handling during login & password reset flows; closes #2064
This commit is contained in:
parent
dd381f65a9
commit
b52fc0b4d9
7 changed files with 131 additions and 136 deletions
|
@ -1,105 +1,39 @@
|
|||
import React, { useRef } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||
import login, { LoginData } from '@/api/auth/login';
|
||||
import login from '@/api/auth/login';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import { ActionCreator, Actions, useStoreActions, useStoreState } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { FormikProps, withFormik } from 'formik';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { object, string } from 'yup';
|
||||
import Field from '@/components/elements/Field';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import { FlashMessage } from '@/state/flashes';
|
||||
import ReCAPTCHA from 'react-google-recaptcha';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import Reaptcha from 'reaptcha';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
|
||||
type OwnProps = RouteComponentProps & {
|
||||
clearFlashes: ActionCreator<void>;
|
||||
addFlash: ActionCreator<FlashMessage>;
|
||||
interface Values {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handleSubmit }: OwnProps & FormikProps<LoginData>) => {
|
||||
const ref = useRef<ReCAPTCHA | null>(null);
|
||||
const { enabled: recaptchaEnabled, siteKey } = useStoreState<ApplicationStore, any>(state => state.settings.data!.recaptcha);
|
||||
const LoginContainer = ({ history }: RouteComponentProps) => {
|
||||
const ref = useRef<Reaptcha>(null);
|
||||
const [ token, setToken ] = useState('');
|
||||
|
||||
const submit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha);
|
||||
|
||||
if (ref.current && !values.recaptchaData) {
|
||||
return ref.current.execute();
|
||||
const onSubmit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
clearFlashes();
|
||||
|
||||
// If there is no token in the state yet, request the token and then abort this submit request
|
||||
// since it will be re-submitted when the recaptcha data is returned by the component.
|
||||
if (recaptchaEnabled && !token) {
|
||||
ref.current!.execute().catch(error => console.error(error));
|
||||
return;
|
||||
}
|
||||
|
||||
handleSubmit(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ref.current && ref.current.render()}
|
||||
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`} onSubmit={submit}>
|
||||
<Field
|
||||
type={'text'}
|
||||
label={'Username or Email'}
|
||||
id={'username'}
|
||||
name={'username'}
|
||||
light
|
||||
/>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
type={'password'}
|
||||
label={'Password'}
|
||||
id={'password'}
|
||||
name={'password'}
|
||||
light
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting}>
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
{recaptchaEnabled &&
|
||||
<ReCAPTCHA
|
||||
ref={ref}
|
||||
size={'invisible'}
|
||||
sitekey={siteKey || '_invalid_key'}
|
||||
onChange={token => {
|
||||
ref.current && ref.current.reset();
|
||||
setFieldValue('recaptchaData', token);
|
||||
submitForm();
|
||||
}}
|
||||
onExpired={() => setFieldValue('recaptchaData', null)}
|
||||
/>
|
||||
}
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
<Link
|
||||
to={'/auth/password'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600`}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const EnhancedForm = withFormik<OwnProps, LoginData>({
|
||||
displayName: 'LoginContainerForm',
|
||||
|
||||
mapPropsToValues: () => ({
|
||||
username: '',
|
||||
password: '',
|
||||
recaptchaData: null,
|
||||
}),
|
||||
|
||||
validationSchema: () => object().shape({
|
||||
username: string().required('A username or email must be provided.'),
|
||||
password: string().required('Please enter your account password.'),
|
||||
}),
|
||||
|
||||
handleSubmit: (values, { props, setFieldValue, setSubmitting }) => {
|
||||
props.clearFlashes();
|
||||
login(values)
|
||||
login({ ...values, recaptchaData: token })
|
||||
.then(response => {
|
||||
if (response.complete) {
|
||||
// @ts-ignore
|
||||
|
@ -107,26 +41,75 @@ const EnhancedForm = withFormik<OwnProps, LoginData>({
|
|||
return;
|
||||
}
|
||||
|
||||
props.history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
|
||||
history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
|
||||
setSubmitting(false);
|
||||
setFieldValue('recaptchaData', null);
|
||||
props.addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
|
||||
clearAndAddHttpError({ error });
|
||||
});
|
||||
},
|
||||
})(LoginContainer);
|
||||
|
||||
export default (props: RouteComponentProps) => {
|
||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
};
|
||||
|
||||
return (
|
||||
<EnhancedForm
|
||||
{...props}
|
||||
addFlash={addFlash}
|
||||
clearFlashes={clearFlashes}
|
||||
/>
|
||||
<Formik
|
||||
onSubmit={onSubmit}
|
||||
initialValues={{ username: '', password: '' }}
|
||||
validationSchema={object().shape({
|
||||
username: string().required('A username or email must be provided.'),
|
||||
password: string().required('Please enter your account password.'),
|
||||
})}
|
||||
>
|
||||
{({ isSubmitting, setSubmitting, submitForm }) => (
|
||||
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`}>
|
||||
<Field
|
||||
type={'text'}
|
||||
label={'Username or Email'}
|
||||
id={'username'}
|
||||
name={'username'}
|
||||
light
|
||||
/>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
type={'password'}
|
||||
label={'Password'}
|
||||
id={'password'}
|
||||
name={'password'}
|
||||
light
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting}>
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
{recaptchaEnabled &&
|
||||
<Reaptcha
|
||||
ref={ref}
|
||||
size={'invisible'}
|
||||
sitekey={siteKey || '_invalid_key'}
|
||||
onVerify={response => {
|
||||
setToken(response);
|
||||
submitForm();
|
||||
}}
|
||||
onExpire={() => {
|
||||
setSubmitting(false);
|
||||
setToken('');
|
||||
}}
|
||||
/>
|
||||
}
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
<Link
|
||||
to={'/auth/password'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600`}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginContainer;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue