mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-04-26 04:45:22 -04:00
switch to cloudflare turnstile
This commit is contained in:
parent
00165a5871
commit
5fe420913e
@ -22,12 +22,12 @@ RESEND_API_KEY=
|
||||
RESEND_DOMAIN=
|
||||
|
||||
# required for production. falls back to testing keys if not set or in dev environment:
|
||||
# https://docs.hcaptcha.com/#integration-testing-test-keys
|
||||
# https://developers.cloudflare.com/turnstile/troubleshooting/testing/
|
||||
# site key must be prefixed with NEXT_PUBLIC_ since it is used to embed the actual captcha widget.
|
||||
# https://dashboard.hcaptcha.com/sites
|
||||
NEXT_PUBLIC_HCAPTCHA_SITE_KEY=
|
||||
# https://developers.cloudflare.com/turnstile/
|
||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY=
|
||||
# used only for backend validation of contact form submissions on /api/contact.
|
||||
HCAPTCHA_SECRET_KEY=
|
||||
TURNSTILE_SECRET_KEY=
|
||||
|
||||
# optional. sets the site ID of the fathom analytics script.
|
||||
# https://app.usefathom.com/sites
|
||||
|
@ -1,36 +1,21 @@
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
import Turnstile from "react-turnstile";
|
||||
import useHasMounted from "../../hooks/useHasMounted";
|
||||
import useTheme from "../../hooks/useTheme";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
|
||||
export type CaptchaProps = {
|
||||
size?: "normal" | "compact" | "invisible";
|
||||
theme?: "light" | "dark";
|
||||
export type CaptchaProps = Omit<ComponentPropsWithoutRef<typeof Turnstile>, "sitekey"> & {
|
||||
className?: string;
|
||||
|
||||
// callbacks pulled verbatim from node_modules/@hcaptcha/react-hcaptcha/types/index.d.ts
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
onExpire?: () => any;
|
||||
onOpen?: () => any;
|
||||
onClose?: () => any;
|
||||
onChalExpired?: () => any;
|
||||
onError?: (event: string) => any;
|
||||
onVerify?: (token: string, ekey: string) => any;
|
||||
onLoad?: () => any;
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
};
|
||||
|
||||
const Captcha = ({ size = "normal", theme, className, ...rest }: CaptchaProps) => {
|
||||
const Captcha = ({ theme, className, ...rest }: CaptchaProps) => {
|
||||
const hasMounted = useHasMounted();
|
||||
const { activeTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{hasMounted && (
|
||||
<HCaptcha
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY || "10000000-ffff-ffff-ffff-000000000001"}
|
||||
reCaptchaCompat={false}
|
||||
tabIndex={0}
|
||||
size={size}
|
||||
<Turnstile
|
||||
sitekey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || "1x00000000000000000000AA"}
|
||||
theme={theme || (activeTheme === "dark" ? activeTheme : "light")}
|
||||
{...rest}
|
||||
/>
|
||||
|
@ -55,7 +55,7 @@ const MarkdownTipIcon = styled(SiMarkdown, {
|
||||
marginRight: "0.25em",
|
||||
});
|
||||
|
||||
const HCaptcha = styled(Captcha, {
|
||||
const Turnstile = styled(Captcha, {
|
||||
margin: "1em 0",
|
||||
});
|
||||
|
||||
@ -133,7 +133,7 @@ type FormValues = {
|
||||
name: string;
|
||||
email: string;
|
||||
message: string;
|
||||
"h-captcha-response": string;
|
||||
"cf-turnstile-response": string;
|
||||
};
|
||||
|
||||
export type ContactFormProps = {
|
||||
@ -195,7 +195,7 @@ const ContactForm = ({ className }: ContactFormProps) => {
|
||||
name: "",
|
||||
email: "",
|
||||
message: "",
|
||||
"h-captcha-response": "",
|
||||
"cf-turnstile-response": "",
|
||||
}}
|
||||
validate={(values: FormValues) => {
|
||||
const errors: Partial<Record<keyof FormValues, boolean>> = {};
|
||||
@ -203,9 +203,9 @@ const ContactForm = ({ className }: ContactFormProps) => {
|
||||
errors.name = !values.name;
|
||||
errors.email = !values.email; // also loosely validated that it's email-like via browser (not foolproof)
|
||||
errors.message = !values.message;
|
||||
errors["h-captcha-response"] = !values["h-captcha-response"];
|
||||
errors["cf-turnstile-response"] = !values["cf-turnstile-response"];
|
||||
|
||||
if (!errors.name && !errors.email && !errors.message && !errors["h-captcha-response"]) {
|
||||
if (!errors.name && !errors.email && !errors.message && !errors["cf-turnstile-response"]) {
|
||||
setFeedback("");
|
||||
return {};
|
||||
} else {
|
||||
@ -267,7 +267,7 @@ const ContactForm = ({ className }: ContactFormProps) => {
|
||||
](https://jarv.is), and <code>`code`</code>.
|
||||
</MarkdownTip>
|
||||
|
||||
<HCaptcha onVerify={(token) => setFieldValue("h-captcha-response", token)} />
|
||||
<Turnstile onVerify={(token) => setFieldValue("cf-turnstile-response", token)} />
|
||||
|
||||
<ActionRow>
|
||||
<SubmitButton
|
||||
|
@ -21,7 +21,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@giscus/react": "^3.0.0",
|
||||
"@hcaptcha/react-hcaptcha": "^1.10.1",
|
||||
"@libsql/client": "^0.6.2",
|
||||
"@novnc/novnc": "1.4.0",
|
||||
"@octokit/graphql": "^8.1.1",
|
||||
@ -59,6 +58,7 @@
|
||||
"react-is": "18.3.1",
|
||||
"react-player": "^2.16.0",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"react-turnstile": "^1.1.3",
|
||||
"react-tweet": "^3.2.1",
|
||||
"rehype-prism-plus": "^2.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
|
@ -1,10 +1,15 @@
|
||||
import nodemailer from "nodemailer";
|
||||
import queryString from "query-string";
|
||||
import fetcher from "../../lib/helpers/fetcher";
|
||||
import config from "../../lib/config";
|
||||
import type { NextApiHandler } from "next";
|
||||
|
||||
const handler: NextApiHandler = async (req, res) => {
|
||||
const handler: NextApiHandler<
|
||||
{
|
||||
success?: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error?: any;
|
||||
} | null
|
||||
> = async (req, res) => {
|
||||
// only allow POST requests, otherwise return a 405 Method Not Allowed
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).send(null);
|
||||
@ -20,7 +25,13 @@ const handler: NextApiHandler = async (req, res) => {
|
||||
// all fields are required
|
||||
throw new Error("missing_data");
|
||||
}
|
||||
if (!data["h-captcha-response"] || !(await validateCaptcha(data["h-captcha-response"]))) {
|
||||
if (
|
||||
!data["cf-turnstile-response"] ||
|
||||
!(await validateCaptcha(
|
||||
data["cf-turnstile-response"],
|
||||
(req.headers["x-forwarded-for"] as string) || (req.headers["x-real-ip"] as string) || ""
|
||||
))
|
||||
) {
|
||||
// either the captcha is wrong or completely missing
|
||||
throw new Error("invalid_captcha");
|
||||
}
|
||||
@ -44,17 +55,17 @@ const handler: NextApiHandler = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
const validateCaptcha = async (formResponse: unknown): Promise<unknown> => {
|
||||
const response = await fetcher("https://hcaptcha.com/siteverify", {
|
||||
const validateCaptcha = async (formResponse: unknown, ip: string): Promise<unknown> => {
|
||||
const response = await fetcher("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: queryString.stringify({
|
||||
body: JSON.stringify({
|
||||
// fallback to dummy secret for testing: https://developers.cloudflare.com/turnstile/troubleshooting/testing/
|
||||
secret: process.env.TURNSTILE_SECRET_KEY || "1x0000000000000000000000000000000AA",
|
||||
response: formResponse,
|
||||
// fallback to dummy secret for testing: https://docs.hcaptcha.com/#integration-testing-test-keys
|
||||
sitekey: process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY || "10000000-ffff-ffff-ffff-000000000001",
|
||||
secret: process.env.HCAPTCHA_SECRET_KEY || "0x0000000000000000000000000000000000000000",
|
||||
remoteip: ip,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -106,34 +106,27 @@ const Privacy = () => {
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<H2 id="hcaptcha">Fighting Spam</H2>
|
||||
<H2 id="spam">Fighting Spam</H2>
|
||||
|
||||
<p>
|
||||
Using{" "}
|
||||
<Link href="https://www.hcaptcha.com/">
|
||||
<strong>hCaptcha</strong>
|
||||
<Link href="https://www.cloudflare.com/products/turnstile/">
|
||||
<strong>Cloudflare Turnstile</strong>
|
||||
</Link>{" "}
|
||||
to fight bot spam on the <Link href="/contact/">contact form</Link> was an easy choice over seemingly
|
||||
unavoidable alternatives like <Link href="https://developers.google.com/recaptcha/">reCAPTCHA</Link>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can refer to hCaptcha's <Link href="https://www.hcaptcha.com/privacy">privacy policy</Link> and{" "}
|
||||
<Link href="https://www.hcaptcha.com/terms">terms of service</Link> for more details. While some information
|
||||
is sent to the hCaptcha API about your behavior <strong>(on the contact page only)</strong>, at least you
|
||||
won't be helping a certain internet conglomerate{" "}
|
||||
You can refer to Cloudflare's <Link href="https://www.cloudflare.com/privacypolicy/">privacy policy</Link> and{" "}
|
||||
<Link href="https://www.cloudflare.com/website-terms/">terms of service</Link> for more details. While some
|
||||
information is sent to the Turnstile API about your behavior <strong>(on the contact page only)</strong>, at
|
||||
least you won't be helping a certain internet conglomerate{" "}
|
||||
<Link href="https://blog.cloudflare.com/moving-from-recaptcha-to-hcaptcha/">
|
||||
train their self-driving cars
|
||||
</Link>
|
||||
. 🚗
|
||||
</p>
|
||||
|
||||
<p>
|
||||
I also enabled the setting to donate 100% of my{" "}
|
||||
<Link href="https://humanprotocol.org/?lng=en-US">HMT token</Link> earnings to the{" "}
|
||||
<Link href="https://wikimediafoundation.org/">Wikimedia Foundation</Link>, for what it's worth. (A few cents,
|
||||
probably... 💰)
|
||||
</p>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
|
35
pnpm-lock.yaml
generated
35
pnpm-lock.yaml
generated
@ -11,9 +11,6 @@ importers:
|
||||
'@giscus/react':
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@hcaptcha/react-hcaptcha':
|
||||
specifier: ^1.10.1
|
||||
version: 1.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@libsql/client':
|
||||
specifier: ^0.6.2
|
||||
version: 0.6.2
|
||||
@ -125,6 +122,9 @@ importers:
|
||||
react-textarea-autosize:
|
||||
specifier: ^8.5.3
|
||||
version: 8.5.3(@types/react@18.3.3)(react@18.3.1)
|
||||
react-turnstile:
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-tweet:
|
||||
specifier: ^3.2.1
|
||||
version: 3.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@ -293,15 +293,6 @@ packages:
|
||||
react: ^16 || ^17 || ^18
|
||||
react-dom: ^16 || ^17 || ^18
|
||||
|
||||
'@hcaptcha/loader@1.2.4':
|
||||
resolution: {integrity: sha512-3MNrIy/nWBfyVVvMPBKdKrX7BeadgiimW0AL/a/8TohNtJqxoySKgTJEXOQvYwlHemQpUzFrIsK74ody7JiMYw==}
|
||||
|
||||
'@hcaptcha/react-hcaptcha@1.10.1':
|
||||
resolution: {integrity: sha512-P0en4gEZAecah7Pt3WIaJO2gFlaLZKkI0+Tfdg8fNqsDxqT9VytZWSkH4WAkiPRULK1QcGgUZK+J56MXYmPifw==}
|
||||
peerDependencies:
|
||||
react: '>= 16.3.0'
|
||||
react-dom: '>= 16.3.0'
|
||||
|
||||
'@humanwhocodes/config-array@0.11.14':
|
||||
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@ -2627,6 +2618,12 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
|
||||
react-turnstile@1.1.3:
|
||||
resolution: {integrity: sha512-nWgsnN2IgDSj91BK2iF/9GMVRJK0KPuDDxgnhs4o/7zfIRfyZG/ALWs+JJ8unW84MtFXpcEiPsookkd/FIb4aw==}
|
||||
peerDependencies:
|
||||
react: '>= 16.13.1'
|
||||
react-dom: '>= 16.13.1'
|
||||
|
||||
react-tweet@3.2.1:
|
||||
resolution: {integrity: sha512-dktP3RMuwRB4pnSDocKpSsW5Hq1IXRW6fONkHhxT5EBIXsKZzdQuI70qtub1XN2dtZdkJWWxfBm/Q+kN+vRYFA==}
|
||||
peerDependencies:
|
||||
@ -3308,15 +3305,6 @@ snapshots:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
'@hcaptcha/loader@1.2.4': {}
|
||||
|
||||
'@hcaptcha/react-hcaptcha@1.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.7
|
||||
'@hcaptcha/loader': 1.2.4
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
'@humanwhocodes/config-array@0.11.14':
|
||||
dependencies:
|
||||
'@humanwhocodes/object-schema': 2.0.3
|
||||
@ -6275,6 +6263,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
react-turnstile@1.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-tweet@3.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.11
|
||||
|
Loading…
x
Reference in New Issue
Block a user