- add recaptcha boilerplate for contact form

- still need to add api key to both project and ci/cd
This commit is contained in:
oonyeje 2023-11-29 09:35:12 -05:00
parent 6cd3a9deba
commit 8b97fcb73e
4 changed files with 127 additions and 28 deletions

View File

@ -1,31 +1,49 @@
import { useState } from 'react'; import { RefObject, useRef, useState } from 'react';
import { useForm, SubmitHandler, FieldValues } from 'react-hook-form'; import { useForm, SubmitHandler, FieldValues } from 'react-hook-form';
import ReCAPTCHA from 'react-google-recaptcha';
export interface ContactFormData extends FieldValues {
firstName: string,
lastName: string,
email: string,
subject: string,
summary: string,
honeypot_xyz: string
};
const ContactForm = () => { const ContactForm = () => {
const { const {
register, register,
resetField,
handleSubmit, handleSubmit,
setValue,
formState: { errors }, formState: { errors },
} = useForm(); } = useForm();
const [submitted, setSubmitted] = useState(false); const [submitted, setSubmitted] = useState(false);
const handleFormSubmission: SubmitHandler<FieldValues> = async (data) => { const handleFormSubmission: SubmitHandler<FieldValues> = async (data: FieldValues) => {
console.log(data) const submissionData = data as ContactFormData;
console.log(submissionData)
const res = await fetch('/api/contact', { if (submissionData.honeypot_xyz !== "") {
// Form submission is spam
return;
}
const res = await fetch('/api/contact', {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/json, text/plain, */*', 'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify(data) body: JSON.stringify(submissionData)
}); });
console.log('Response received') console.log('Response received')
if (res.status === 200) { if (res.status === 200) {
console.log('Response succeeded!') console.log('Response succeeded!')
setSubmitted(true) setSubmitted(true)
resetField('captchaToken')
} }
}; };
@ -33,6 +51,12 @@ const ContactForm = () => {
console.log(error) console.log(error)
}; };
const onCaptchaChange = (token: string | null) => {
// Set the captcha token when the user completes the reCAPTCHA
if (token) {
setValue('captchaToken', token);
}
};
return ( return (
<div className='w-full flex flex-row justify-center'> <div className='w-full flex flex-row justify-center'>
<form className=" w-1/2" onSubmit={handleSubmit(handleFormSubmission, handleFormError)}> <form className=" w-1/2" onSubmit={handleSubmit(handleFormSubmission, handleFormError)}>
@ -62,11 +86,22 @@ const ContactForm = () => {
</select> </select>
{errors.subject && <p>Subject is required.</p>} {errors.subject && <p>Subject is required.</p>}
</div> </div>
<div className="flex flex-col w-full">
<input type='hidden' value="" {...register('honeypot_xyz', { required: true })}/>
</div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<textarea placeholder='Summary...' className="flex grow h-52" {...register('summary')} /> <textarea placeholder='Summary...' className="flex grow h-52" {...register('summary')} />
{errors.summary && <p>Please enter a message for your Project Inquiry.</p>} {errors.summary && <p>Please enter a message for your Project Inquiry.</p>}
</div> </div>
<div className="pb-20px">
<ReCAPTCHA
size="normal"
sitekey={process.env.RECAPTCHA_SECRET_KEY ? process.env.RECAPTCHA_SECRET_KEY : 'ENTER_API_KEY'}
{...register('captchaToken', { required: true })}
onChange={onCaptchaChange}
/>
</div>
</div> </div>
<input className="bg-blue-600 text-white rounded-sm cursor-pointer" type="submit" /> <input className="bg-blue-600 text-white rounded-sm cursor-pointer" type="submit" />
</div> </div>

View File

@ -16,12 +16,14 @@
"@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@types/nodemailer": "^6.4.14", "@types/nodemailer": "^6.4.14",
"@types/react-google-recaptcha": "^2.1.8",
"iframe-resizer-react": "^1.1.0", "iframe-resizer-react": "^1.1.0",
"next": "latest", "next": "latest",
"nodemailer": "^6.9.7", "nodemailer": "^6.9.7",
"pdfjs-dist": "^4.0.269", "pdfjs-dist": "^4.0.269",
"react": "latest", "react": "latest",
"react-dom": "latest", "react-dom": "latest",
"react-google-recaptcha": "^3.1.0",
"react-hook-form": "^7.47.0", "react-hook-form": "^7.47.0",
"react-modal": "^3.16.1", "react-modal": "^3.16.1",
"react-pdf": "^7.4.0", "react-pdf": "^7.4.0",

View File

@ -18,7 +18,7 @@ type MailData = {
html?: string html?: string
} }
export default function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse<Data> res: NextApiResponse<Data>
) { ) {
@ -29,12 +29,20 @@ export default function handler(
lastName, lastName,
subject, subject,
email, email,
summary summary,
captchaToken
} = req.body; } = req.body;
const {SMTP_PROXY_EMAIL, SMTP_RECIPIENT_EMAIL, SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD} = process.env; const {
SMTP_PROXY_EMAIL,
SMTP_RECIPIENT_EMAIL,
SMTP_HOST,
SMTP_PORT,
SMTP_USERNAME,
SMTP_PASSWORD,
RECAPTCHA_SECRET_KEY
} = process.env;
console.log(JSON.stringify({SMTP_PROXY_EMAIL, SMTP_RECIPIENT_EMAIL, SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD}));
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
port: parseInt(SMTP_PORT!), port: parseInt(SMTP_PORT!),
host: SMTP_HOST!, host: SMTP_HOST!,
@ -45,21 +53,45 @@ export default function handler(
secure: true, secure: true,
}); });
const mailData: MailData = { console.log(JSON.stringify({SMTP_PROXY_EMAIL, SMTP_RECIPIENT_EMAIL, SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD}));
from: SMTP_PROXY_EMAIL!,
to: SMTP_RECIPIENT_EMAIL!, try {
subject: `Message From ${firstName} ${lastName}: ${subject}`,
text: summary + " | Sent from: " + email, const response = await fetch(
html: `<div>${summary}</div><p>Sent from: `https://www.google.com/recaptcha/api/siteverify?secret=${RECAPTCHA_SECRET_KEY}&response=${captchaToken}`
${email}</p>` );
} if ((await response.json()).success) {
//reCaptcha verification successfull
const mailData: MailData = {
from: SMTP_PROXY_EMAIL!,
to: SMTP_RECIPIENT_EMAIL!,
subject: `Message From ${firstName} ${lastName}: ${subject}`,
text: summary + " | Sent from: " + email,
html: `<div>${summary}</div><p>Sent from:
${email}</p>`
}
transporter.sendMail(mailData, function (err, info) {
if(err) {
console.log(err);
res.status(500).send('Internal Server Error');
}
else {
console.log('successful');
console.log(info);
res.status(200).end();
}
})
res.status(200).json(req.body);
} else {
// reCAPTCHA verification failed
res.status(400).send('reCAPTCHA verification failed.');
}
} catch (error) {
console.error(error);
res.status(500).send('Internal server error');
}
transporter.sendMail(mailData, function (err, info) {
if(err)
console.log(err)
else
console.log(info)
})
res.status(200).json(req.body);
} }

View File

@ -283,6 +283,13 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-google-recaptcha@^2.1.8":
version "2.1.8"
resolved "https://registry.yarnpkg.com/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.8.tgz#a76065be49b22851914b539c32601402fde17bbd"
integrity sha512-nYI3ZDoteZ0g4FYusyKWqz7AZqRdu70R3wDkosCcN0peb2WLn57i0Alm4IPiCRIx59yTUVPTiOELZH08gV1wXA==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@latest": "@types/react@*", "@types/react@latest":
version "18.2.23" version "18.2.23"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.23.tgz#60ad6cf4895e93bed858db0e03bcc4ff97d0410e" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.23.tgz#60ad6cf4895e93bed858db0e03bcc4ff97d0410e"
@ -1529,6 +1536,13 @@ has@^1.0.3:
dependencies: dependencies:
function-bind "^1.1.1" function-bind "^1.1.1"
hoist-non-react-statics@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
https-proxy-agent@^5.0.0: https-proxy-agent@^5.0.0:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
@ -2386,7 +2400,7 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: prop-types@^15.5.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -2428,6 +2442,14 @@ rc@^1.2.7:
minimist "^1.2.0" minimist "^1.2.0"
strip-json-comments "~2.0.1" strip-json-comments "~2.0.1"
react-async-script@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.2.0.tgz#ab9412a26f0b83f5e2e00de1d2befc9400834b21"
integrity sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==
dependencies:
hoist-non-react-statics "^3.3.0"
prop-types "^15.5.0"
react-dom@latest: react-dom@latest:
version "18.2.0" version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
@ -2436,12 +2458,20 @@ react-dom@latest:
loose-envify "^1.1.0" loose-envify "^1.1.0"
scheduler "^0.23.0" scheduler "^0.23.0"
react-google-recaptcha@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz#44aaab834495d922b9d93d7d7a7fb2326315b4ab"
integrity sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg==
dependencies:
prop-types "^15.5.0"
react-async-script "^1.2.0"
react-hook-form@^7.47.0: react-hook-form@^7.47.0:
version "7.47.0" version "7.47.0"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.47.0.tgz#a42f07266bd297ddf1f914f08f4b5f9783262f31" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.47.0.tgz#a42f07266bd297ddf1f914f08f4b5f9783262f31"
integrity sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg== integrity sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg==
react-is@^16.13.1: react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==