diff --git a/package.json b/package.json index 6b1d138e..30eb76d5 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@fortawesome/free-brands-svg-icons": "5.15.2", "@fortawesome/free-solid-svg-icons": "5.15.2", "@fortawesome/react-fontawesome": "0.1.14", + "@hcaptcha/react-hcaptcha": "0.3.2", "@sentry/react": "6.2.2", "@svgr/webpack": "5.5.0", "@swc/core": "1.2.50", @@ -59,6 +60,7 @@ "@testing-library/jest-dom": "5.11.9", "@testing-library/react": "11.2.5", "@testing-library/user-event": "12.8.3", + "@types/hcaptcha__react-hcaptcha": "0.1.4", "@types/jest": "26.0.20", "@types/node": "14.14.33", "@types/react": "17.0.3", diff --git a/src/pages/FormPage.tsx b/src/pages/FormPage.tsx index 8852ac5d..2022d43f 100644 --- a/src/pages/FormPage.tsx +++ b/src/pages/FormPage.tsx @@ -2,7 +2,8 @@ import { jsx, css } from "@emotion/react"; import { Link } from "react-router-dom"; -import React, { SyntheticEvent, useEffect, useState, createRef } from "react"; +import HCaptcha from "@hcaptcha/react-hcaptcha"; +import React, {SyntheticEvent, useEffect, useState, createRef, useRef} from "react"; import { useParams } from "react-router"; import { PropagateLoader } from "react-spinners"; @@ -143,6 +144,12 @@ const closedHeaderStyles = css` } `; +const captchaStyles = css` + text-align: center; + + margin-bottom: 1.5rem; +`; + function FormPage(): JSX.Element { const { id } = useParams(); @@ -150,6 +157,9 @@ function FormPage(): JSX.Element { const [sending, setSending] = useState(); const [sent, setSent] = useState(); + let captchaToken: string | null = null; + const captchaRef = useRef(null); + useEffect(() => { getForm(id).then(form => { setForm(form); @@ -197,6 +207,9 @@ function FormPage(): JSX.Element { async function handleSubmit(event: SyntheticEvent) { event.preventDefault(); + // Make copy to avoid losing value on state change. + const submitCaptchaToken = captchaToken; + // Client-side required validation const invalidFieldIDs: number[] = []; questions.forEach((prop, i) => { @@ -231,6 +244,11 @@ function FormPage(): JSX.Element { return; } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (!(FormFeatures.DisableAntispam in form!.features) && !submitCaptchaToken && captchaRef && captchaRef.current) { + captchaRef.current.execute(); + } + setSending(true); const answers: { [key: string]: unknown } = {}; @@ -265,7 +283,13 @@ function FormPage(): JSX.Element { } }); - await ApiClient.post(`forms/submit/${id}`, {response: answers}); + const data: { [key: string]: unknown } = {response: answers}; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (!(FormFeatures.DisableAntispam in form!.features)) { + data["captcha"] = submitCaptchaToken; + } + + await ApiClient.post(`forms/submit/${id}`, data); setSending(false); setSent(true); } @@ -284,6 +308,25 @@ function FormPage(): JSX.Element { closed_header =
This form is now closed. You will not be able to submit your response.
; } + let captcha = null; + if (!(FormFeatures.DisableAntispam in form.features) && open) { + captcha = ( +
+ { + captchaToken = token; + }} + onExpire={() => { + captchaToken = null; + }} + ref={captchaRef} + /> +
+ ); + } + return (
@@ -293,6 +336,7 @@ function FormPage(): JSX.Element { { closed_header } { questions } + { captcha }
diff --git a/webpack.config.js b/webpack.config.js index 35c90a74..70c2ae99 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -93,7 +93,8 @@ module.exports = { REACT_APP_SENTRY_DSN: "https://false@notreal.ingest.sentry.io/1234", REACT_APP_OAUTH2_CLIENT_ID: "0", BACKEND_URL: "https://forms-api.pythondiscord.com/", - CONTEXT: "development" + CONTEXT: "development", + HCAPTCHA_SITEKEY: "10000000-ffff-ffff-ffff-000000000001" }), new HtmlWebpackPlugin({ inject: true, template: 'public/index.html' diff --git a/yarn.lock b/yarn.lock index c41676f3..259c1870 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1295,6 +1295,11 @@ dependencies: "@hapi/hoek" "^8.3.0" +"@hcaptcha/react-hcaptcha@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@hcaptcha/react-hcaptcha/-/react-hcaptcha-0.3.2.tgz#8910ea4c111799336fb64de6aa7b74329a7d7579" + integrity sha512-+90hSDwtnKAk3PXJsyABi+wRHS1B+wgWjDO4nz68KpkLnE73rMz/XMdl+ckrwYkFFilzIDKI3o1IqOpMapEwgg== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -2015,6 +2020,13 @@ dependencies: "@types/node" "*" +"@types/hcaptcha__react-hcaptcha@0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@types/hcaptcha__react-hcaptcha/-/hcaptcha__react-hcaptcha-0.1.4.tgz#145cab4f0ac29fe8925dc98ab16cf2dffacad7d5" + integrity sha512-eqEIBR7yn4Y1fRtxPnFlEP8SAHwX762Z27s/ifd5wfJicviz6HynF8gHCgUPPtfYlaQFHa/A0NkwBA5YNmNCmQ== + dependencies: + "@types/react" "*" + "@types/history@*": version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934"