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 +++++++++++++++++++++++++++++++++++++++++++--- src/store/form/actions.ts | 14 +++++++++++-- src/store/form/reducers.ts | 7 ++++++- src/store/form/types.ts | 3 ++- 4 files changed, 69 insertions(+), 7 deletions(-) (limited to 'src') 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(); @@ -166,6 +175,9 @@ function FormPage(): JSX.Element { const values = useSelector( state => state.values ); + const captchaToken = useSelector( + state => state.captchaToken + ); const dispatch = useDispatch(); @@ -173,6 +185,9 @@ function FormPage(): JSX.Element { const [sending, setSending] = useState(); const [sent, setSent] = useState(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const captchaRef = useRef(); + 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 =
This form is now closed. You will not be able to submit your response.
; } + let captcha = null; + if (!(FormFeatures.DisableAntispam in form.features)) { + captcha = ( +
+ dispatch(setCaptchaToken(token))} + onExpire={() => dispatch(setCaptchaToken(null))} + ref={captchaRef} + /> +
+ ); + } + return (
@@ -314,6 +359,7 @@ function FormPage(): JSX.Element {
{ closed_header } { questions } + { captcha }
diff --git a/src/store/form/actions.ts b/src/store/form/actions.ts index 8b48a37..dc43729 100644 --- a/src/store/form/actions.ts +++ b/src/store/form/actions.ts @@ -5,7 +5,8 @@ export enum FormAction { SET_VALUE = "SET_VALUE", SET_ERROR = "SET_ERROR", SET_VALID = "SET_VALID", - CLEAN = "CLEAN" + CLEAN = "CLEAN", + SET_CAPTCHA_TOKEN = "SET_CAPTCHA_TOKEN" } // This is base for all actions @@ -42,7 +43,12 @@ export interface CleanAction extends DefaultFormAction { type: FormAction.CLEAN } -export type Action = SetValueAction | SetErrorAction | SetValidAction | CleanAction; +export interface SetCaptchaTokenAction extends DefaultFormAction { + type: FormAction.SET_CAPTCHA_TOKEN, + payload: string | null +} + +export type Action = SetValueAction | SetErrorAction | SetValidAction | CleanAction | SetCaptchaTokenAction; export function setValue(question: Question, value: string | Map | null): SetValueAction { return { @@ -77,3 +83,7 @@ export function setValid(question: Question, valid: boolean): SetValidAction { export function clean(): CleanAction { return { type: FormAction.CLEAN }; } + +export function setCaptchaToken(token: string | null): SetCaptchaTokenAction { + return { type: FormAction.SET_CAPTCHA_TOKEN, payload: token }; +} diff --git a/src/store/form/reducers.ts b/src/store/form/reducers.ts index fcbb33a..21bd047 100644 --- a/src/store/form/reducers.ts +++ b/src/store/form/reducers.ts @@ -4,7 +4,8 @@ import {FormState} from "./types"; export const initialState: FormState = { values: new Map(), errors: new Map(), - valid: new Map() + valid: new Map(), + captchaToken: null }; export function formReducer(state = initialState, action: Action): FormState { @@ -24,6 +25,10 @@ export function formReducer(state = initialState, action: Action): FormState { case FormAction.CLEAN: return initialState; + + case FormAction.SET_CAPTCHA_TOKEN: + new_state.captchaToken = action.payload; + break; } return new_state; } diff --git a/src/store/form/types.ts b/src/store/form/types.ts index 1d3e594..3f8557e 100644 --- a/src/store/form/types.ts +++ b/src/store/form/types.ts @@ -1,5 +1,6 @@ export interface FormState { values: Map | null>, errors: Map, - valid: Map + valid: Map, + captchaToken: string | null } -- cgit v1.2.3