Skip to content

Commit

Permalink
feat: validate captcha on new response
Browse files Browse the repository at this point in the history
  • Loading branch information
mplewis committed Nov 20, 2024
1 parent 2547322 commit 73d296b
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 3 deletions.
1 change: 1 addition & 0 deletions api/src/graphql/responses.sdl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const schema = gql`
headCount: Int!
comment: String!
remindPriorSec: Int
captchaResponse: String!
}
input UpdateResponseInput {
Expand Down
12 changes: 11 additions & 1 deletion api/src/services/responses/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import type {
Reminder,
} from 'types/graphql'

import { RedwoodError } from '@redwoodjs/api'

import { validateCaptcha } from 'src/lib/captcha'
import { db } from 'src/lib/db'
import {
sendNewResponseReceived,
Expand Down Expand Up @@ -91,11 +94,18 @@ export const responseByEditToken: QueryResolvers['responseByEditToken'] =

export const createResponse: MutationResolvers['createResponse'] = async ({
eventId,
input,
input: _input,
}) => {
const event = await db.event.findUnique({ where: { id: eventId } })
if (!event) throw new Error(`Event not found: ${eventId}`)

const { captchaResponse, ...input } = _input
const valid = await validateCaptcha(captchaResponse)
if (!valid)
throw new RedwoodError(
'Could not validate reCAPTCHA. Please refresh the page and try again.'
)

const reminders: { sendAt: Date }[] = []
if (input.remindPriorSec) {
const sendAt = dayjs(event.start)
Expand Down
4 changes: 3 additions & 1 deletion web/src/components/NewResponseForm/NewResponseForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const NewResponseForm = ({ event, responseToken }: Props) => {
defaultValues: { headCount: 1 },
})

const [captchaResponse, setCaptchaResponse] = useState<string | null>(null)
const [createdForEmail, setCreatedForEmail] = useState<string | null>(null)

const [create, { loading, error }] = useMutation<
Expand Down Expand Up @@ -90,9 +91,10 @@ const NewResponseForm = ({ event, responseToken }: Props) => {
loading={loading}
error={error}
formMethods={formMethods}
onCaptchaResponse={setCaptchaResponse}
onSubmit={(data: FormValues) => {
if (!data.email) throw new Error('Email is required')
const input = { ...data, email: data.email }
const input = { ...data, email: data.email, captchaResponse }
create({ variables: { eventId: event.id, input } })
}}
/>
Expand Down
21 changes: 20 additions & 1 deletion web/src/components/ResponseForm/ResponseForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'

import { ApolloError } from '@apollo/client/errors'
import ReCAPTCHA from 'react-google-recaptcha'
import { SetOptional } from 'type-fest'
import { PublicEvent, UpdatableResponse } from 'types/graphql'

Expand All @@ -15,6 +16,7 @@ import {
UseFormReturn,
} from '@redwoodjs/forms'

import { RECAPTCHA_CLIENT_KEY } from 'src/app.config'
import FormField from 'src/components/FormField/FormField'
import Typ from 'src/components/Typ/Typ'
import { isEmail } from 'src/logic/validation'
Expand All @@ -30,6 +32,7 @@ export type Props = {
error?: ApolloError
formMethods: UseFormReturn<FormValues>
onChange?: React.FormEventHandler<HTMLFormElement>
onCaptchaResponse?: (response: string | null) => void
onSubmit: (data: FormValues) => void
}

Expand All @@ -49,7 +52,16 @@ function random<T>(choices: T[]): T {
}

const ResponseForm = (props: Props) => {
const { mode, event, error, loading, formMethods, onChange, onSubmit } = props
const {
mode,
event,
error,
loading,
formMethods,
onChange,
onCaptchaResponse,
onSubmit,
} = props
const { formState } = formMethods

const [exampleName, setExampleName] = useState('')
Expand Down Expand Up @@ -151,6 +163,13 @@ const ResponseForm = (props: Props) => {
</SelectField>
</FormField>

{mode === 'CREATE' && (
<ReCAPTCHA
sitekey={RECAPTCHA_CLIENT_KEY}
onChange={onCaptchaResponse}
/>
)}

<Submit
className="button is-success mt-3"
disabled={loading || !formState.isValid || !formState.isDirty}
Expand Down

0 comments on commit 73d296b

Please sign in to comment.