Skip to content

Commit

Permalink
verify captcha token serverside
Browse files Browse the repository at this point in the history
  • Loading branch information
pettinarip committed Mar 14, 2022
1 parent f43f9e2 commit 0a3ecf1
Show file tree
Hide file tree
Showing 22 changed files with 267 additions and 168 deletions.
5 changes: 3 additions & 2 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ GOOGLE_ACADEMIC_SHEET_NAME=
GOOGLE_DEVCON_SPREADSHEET_ID=
GOOGLE_DEVCON_SHEET_NAME=

# captcha sitekey
NEXT_PUBLIC_CAPTCHA_SITEKEY=
# captcha
NEXT_PUBLIC_HCAPTCHA_SITEKEY=
HCAPTCHA_SECRET=
9 changes: 9 additions & 0 deletions additional.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Fields, Files } from 'formidable';
import { IncomingMessage } from 'http';

declare module 'next' {
export interface NextApiRequest extends IncomingMessage {
fields?: Fields;
files?: Files;
}
}
8 changes: 3 additions & 5 deletions src/components/forms/AcademicGrantsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ import {
} from './constants';
import { ACADEMIC_GRANTS_THANK_YOU_PAGE_URL, TOAST_OPTIONS } from '../../constants';

import { AcademicGrantsFormData, ApplyingAs, BasicForm, GrantsReferralSource } from '../../types';

interface AcademicGrantsFormForm extends AcademicGrantsFormData, BasicForm {}
import { AcademicGrantsFormData, ApplyingAs, GrantsReferralSource } from '../../types';

export const AcademicGrantsForm: FC = () => {
const router = useRouter();
Expand All @@ -53,7 +51,7 @@ export const AcademicGrantsForm: FC = () => {
value: '',
label: ''
});
const methods = useForm<AcademicGrantsFormForm>({
const methods = useForm<AcademicGrantsFormData>({
mode: 'onBlur'
});
const {
Expand All @@ -65,7 +63,7 @@ export const AcademicGrantsForm: FC = () => {
reset
} = methods;

const onSubmit = async ({ captchaToken, ...data }: AcademicGrantsFormForm) => {
const onSubmit = async (data: AcademicGrantsFormData) => {
return api.academicGrants
.submit(data)
.then(res => {
Expand Down
8 changes: 3 additions & 5 deletions src/components/forms/DevconGrantsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ import {
} from './constants';
import { DEVCON_GRANTS_THANK_YOU_PAGE_URL, TOAST_OPTIONS } from '../../constants';

import { BasicForm, DevconGrantsFormData, EventFormat } from '../../types';

interface DevconGrantsFormForm extends DevconGrantsFormData, BasicForm {}
import { DevconGrantsFormData, EventFormat } from '../../types';

export const DevconGrantsForm: FC = () => {
const router = useRouter();
Expand All @@ -48,7 +46,7 @@ export const DevconGrantsForm: FC = () => {
EVENT_FORMAT_OPTIONS[2].value // hibrid
].includes((eventFormat as EventFormat).value);

const methods = useForm<DevconGrantsFormForm>({
const methods = useForm<DevconGrantsFormData>({
mode: 'onBlur'
});
const {
Expand All @@ -59,7 +57,7 @@ export const DevconGrantsForm: FC = () => {
reset
} = methods;

const onSubmit = async ({ captchaToken, ...data }: DevconGrantsFormForm) => {
const onSubmit = async (data: DevconGrantsFormData) => {
return api.devconGrants
.submit(data)
.then(res => {
Expand Down
8 changes: 3 additions & 5 deletions src/components/forms/GranteeFinanceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,14 @@ import { api } from './api';

import { GRANTEE_FINANCE_THANK_YOU_PAGE_URL, TOAST_OPTIONS } from '../../constants';

import { GranteeFinanceFormData, TokenPreference, PaymentPreference, BasicForm } from '../../types';

interface GranteeFinanceFormForm extends GranteeFinanceFormData, BasicForm {}
import { GranteeFinanceFormData, TokenPreference, PaymentPreference } from '../../types';

export const GranteeFinanceForm: FC = () => {
const [paymentPreference, setPaymentPreference] = useState<PaymentPreference>('');
const [tokenPreference, setTokenPreference] = useState<TokenPreference>('ETH');
const router = useRouter();
const toast = useToast();
const methods = useForm<GranteeFinanceFormForm>({
const methods = useForm<GranteeFinanceFormData>({
mode: 'onBlur'
});
const {
Expand All @@ -49,7 +47,7 @@ export const GranteeFinanceForm: FC = () => {
const preferETH = receivesCrypto && tokenPreference === 'ETH';
const preferDAI = receivesCrypto && tokenPreference === 'DAI';

const onSubmit = async ({ captchaToken, ...data }: GranteeFinanceFormForm) => {
const onSubmit = async (data: GranteeFinanceFormData) => {
return api.granteeFinance
.submit(data)
.then(res => {
Expand Down
8 changes: 3 additions & 5 deletions src/components/forms/OfficeHoursForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,15 @@ import {
} from './constants';
import { OFFICE_HOURS_THANK_YOU_PAGE_URL, TOAST_OPTIONS } from '../../constants';

import { BasicForm, IndividualOrTeam, OfficeHoursFormData, ReasonForMeeting } from '../../types';

interface OfficeHoursForm extends OfficeHoursFormData, BasicForm {}
import { IndividualOrTeam, OfficeHoursFormData, ReasonForMeeting } from '../../types';

export const OfficeHoursForm: FC = () => {
const [individualOrTeam, setIndividualOrTeam] = useState<IndividualOrTeam>('Individual');
const [reasonForMeeting, setReasonForMeeting] = useState<ReasonForMeeting>(['']);
const router = useRouter();
const toast = useToast();

const methods = useForm<OfficeHoursForm>({
const methods = useForm<OfficeHoursFormData>({
mode: 'onBlur'
});
const {
Expand All @@ -57,7 +55,7 @@ export const OfficeHoursForm: FC = () => {
reset
} = methods;

const onSubmit = async ({ captchaToken, ...data }: OfficeHoursForm) => {
const onSubmit = async (data: OfficeHoursFormData) => {
return api.officeHours
.submit(data)
.then(res => {
Expand Down
8 changes: 3 additions & 5 deletions src/components/forms/ProjectGrantsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,9 @@ import {
TOAST_OPTIONS
} from '../../constants';

import { BasicForm, ProjectGrantsFormData, ReferralSource } from '../../types';
import { ProjectGrantsFormData, ReferralSource } from '../../types';
import { RemoveIcon } from '../UI/icons';

interface ProjectGrantsForm extends ProjectGrantsFormData, BasicForm {}

export const ProjectGrantsForm: FC = () => {
const router = useRouter();
const toast = useToast();
Expand All @@ -58,7 +56,7 @@ export const ProjectGrantsForm: FC = () => {
label: ''
});

const methods = useForm<ProjectGrantsForm>({
const methods = useForm<ProjectGrantsFormData>({
mode: 'onBlur'
});

Expand Down Expand Up @@ -90,7 +88,7 @@ export const ProjectGrantsForm: FC = () => {
);
const { getRootProps, getInputProps } = useDropzone({ onDrop });

const onSubmit = async ({ captchaToken, ...data }: ProjectGrantsForm) => {
const onSubmit = async (data: ProjectGrantsFormData) => {
return api.projectGrants
.submit(data)
.then(res => {
Expand Down
7 changes: 2 additions & 5 deletions src/components/forms/SmallGrantsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,12 @@ import {
import { SMALL_GRANTS_THANK_YOU_PAGE_URL, TOAST_OPTIONS } from '../../constants';

import {
BasicForm,
IndividualOrTeam,
ProjectCategory,
RepeatApplicant,
SmallGrantsFormData
} from '../../types';

interface SmallGrantsFormForm extends SmallGrantsFormData, BasicForm {}

export const SmallGrantsForm: FC = () => {
const router = useRouter();
const toast = useToast();
Expand All @@ -55,7 +52,7 @@ export const SmallGrantsForm: FC = () => {
label: ''
});

const methods = useForm<SmallGrantsFormForm>({
const methods = useForm<SmallGrantsFormData>({
mode: 'onBlur'
});
const {
Expand All @@ -72,7 +69,7 @@ export const SmallGrantsForm: FC = () => {

const isAnEvent = (projectCategory as ProjectCategory).value === COMMUNITY_EVENT;

const onSubmit = async ({ captchaToken, ...data }: SmallGrantsFormForm) => {
const onSubmit = async (data: SmallGrantsFormData) => {
return api.smallGrants
.submit(data, isAProject)
.then(res => {
Expand Down
24 changes: 19 additions & 5 deletions src/components/forms/fields/Captcha.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
import React, { FC, useEffect } from 'react';
import React, { FC, useCallback, useEffect, useRef } from 'react';
import HCaptcha from '@hcaptcha/react-hcaptcha';
import { useFormContext } from 'react-hook-form';

export const Captcha: FC = () => {
const { register, reset, setValue, getValues } = useFormContext();
const captchaRef = useRef<HCaptcha>(null);
const { register, reset: resetForm, setValue, getValues, formState } = useFormContext();

const reset = useCallback(() => {
const { captchaToken, ...values } = getValues();
resetForm({ ...values });
}, [getValues, resetForm]);

useEffect(() => {
register('captchaToken', { required: true });
});

useEffect(() => {
// Whenever the form is submitted reset the captcha input
if (formState.isSubmitted && captchaRef.current) {
captchaRef.current.resetCaptcha();
reset();
}
}, [formState, reset]);

const onVerify = (token: string) => {
setValue('captchaToken', token, { shouldValidate: true });
};

const onExpire = () => {
// when token expires, reset the captcha field
const { captchaToken, ...values } = getValues();
reset({ ...values });
reset();
};

return (
<HCaptcha
sitekey={process.env.NEXT_PUBLIC_CAPTCHA_SITEKEY!}
ref={captchaRef}
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITEKEY!}
onVerify={onVerify}
onExpire={onExpire}
/>
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export const NAV_LINKS: NavLink[] = [
// external links
export const ETHRESEARCH_URL = 'https://ethresear.ch/';
export const DEVCON_URL = 'https://devcon.org/';
export const HCAPTCHA_VERIFY_URL = 'https://hcaptcha.com/siteverify';

// api
export const DOWNLOAD_APPLICATION_URL = '/projectGrantsApplication.docx';
Expand Down
2 changes: 2 additions & 0 deletions src/middlewares/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './verifyCaptcha';
export * from './multipartyParse';
25 changes: 25 additions & 0 deletions src/middlewares/multipartyParse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import formidable from 'formidable';
import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';

/**
* Parses multipart/form-data
*/
export const multipartyParse =
(handler: NextApiHandler, options: formidable.Options) =>
(req: NextApiRequest, res: NextApiResponse) => {
const form = formidable(options);

form.parse(req, async (err, fields, files) => {
if (err) {
console.error(err);
res.status(400).json({ status: 'fail' });
return;
}

// Extend `req` object with parsed fields and files
req.fields = fields;
req.files = files;

handler(req, res);
});
};
43 changes: 43 additions & 0 deletions src/middlewares/verifyCaptcha.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';

import { HCAPTCHA_VERIFY_URL } from '../constants';

const { HCAPTCHA_SECRET } = process.env;

/**
* Verifies client captcha token against hCaptcha endpoint
*/
export const verifyCaptcha =
(handler: NextApiHandler) => async (req: NextApiRequest, res: NextApiResponse) => {
let captchaToken;

if (req.fields?.captchaToken) {
captchaToken = req.fields.captchaToken;
}

if (req.body?.captchaToken) {
captchaToken = req.body.captchaToken;
}

if (!captchaToken) {
return res.status(400).json({ status: 'captcha failed' });
}

const response = await fetch(HCAPTCHA_VERIFY_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `response=${captchaToken}&secret=${HCAPTCHA_SECRET}`
});

if (!response.ok) {
return res.status(400).json({ status: 'captcha failed' });
}

const { success } = await response.json();

if (!success) {
return res.status(400).json({ status: 'captcha failed' });
}

return handler(req, res);
};
5 changes: 4 additions & 1 deletion src/pages/api/academic-grants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import jsforce from 'jsforce';
import { NextApiRequest, NextApiResponse } from 'next';

import addRowToSpreadsheet from '../../utils/addRowToSpreadsheet';
import { verifyCaptcha } from '../../middlewares';

const googleSpreadsheetId = process.env.GOOGLE_ACADEMIC_SPREADSHEET_ID;
const googleSheetName = process.env.GOOGLE_ACADEMIC_SHEET_NAME;

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { body } = req;
const {
firstName: FirstName,
Expand Down Expand Up @@ -122,3 +123,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
});
}

export default verifyCaptcha(handler);
5 changes: 4 additions & 1 deletion src/pages/api/devcon-grants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import jsforce from 'jsforce';
import { NextApiRequest, NextApiResponse } from 'next';

import addRowToSpreadsheet from '../../utils/addRowToSpreadsheet';
import { verifyCaptcha } from '../../middlewares';

const googleSpreadsheetId = process.env.GOOGLE_DEVCON_SPREADSHEET_ID;
const googleSheetName = process.env.GOOGLE_DEVCON_SHEET_NAME;

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { body } = req;
const {
firstName: FirstName,
Expand Down Expand Up @@ -102,3 +103,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
});
}

export default verifyCaptcha(handler);
6 changes: 5 additions & 1 deletion src/pages/api/grantee-finance.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import jsforce from 'jsforce';
import { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
import { verifyCaptcha } from '../../middlewares';

async function handler(req: NextApiRequest, res: NextApiResponse) {
const { body } = req;
const {
beneficiaryName: Beneficiary_Name__c,
Expand Down Expand Up @@ -73,3 +75,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
});
}

export default verifyCaptcha(handler);

0 comments on commit 0a3ecf1

Please sign in to comment.