diff --git a/package.json b/package.json index 3a81f98fa..0ca8c2e9b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "@fortawesome/fontawesome-svg-core": "1.2.19", "@fortawesome/free-solid-svg-icons": "^5.9.0", "@fortawesome/react-fontawesome": "0.1.4", - "@types/react-google-recaptcha": "^1.1.1", "axios": "^0.19.2", "ayu-ace": "^2.0.4", "brace": "^0.11.1", @@ -23,15 +22,15 @@ "path": "^0.12.7", "query-string": "^6.7.0", "react": "^16.13.1", - "react-ga": "^3.1.2", "react-dom": "npm:@hot-loader/react-dom", "react-fast-compare": "^3.2.0", - "react-google-recaptcha": "^2.0.1", + "react-ga": "^3.1.2", "react-hot-loader": "^4.12.21", "react-i18next": "^11.2.1", "react-redux": "^7.1.0", "react-router-dom": "^5.1.2", "react-transition-group": "^4.4.1", + "reaptcha": "^1.7.2", "sockette": "^2.0.6", "styled-components": "^5.1.1", "styled-components-breakpoint": "^3.0.0-preview.20", diff --git a/resources/scripts/api/auth/requestPasswordResetEmail.ts b/resources/scripts/api/auth/requestPasswordResetEmail.ts index d70139899..2168160c2 100644 --- a/resources/scripts/api/auth/requestPasswordResetEmail.ts +++ b/resources/scripts/api/auth/requestPasswordResetEmail.ts @@ -1,8 +1,8 @@ import http from '@/api/http'; -export default (email: string): Promise => { +export default (email: string, recaptchaData?: string): Promise => { return new Promise((resolve, reject) => { - http.post('/auth/password', { email }) + http.post('/auth/password', { email, 'g-recaptcha-response': recaptchaData }) .then(response => resolve(response.data.status || '')) .catch(reject); }); diff --git a/resources/scripts/components/auth/ForgotPasswordContainer.tsx b/resources/scripts/components/auth/ForgotPasswordContainer.tsx index dbd4ed469..82bd5e5ff 100644 --- a/resources/scripts/components/auth/ForgotPasswordContainer.tsx +++ b/resources/scripts/components/auth/ForgotPasswordContainer.tsx @@ -1,27 +1,40 @@ import * as React from 'react'; +import { useRef, useState } from 'react'; import { Link } from 'react-router-dom'; import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail'; import { httpErrorToHuman } from '@/api/http'; import LoginFormContainer from '@/components/auth/LoginFormContainer'; -import { Actions, useStoreActions } from 'easy-peasy'; -import { ApplicationStore } from '@/state'; +import { useStoreState } from 'easy-peasy'; import Field from '@/components/elements/Field'; import { Formik, FormikHelpers } from 'formik'; import { object, string } from 'yup'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; +import Reaptcha from 'reaptcha'; +import useFlash from '@/plugins/useFlash'; interface Values { email: string; } export default () => { - const { clearFlashes, addFlash } = useStoreActions((actions: Actions) => actions.flashes); + const ref = useRef(null); + const [ token, setToken ] = useState(''); + + const { clearFlashes, addFlash } = useFlash(); + const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha); const handleSubmission = ({ email }: Values, { setSubmitting, resetForm }: FormikHelpers) => { - setSubmitting(true); clearFlashes(); - requestPasswordResetEmail(email) + + // 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; + } + + requestPasswordResetEmail(email, token) .then(response => { resetForm(); addFlash({ type: 'success', title: 'Success', message: response }); @@ -42,7 +55,7 @@ export default () => { .required('A valid email address must be provided to continue.'), })} > - {({ isSubmitting }) => ( + {({ isSubmitting, setSubmitting, submitForm }) => ( { Send Email + {recaptchaEnabled && + { + setToken(response); + submitForm(); + }} + onExpire={() => { + setSubmitting(false); + setToken(''); + }} + /> + }
; - addFlash: ActionCreator; +interface Values { + username: string; + password: string; } -const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handleSubmit }: OwnProps & FormikProps) => { - const ref = useRef(null); - const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha); +const LoginContainer = ({ history }: RouteComponentProps) => { + const ref = useRef(null); + const [ token, setToken ] = useState(''); - const submit = (e: React.FormEvent) => { - 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) => { + 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 ( - - {ref.current && ref.current.render()} - - -
- -
-
- -
- {recaptchaEnabled && - { - ref.current && ref.current.reset(); - setFieldValue('recaptchaData', token); - submitForm(); - }} - onExpired={() => setFieldValue('recaptchaData', null)} - /> - } -
- - Forgot password? - -
-
-
- ); -}; - -const EnhancedForm = withFormik({ - 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({ 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) => actions.flashes); + }; return ( - + + {({ isSubmitting, setSubmitting, submitForm }) => ( + + +
+ +
+
+ +
+ {recaptchaEnabled && + { + setToken(response); + submitForm(); + }} + onExpire={() => { + setSubmitting(false); + setToken(''); + }} + /> + } +
+ + Forgot password? + +
+
+ )} +
); }; + +export default LoginContainer; diff --git a/resources/scripts/state/flashes.ts b/resources/scripts/state/flashes.ts index 8e4fb258e..fb89a0a8d 100644 --- a/resources/scripts/state/flashes.ts +++ b/resources/scripts/state/flashes.ts @@ -6,7 +6,7 @@ export interface FlashStore { items: FlashMessage[]; addFlash: Action; addError: Action; - clearAndAddHttpError: Action; + clearAndAddHttpError: Action; clearFlashes: Action; } diff --git a/routes/auth.php b/routes/auth.php index a6038447b..4bdb72206 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -26,7 +26,7 @@ Route::group(['middleware' => 'guest'], function () { // Password reset routes. This endpoint is hit after going through // the forgot password routes to acquire a token (or after an account // is created). - Route::post('/password/reset', 'ResetPasswordController')->name('auth.reset-password')->middleware('recaptcha'); + Route::post('/password/reset', 'ResetPasswordController')->name('auth.reset-password'); // Catch any other combinations of routes and pass them off to the Vuejs component. Route::fallback('LoginController@index'); diff --git a/yarn.lock b/yarn.lock index f20fef049..73e239aef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1013,12 +1013,6 @@ dependencies: "@types/react" "*" -"@types/react-google-recaptcha@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/react-google-recaptcha/-/react-google-recaptcha-1.1.1.tgz#7dd2a4dd15d38d8059a2753cd4a7e3485c9bb3ea" - dependencies: - "@types/react" "*" - "@types/react-native@*": version "0.60.2" resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.60.2.tgz#2dca78481a904419c2a5907288dd97d1090c6e3c" @@ -5399,7 +5393,7 @@ promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" -prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.10, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -5544,13 +5538,6 @@ rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-async-script@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.1.1.tgz#f481c6c5f094bf4b94a9d52da0d0dda2e1a74bdf" - dependencies: - hoist-non-react-statics "^3.3.0" - prop-types "^15.5.0" - "react-dom@npm:@hot-loader/react-dom": version "16.11.0" resolved "https://registry.yarnpkg.com/@hot-loader/react-dom/-/react-dom-16.11.0.tgz#c0b483923b289db5431516f56ee2a69448ebf9bd" @@ -5574,13 +5561,6 @@ react-ga@^3.1.2: resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-3.1.2.tgz#e13f211c51a2e5c401ea69cf094b9501fe3c51ce" integrity sha512-OJrMqaHEHbodm+XsnjA6ISBEHTwvpFrxco65mctzl/v3CASMSLSyUkFqz9yYrPDKGBUfNQzKCjuMJwctjlWBbw== -react-google-recaptcha@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz#3276b29659493f7ca2a5b7739f6c239293cdf1d8" - dependencies: - prop-types "^15.5.0" - react-async-script "^1.1.1" - react-hot-loader@^4.12.21: version "4.12.21" resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.21.tgz#332e830801fb33024b5a147d6b13417f491eb975" @@ -5719,6 +5699,11 @@ readdirp@~3.4.0: dependencies: picomatch "^2.2.1" +reaptcha@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/reaptcha/-/reaptcha-1.7.2.tgz#d829f54270c241f46501e92a5a7badeb1fcf372d" + integrity sha512-/RXiPeMd+fPUGByv+kAaQlCXCsSflZ9bKX5Fcwv9IYGS1oyT2nntL/8zn9IaiUFHL66T1jBtOABcb92g2+3w8w== + reduce-css-calc@^2.1.6: version "2.1.7" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz#1ace2e02c286d78abcd01fd92bfe8097ab0602c2"