diff --git a/README.md b/README.md index e215bc4..3ab925d 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,15 @@ pnpm dev bun dev ``` +### Environment Variables + +Create a `.env.local` file in the project root with the following variables: + +```bash +NEXT_PUBLIC_RECAPTCHA_SITE_KEY=your_site_key_here +RECAPTCHA_SECRET_KEY=your_secret_key_here +``` + Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. diff --git a/app/api/recaptcha/route.ts b/app/api/recaptcha/route.ts new file mode 100644 index 0000000..eb647c2 --- /dev/null +++ b/app/api/recaptcha/route.ts @@ -0,0 +1,32 @@ +import { NextResponse } from 'next/server'; + +export async function POST(request: Request) { + const { token } = await request.json(); + if (!token) { + return NextResponse.json({ success: false, error: 'No CAPTCHA token provided' }, { status: 400 }); + } + + const secret = process.env.RECAPTCHA_SECRET_KEY; + if (!secret) { + console.error('RECAPTCHA_SECRET_KEY is not defined'); + return NextResponse.json({ success: false, error: 'Server misconfiguration' }, { status: 500 }); + } + + const params = new URLSearchParams(); + params.append('secret', secret); + params.append('response', token); + + const verificationResponse = await fetch('https://www.google.com/recaptcha/api/siteverify', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: params.toString(), + }); + + const verificationJson = (await verificationResponse.json()) as { success: boolean; [key: string]: any }; + + if (!verificationJson.success) { + return NextResponse.json({ success: false, error: 'CAPTCHA verification failed' }, { status: 400 }); + } + + return NextResponse.json({ success: true }); +} \ No newline at end of file diff --git a/components/faucet.tsx b/components/faucet.tsx index 6932f3e..bd71f01 100644 --- a/components/faucet.tsx +++ b/components/faucet.tsx @@ -11,6 +11,7 @@ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from ". import { Input } from "./ui/input"; import Link from "next/link"; import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle } from "./ui/alert-dialog"; +import ReCAPTCHA from "react-google-recaptcha"; export type NetworkType = "Devnet" | "Testnet"; @@ -40,6 +41,7 @@ export function Faucet({ network, setNetwork, evmAddressFromHeader }: FaucetProp const [showMissingRequirementsModal, setShowMissingRequirementsModal] = useState(false); const [showTxModal, setShowTxModal] = useState(false); const [showInvalidAddressModal, setShowInvalidAddressModal] = useState(false); + const [captchaToken, setCaptchaToken] = useState(null); const [chainId, setChainId] = useState(null); const ethereum = getEthereumProvider(); @@ -94,8 +96,21 @@ export function Faucet({ network, setNetwork, evmAddressFromHeader }: FaucetProp return; } + if (!captchaToken) { + alert("Please complete the CAPTCHA"); + return; + } setLoading(true); try { + const recaptchaResponse = await fetch('/api/recaptcha', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token: captchaToken }), + }); + const recaptchaResult = await recaptchaResponse.json(); + if (!recaptchaResponse.ok || !recaptchaResult.success) { + throw new Error(recaptchaResult.error || 'CAPTCHA verification failed'); + } const txHash = await getXrp(evmAddress); const closeTimeIso = new Date().toISOString(); setTxData({ txHash, sourceCloseTimeIso: closeTimeIso }); @@ -292,6 +307,12 @@ export function Faucet({ network, setNetwork, evmAddressFromHeader }: FaucetProp {socialsCompleted.discord && "✅"} +
+ setCaptchaToken(token)} + /> +