diff --git a/api/src/graphql/events.sdl.ts b/api/src/graphql/events.sdl.ts index cc4fdf7..4b1a6ec 100644 --- a/api/src/graphql/events.sdl.ts +++ b/api/src/graphql/events.sdl.ts @@ -52,7 +52,7 @@ export const schema = gql` ownerEmail: String! title: String! responseConfig: ResponseConfig! - captcha: String! + captchaResponse: String! } input UpdateEventInput { diff --git a/api/src/lib/captcha.ts b/api/src/lib/captcha.ts new file mode 100644 index 0000000..7bc5e1e --- /dev/null +++ b/api/src/lib/captcha.ts @@ -0,0 +1,25 @@ +import querystring from 'querystring' + +import { RECAPTCHA_SERVER_KEY } from 'src/app.config' + +import { logger } from './logger' + +const RECAPTCHA_ENDPOINT = 'https://www.google.com/recaptcha/api/siteverify' + +/** Return true if the given captcha token was valid, false otherwise. */ +export async function validateCaptcha(token: string): Promise { + try { + const data = { secret: RECAPTCHA_SERVER_KEY, response: token } + const res = await fetch(RECAPTCHA_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: querystring.stringify(data), + }) + const json = await res.json() + logger.debug({ json }, 'reCAPTCHA response') + return json.success + } catch (err) { + logger.error({ err }, 'Failed to validate reCAPTCHA') + return false + } +} diff --git a/api/src/services/events/events.ts b/api/src/services/events/events.ts index 5a42345..aede8f5 100644 --- a/api/src/services/events/events.ts +++ b/api/src/services/events/events.ts @@ -4,8 +4,9 @@ import type { PublicResponse, } from 'types/graphql' -import { validate } from '@redwoodjs/api' +import { RedwoodError, validate } from '@redwoodjs/api' +import { validateCaptcha } from 'src/lib/captcha' import { db } from 'src/lib/db' import { sendEventDetails } from 'src/lib/email/template/event' import { notifyEventCreated, notifyEventUpdated } from 'src/lib/notify/event' @@ -98,7 +99,14 @@ export const eventByPreviewToken: QueryResolvers['eventByPreviewToken'] = export const createEvent: MutationResolvers['createEvent'] = async ({ input: _input, }) => { - const { captcha, ...input } = _input + 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.' + ) + validate(input.ownerEmail, 'email', { email: true }) validate(input.title, 'title', { custom: { diff --git a/app.config.ts b/app.config.ts index 053bd32..15cf02b 100644 --- a/app.config.ts +++ b/app.config.ts @@ -24,4 +24,5 @@ export const S3_REGION = process.env.S3_REGION export const S3_BUCKET = process.env.S3_BUCKET export const S3_NAMESPACE = process.env.S3_NAMESPACE +export const RECAPTCHA_SERVER_KEY = process.env.RECAPTCHA_SERVER_KEY export const RECAPTCHA_CLIENT_KEY = process.env.REDWOOD_ENV_RECAPTCHA_CLIENT_KEY diff --git a/web/src/pages/NewEventPage/NewEventPage.tsx b/web/src/pages/NewEventPage/NewEventPage.tsx index a1d6374..75d67ce 100644 --- a/web/src/pages/NewEventPage/NewEventPage.tsx +++ b/web/src/pages/NewEventPage/NewEventPage.tsx @@ -69,7 +69,7 @@ const NewEventPage = () => { const { formState } = formMethods const [redirecting, setRedirecting] = useState(false) - const [captcha, setCaptcha] = useState(null) + const [captchaResponse, setCaptchaResponse] = useState(null) const [create, { loading, error }] = useMutation< CreateEventMutation, @@ -93,7 +93,9 @@ const NewEventPage = () => { className="mt-3" formMethods={formMethods} onSubmit={(input: FormValues) => - create({ variables: { input: { ...input, captcha } } }) + create({ + variables: { input: { ...input, captchaResponse } }, + }) } > @@ -140,11 +142,16 @@ const NewEventPage = () => { - + {loading || redirecting ? 'Creating...' : 'Create New Event'}