
Tengo un video de cómo añadir el re-captcha de Google a tu proyecto web, aquí te lo dejo, pero, en esta entrada, te voy a mostrar lo que yo hago para implementar esta herramienta de Cloudflare llamada: Turnstile, y, así, ****asegurarme de que mis formularios sean usados solo por usuarios reales. 👶🏻
Haremos la integración paso a paso. En cada paso te voy ir mostrando cómo uso yo las rutas de recursos de React Router, cómo las consumo con fetcher
y cómo obtengo la respuesta para usarla en el cliente también, y hasta cómo podemos evitar el redireccionamiento natural usando el componente <fetcher.Form/>
. 🤯
Además, te voy a mostrar cómo suelo cargar <scripts>
de terceros con React y mi custom hook: useScript
. 😬
Y pos ya, seguro que esto te interesa, tráete las nueces, el chocolate semi-amargo y comencemos pues. 🍫👩🏻🔧
👀 Pequeño disclaimer: El código está simplificado para fines educativos, este documento pretende ser solo una sugerencia de piezas, te recomiendo ampliamente leer la documentación oficial con calma. 🗒️
🤖 Comencemos con el servidor
Yo ya tengo algunas rutas de recursos en el repo de easybits.cloud, que es open-source, así que, simplemente añadiré una coincidencia, yo les llamo “intents”, se los copié a Ryan Florence, creo... 🤨
Así, solo añadimos un bloque if
en mi ruta existente routes/api/v1/utils.ts
.
// routes/api/v1/utils.ts export const action = async ({ request }: Route.ActionArgs) => { const formData = await request.formData(); const intent = formData.get("intent"); // Este es el intent if (intent === "send_confirmation") { const email = formData.get("email") as string; z.string().email().parse(email); // <= zod parsing (email validation) const success = await handleTurnstilePost(request, formData); // body está ya consumido // turnstile validation (bot validation) ^ if (!success) throw new Response("Pensamos que eres un robot. 🤖 No puedes pasar.", { status: 401, }); // En este punto, la validación fue existosa. await sendConfrimation(email); return { success }; } if (intent === "test_action_email") { // ...
La lógica dentro de nuestro bloque if
aquí, es muy sencilla: esperamos la respuesta booleana que obtendremos de la función handleTurnstilePost
, si la respuesta es false
detonamos Unauthorized
, o enviamos el correo y le devolvemos un objeto con la llave success al fetcher
así, el cliente mostrará el confeti. 🎉
Veamos entonces todo lo que handleTurnstilePost
tiene que hacer para validar el token inyectó el script en el objeto formData
. 👀
🪪 Esta es la verdadera validación en el servidor
Tampoco es para tanto, no es nada más que una típica petición post. 🦾
export const handleTurnstilePost = async (request: Request, body: FormData) => { // Se valida el token llamando al // "/siteverify" API endpoint. const url = "https://challenges.cloudflare.com/turnstile/v0/siteverify"; const result = await fetch(url, { body: new URLSearchParams({ secret: process.env.TURNSTILE_SECRET!, response:body.get("cf-turnstile-response")!, // Turnstile inyecta un token en "cf-turnstile-response". remoteip:request.headers.get("CF-Connecting-IP")!, }), method: "POST", }); const outcome = (await result.json()) as { success: boolean }; console.info("::TURNSTILE_SITEVERIFY_RESPONSE::", outcome); // INFO return outcome.success; };
Aquí, prefiero emplear URLSearchParams
que se pueden serializar a FormData
, así no tengo que usar el método .set()
del FormData
para construirlo y escribo mucho menos código. Me da menos hueva armar un objeto. 🥚🔫
Necesitamos la variable de entorno TURNSTILE_SECRET
, que podremos encontrar en nuestro dashboard ya que añadamos un nuevo Widget
. Cópiatelo. 🧻
Esto es todo lo que hay en el servidor, es momento de cargar el script del cliente que hará su magia para generar el token que el action
ya esta esperando y validando.
Este *script* colocará el token como parte del formulario que se quiere asegurar
. 🔒
📼 useScript
colocará un <script>
en modo defer
y async
dentro de <head>
Así es, y es un hook bien sencillo.
import { useEffect } from "react"; export const useScript = (url: string, cb?: (arg0: Event) => void) => { useEffect(() => { const s = document.createElement("script"); s.async = true; s.defer = true; s.src = url; document.head.appendChild(s); s.onload = (e) => { // Esto es opcional cb?.(e); }; }, []); };
Este hook está super simplificado para este caso específico, pero podrías añadir la opción de poblar el textContent
. 🤓 Usaremos este hook de la siguiente manera.
Creando un componente <Turnstile/> reutilizable ✅
Podemos fácilmente crear un componente en el que juntaremos la carga del script y los atributos necesarios para que el script inyecte lo necesario en estos nodos <div>
.
export const Turnstile = () => { useScript("https://challenges.cloudflare.com/turnstile/v0/api.js"); return ( <div className="fixed bottom-1 right-1 z-50"> <div className="cf-turnstile" data-sitekey="0x4FFFFFAMBbVWGBqyYY47hTw" // <= Aquí va tu "Site Key" data-theme="light" ></div> </div> ); };
Observa que hemos pasado el url
del script de Turnstile para que nuestro custom hook añada un nodo script a la cabecera de la página y este cargue. 📑
¡Ya es momento de echarlo a andar! 🚘
Usando nuestro componente en un formulario
Este otro componente es fácil de leer. Se llama SuscriptionBox y se puede usar en todo el sitio en cualquiera de las rutas, porque es autónomo en su comunicación con el servidor gracias al fetcher
. 🫨
export const SuscriptionBox = ({ className }: { className?: string }) => { const fetcher = useFetcher(); const isSuccess = fetcher.data?.success; return ( <section className={cn( "max-w-3xl h-fit md:h-72 border-black border-[2px] overflow-hidden md:rounded-t-full bg-coverSuscription rounded-r-3xl rounded-t-3xl md:rounded-r-full bg-cover mx-auto p-6 md:p-8 justify-center relative", className )} > <div className="w-full text-center relative"> <h3 className="text-2xl md:text-3xl font-bold"> Suscríbete a nuestro newsletter </h3> <p className="text-base md:text-xl mt-2 md:mt-3 max-w-4xl mx-auto"> Recibe un resumen mensual de las mejores consejos de marketing y business para creadores, o de las nuevas funcionalidades nuevas de EasyBits. </p> {!isSuccess && ( <fetcher.Form action="/api/v1/utils" method="post" className="flex gap-4 max-w-2xl mx-auto mt-10 flex-wrap md:flex-nowrap justify-center" > <input name="email" required className="bg-white rounded-xl w-full border-2 border-black " placeholder="ejemplo@easybist.cloud" />{" "} <BrutalButton isLoading={fetcher.state !== "idle"} name="intent" value="send_confirmation" type="submit" containerClassName=" -mt-[2px] ml-[1px]" id="Suscripcion" > ¡Apuntarme! </BrutalButton> <Turnstile /> </fetcher.Form> )} {isSuccess && ( <p className="text-xl mt-2 md:mt-3 font-bold text-brand-500"> ¡Super! Ahora, revisa tu correo para confirmar tu cuenta. 🎊 </p> )} {isSuccess && <BrendisConfetti />} </div> </section> ); };
Hay mucho qué observar aquí, pero, podemos comenzar con que declaramos al fetcher
y también declaramos una variable isSuccess
que como se define directamente en el scope del componente, cada que el fetcher cambie se estará redefiniendo, asegurándonos así de que el dato será fresco. 💦
Yo suelo definir muchas variables así. Ahora, mira hasta abajo del componente, encontrarás que isSuccess
se usa para mostrar el confeti y ocultar el formulario. 😶🌫️
Toda la información para comunicarse con el servidor está presente en las propiedades del <fetcher.Form>
, a este estilo se le conoce como declarativo y se evita tener que usar una función para prevenir el default del onsubmit. ✅
action
Indica nuestro endpoint donde está el bloqueif
con elintent
que creamos al principio.method
Indica el tipo de petición HTTP, debe serpost
,put
,patch
odelete
para detonar elaction
(o la ruta de recursos).- El
<input>
al tener el atributoname
será incluido en la petición dentro de unformData
, este es el comportamiento natural de un input dentro de un formulario web. - Pasa lo mismo con el
<button>
que también lleva el atributoname
con el valorsend_confirmation
, será parte delformData
y se usará para coincidir con un bloqueif
dentro de la funciónaction
. 🤯
Si necesitas el token en el cliente antes, puedes usar el render explícito.
export const Turnstile = ({ setIsDisabled, }: { setIsDisabled?: (arg0: boolean) => void; }) => { const ref = useRef(null); useScript("https://challenges.cloudflare.com/turnstile/v0/api.js", () => { window.turnstile?.ready(() => { if (ref.current.id) return; ref.current.id = "loaded"; // avoiding duplication window.turnstile?.render(ref.current, { callback: enable, sitekey: "0x4AATRTYUbVIYBqxLL33hTw", }); }); }); const enable = (token: string) => { console.log(token); if (token) { setIsDisabled?.(false); // habilitamos o desabilitamos el botón del form } else { setIsDisabled?.(true); } }; return <div ref={ref} className="fixed bottom-0 right-0 z-50" />; };
¡Y ya está! Nos hemos asegurado de que las peticiones a este formulario sean auténticas y ahora podemos emplear nuestro componente <Turnstile />
en conjunto con la función handleTurnstilePost
en cualquier formulario público de nuestro sitio web. ✅
Bien hecho Geek. Espero esto te sea de utilidad, nos leemos pronto. 🤘🏼
Abrazo. Bliss. 🤓
Enlaces relacionados

2 maneras muy simples de mejorar tus entrevistas de trabajo.
Checa este otro Post
