Skip to content

Commit 658c2b1

Browse files
Merge branch 'develop' into pagetitles2
2 parents a1f1e42 + b52fc0b commit 658c2b1

File tree

6 files changed

+124
-112
lines changed

6 files changed

+124
-112
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"react-fast-compare": "^3.2.0",
2727
"react-google-recaptcha": "^2.0.1",
2828
"react-helmet": "^6.1.0",
29+
"react-ga": "^3.1.2",
2930
"react-hot-loader": "^4.12.21",
3031
"react-i18next": "^11.2.1",
3132
"react-redux": "^7.1.0",

resources/scripts/api/auth/requestPasswordResetEmail.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import http from '@/api/http';
22

3-
export default (email: string): Promise<string> => {
3+
export default (email: string, recaptchaData?: string): Promise<string> => {
44
return new Promise((resolve, reject) => {
5-
http.post('/auth/password', { email })
5+
http.post('/auth/password', { email, 'g-recaptcha-response': recaptchaData })
66
.then(response => resolve(response.data.status || ''))
77
.catch(reject);
88
});

resources/scripts/components/auth/ForgotPasswordContainer.tsx

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
11
import * as React from 'react';
2+
import { useRef, useState } from 'react';
23
import { Link } from 'react-router-dom';
34
import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail';
45
import { httpErrorToHuman } from '@/api/http';
56
import LoginFormContainer from '@/components/auth/LoginFormContainer';
6-
import { Actions, useStoreActions } from 'easy-peasy';
7-
import { ApplicationStore } from '@/state';
7+
import { useStoreState } from 'easy-peasy';
88
import Field from '@/components/elements/Field';
99
import { Formik, FormikHelpers } from 'formik';
1010
import { object, string } from 'yup';
1111
import tw from 'twin.macro';
1212
import Button from '@/components/elements/Button';
13+
import Reaptcha from 'reaptcha';
14+
import useFlash from '@/plugins/useFlash';
1315

1416
interface Values {
1517
email: string;
1618
}
1719

1820
export default () => {
19-
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
21+
const ref = useRef<Reaptcha>(null);
22+
const [ token, setToken ] = useState('');
23+
24+
const { clearFlashes, addFlash } = useFlash();
25+
const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha);
2026

2127
const handleSubmission = ({ email }: Values, { setSubmitting, resetForm }: FormikHelpers<Values>) => {
22-
setSubmitting(true);
2328
clearFlashes();
24-
requestPasswordResetEmail(email)
29+
30+
// If there is no token in the state yet, request the token and then abort this submit request
31+
// since it will be re-submitted when the recaptcha data is returned by the component.
32+
if (recaptchaEnabled && !token) {
33+
ref.current!.execute().catch(error => console.error(error));
34+
return;
35+
}
36+
37+
requestPasswordResetEmail(email, token)
2538
.then(response => {
2639
resetForm();
2740
addFlash({ type: 'success', title: 'Success', message: response });
@@ -42,7 +55,7 @@ export default () => {
4255
.required('A valid email address must be provided to continue.'),
4356
})}
4457
>
45-
{({ isSubmitting }) => (
58+
{({ isSubmitting, setSubmitting, submitForm }) => (
4659
<LoginFormContainer
4760
title={'Request Password Reset'}
4861
css={tw`w-full flex`}
@@ -64,6 +77,21 @@ export default () => {
6477
Send Email
6578
</Button>
6679
</div>
80+
{recaptchaEnabled &&
81+
<Reaptcha
82+
ref={ref}
83+
size={'invisible'}
84+
sitekey={siteKey || '_invalid_key'}
85+
onVerify={response => {
86+
setToken(response);
87+
submitForm();
88+
}}
89+
onExpire={() => {
90+
setSubmitting(false);
91+
setToken('');
92+
}}
93+
/>
94+
}
6795
<div css={tw`mt-6 text-center`}>
6896
<Link
6997
type={'button'}
Lines changed: 85 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,132 +1,115 @@
1-
import React, { useRef } from 'react';
1+
import React, { useRef, useState } from 'react';
22
import { Link, RouteComponentProps } from 'react-router-dom';
3-
import login, { LoginData } from '@/api/auth/login';
3+
import login from '@/api/auth/login';
44
import LoginFormContainer from '@/components/auth/LoginFormContainer';
5-
import { ActionCreator, Actions, useStoreActions, useStoreState } from 'easy-peasy';
6-
import { ApplicationStore } from '@/state';
7-
import { FormikProps, withFormik } from 'formik';
5+
import { useStoreState } from 'easy-peasy';
6+
import { Formik, FormikHelpers } from 'formik';
87
import { object, string } from 'yup';
98
import Field from '@/components/elements/Field';
10-
import { httpErrorToHuman } from '@/api/http';
11-
import { FlashMessage } from '@/state/flashes';
12-
import ReCAPTCHA from 'react-google-recaptcha';
139
import tw from 'twin.macro';
1410
import Button from '@/components/elements/Button';
11+
import Reaptcha from 'reaptcha';
12+
import useFlash from '@/plugins/useFlash';
1513

16-
type OwnProps = RouteComponentProps & {
17-
clearFlashes: ActionCreator<void>;
18-
addFlash: ActionCreator<FlashMessage>;
14+
interface Values {
15+
username: string;
16+
password: string;
1917
}
2018

21-
const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handleSubmit }: OwnProps & FormikProps<LoginData>) => {
22-
const ref = useRef<ReCAPTCHA | null>(null);
23-
const { enabled: recaptchaEnabled, siteKey } = useStoreState<ApplicationStore, any>(state => state.settings.data!.recaptcha);
19+
const LoginContainer = ({ history }: RouteComponentProps) => {
20+
const ref = useRef<Reaptcha>(null);
21+
const [ token, setToken ] = useState('');
2422

25-
const submit = (e: React.FormEvent<HTMLFormElement>) => {
26-
e.preventDefault();
23+
const { clearFlashes, clearAndAddHttpError } = useFlash();
24+
const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha);
2725

28-
if (ref.current && !values.recaptchaData) {
29-
return ref.current.execute();
30-
}
31-
32-
handleSubmit(e);
33-
};
34-
35-
return (
36-
<React.Fragment>
37-
{ref.current && ref.current.render()}
38-
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`} onSubmit={submit}>
39-
<Field
40-
type={'text'}
41-
label={'Username or Email'}
42-
id={'username'}
43-
name={'username'}
44-
light
45-
/>
46-
<div css={tw`mt-6`}>
47-
<Field
48-
type={'password'}
49-
label={'Password'}
50-
id={'password'}
51-
name={'password'}
52-
light
53-
/>
54-
</div>
55-
<div css={tw`mt-6`}>
56-
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting}>
57-
Login
58-
</Button>
59-
</div>
60-
{recaptchaEnabled &&
61-
<ReCAPTCHA
62-
ref={ref}
63-
size={'invisible'}
64-
sitekey={siteKey || '_invalid_key'}
65-
onChange={token => {
66-
ref.current && ref.current.reset();
67-
setFieldValue('recaptchaData', token);
68-
submitForm();
69-
}}
70-
onExpired={() => setFieldValue('recaptchaData', null)}
71-
/>
72-
}
73-
<div css={tw`mt-6 text-center`}>
74-
<Link
75-
to={'/auth/password'}
76-
css={tw`text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600`}
77-
>
78-
Forgot password?
79-
</Link>
80-
</div>
81-
</LoginFormContainer>
82-
</React.Fragment>
83-
);
84-
};
26+
const onSubmit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
27+
clearFlashes();
8528

86-
const EnhancedForm = withFormik<OwnProps, LoginData>({
87-
displayName: 'LoginContainerForm',
88-
89-
mapPropsToValues: () => ({
90-
username: '',
91-
password: '',
92-
recaptchaData: null,
93-
}),
94-
95-
validationSchema: () => object().shape({
96-
username: string().required('A username or email must be provided.'),
97-
password: string().required('Please enter your account password.'),
98-
}),
29+
// If there is no token in the state yet, request the token and then abort this submit request
30+
// since it will be re-submitted when the recaptcha data is returned by the component.
31+
if (recaptchaEnabled && !token) {
32+
ref.current!.execute().catch(error => console.error(error));
33+
return;
34+
}
9935

100-
handleSubmit: (values, { props, setFieldValue, setSubmitting }) => {
101-
props.clearFlashes();
102-
login(values)
36+
login({ ...values, recaptchaData: token })
10337
.then(response => {
10438
if (response.complete) {
10539
// @ts-ignore
10640
window.location = response.intended || '/';
10741
return;
10842
}
10943

110-
props.history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
44+
history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
11145
})
11246
.catch(error => {
11347
console.error(error);
11448

11549
setSubmitting(false);
116-
setFieldValue('recaptchaData', null);
117-
props.addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
50+
clearAndAddHttpError({ error });
11851
});
119-
},
120-
})(LoginContainer);
121-
122-
export default (props: RouteComponentProps) => {
123-
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
52+
};
12453

12554
return (
126-
<EnhancedForm
127-
{...props}
128-
addFlash={addFlash}
129-
clearFlashes={clearFlashes}
130-
/>
55+
<Formik
56+
onSubmit={onSubmit}
57+
initialValues={{ username: '', password: '' }}
58+
validationSchema={object().shape({
59+
username: string().required('A username or email must be provided.'),
60+
password: string().required('Please enter your account password.'),
61+
})}
62+
>
63+
{({ isSubmitting, setSubmitting, submitForm }) => (
64+
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`}>
65+
<Field
66+
type={'text'}
67+
label={'Username or Email'}
68+
id={'username'}
69+
name={'username'}
70+
light
71+
/>
72+
<div css={tw`mt-6`}>
73+
<Field
74+
type={'password'}
75+
label={'Password'}
76+
id={'password'}
77+
name={'password'}
78+
light
79+
/>
80+
</div>
81+
<div css={tw`mt-6`}>
82+
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting}>
83+
Login
84+
</Button>
85+
</div>
86+
{recaptchaEnabled &&
87+
<Reaptcha
88+
ref={ref}
89+
size={'invisible'}
90+
sitekey={siteKey || '_invalid_key'}
91+
onVerify={response => {
92+
setToken(response);
93+
submitForm();
94+
}}
95+
onExpire={() => {
96+
setSubmitting(false);
97+
setToken('');
98+
}}
99+
/>
100+
}
101+
<div css={tw`mt-6 text-center`}>
102+
<Link
103+
to={'/auth/password'}
104+
css={tw`text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600`}
105+
>
106+
Forgot password?
107+
</Link>
108+
</div>
109+
</LoginFormContainer>
110+
)}
111+
</Formik>
131112
);
132113
};
114+
115+
export default LoginContainer;

resources/scripts/state/flashes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export interface FlashStore {
66
items: FlashMessage[];
77
addFlash: Action<FlashStore, FlashMessage>;
88
addError: Action<FlashStore, { message: string; key?: string }>;
9-
clearAndAddHttpError: Action<FlashStore, { error: any, key: string }>;
9+
clearAndAddHttpError: Action<FlashStore, { error: any, key?: string }>;
1010
clearFlashes: Action<FlashStore, string | void>;
1111
}
1212

routes/auth.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
// Password reset routes. This endpoint is hit after going through
2727
// the forgot password routes to acquire a token (or after an account
2828
// is created).
29-
Route::post('/password/reset', 'ResetPasswordController')->name('auth.reset-password')->middleware('recaptcha');
29+
Route::post('/password/reset', 'ResetPasswordController')->name('auth.reset-password');
3030

3131
// Catch any other combinations of routes and pass them off to the Vuejs component.
3232
Route::fallback('LoginController@index');

0 commit comments

Comments
 (0)