From 75273e5798eb3cc69fb23ab8547b9a438d35845e Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sun, 21 Feb 2021 08:33:06 +0200
Subject: Implement basic hCaptcha

---
 src/pages/FormPage.tsx | 52 +++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 49 insertions(+), 3 deletions(-)

(limited to 'src/pages/FormPage.tsx')

diff --git a/src/pages/FormPage.tsx b/src/pages/FormPage.tsx
index e9306e3..c06b33c 100644
--- a/src/pages/FormPage.tsx
+++ b/src/pages/FormPage.tsx
@@ -2,10 +2,11 @@
 import { jsx, css } from "@emotion/react";
 import { Link } from "react-router-dom";
 
-import React, { SyntheticEvent, useEffect, useState, createRef } from "react";
+import React, {SyntheticEvent, useEffect, useState, createRef, useRef} from "react";
 import { useParams } from "react-router";
 import { PropagateLoader } from "react-spinners";
 import { useDispatch, useSelector } from "react-redux";
+import HCaptcha from "@hcaptcha/react-hcaptcha";
 
 import HeaderBar from "../components/HeaderBar";
 import ConnectedRenderedQuestion, { RenderedQuestion } from "../components/Question";
@@ -18,7 +19,7 @@ import { unselectable }  from "../commonStyles";
 import { Question, QuestionType } from "../api/question";
 import ApiClient from "../api/client";
 import { FormState } from "../store/form/types";
-import { clean } from "../store/form/actions";
+import { clean, setCaptchaToken } from "../store/form/actions";
 
 interface PathParams {
     id: string
@@ -157,6 +158,14 @@ const closedHeaderStyles = css`
   }
 `;
 
+const captchaStyles = css`
+  text-align: center;
+  
+  @media (max-width: 850px) {
+    padding-bottom: 1.2rem;
+  }
+`;
+
 function FormPage(): JSX.Element {
     const { id } = useParams<PathParams>();
 
@@ -166,6 +175,9 @@ function FormPage(): JSX.Element {
     const values = useSelector<FormState, FormState["values"]>(
         state => state.values
     );
+    const captchaToken = useSelector<FormState, FormState["captchaToken"]>(
+        state => state.captchaToken
+    );
 
     const dispatch = useDispatch();
 
@@ -173,6 +185,9 @@ function FormPage(): JSX.Element {
     const [sending, setSending] = useState<boolean>();
     const [sent, setSent] = useState<boolean>();
 
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const captchaRef = useRef<any>();
+
     useEffect(() => {
         dispatch(clean());
         getForm(id).then(form => {
@@ -255,6 +270,14 @@ function FormPage(): JSX.Element {
             return;
         }
 
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        if (!(FormFeatures.DisableAntispam in form!.features) && captchaToken === null) {
+            if (captchaRef && captchaRef.current) {
+                captchaRef.current.execute();
+            }
+            return;
+        }
+
         setSending(true);
 
         const answers: { [key: string]: unknown } = {};
@@ -294,7 +317,14 @@ 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"] = captchaToken;
+        }
+
+        await ApiClient.post(`forms/submit/${id}`, data);
         setSending(false);
         setSent(true);
     }
@@ -306,6 +336,21 @@ function FormPage(): JSX.Element {
         closed_header = <div css={closedHeaderStyles}>This form is now closed. You will not be able to submit your response.</div>;
     }
 
+    let captcha = null;
+    if (!(FormFeatures.DisableAntispam in form.features)) {
+        captcha = (
+            <div css={captchaStyles}>
+                <HCaptcha
+                    sitekey={process.env.HCAPTCHA_SITEKEY ? process.env.HCAPTCHA_SITEKEY : ""}
+                    theme={"dark"}
+                    onVerify={(token) => dispatch(setCaptchaToken(token))}
+                    onExpire={() => dispatch(setCaptchaToken(null))}
+                    ref={captchaRef}
+                />
+            </div>
+        );
+    }
+
     return (
         <div>
             <HeaderBar title={form.name} description={form.description}/>
@@ -314,6 +359,7 @@ function FormPage(): JSX.Element {
                 <form id="form" onSubmit={handleSubmit} css={[formStyles, unselectable]}>
                     { closed_header }
                     { questions }
+                    { captcha }
                 </form>
                 <Navigation form_state={open}/>
             </div>
-- 
cgit v1.2.3

