TL/DR: If you read the title and know exactly what I mean already, simply go to https://deno.land/x/fresh_turnstile and follow the README.
This is part 2 of a series about my Deno Fresh plugins. If you are not sure about some of the topics mentioned here, you are very likely to find them explained in part 1. In this post, I will continue to discuss the remaining use cases supported by the first release of my Turnstile plugin.
Use Case 2: JavaScript-based form submission.
In the previous post, we discussed how to protect a form traditionally submitted. Now imagine the same form, but submitted via AJAX / fetch / REST APIs / GraphQL / etc.
You can use the implicit rendered <CfTurnstile/>
component similar to use case 1, but this time we will add an ID and a callback property.
import { useId, useState } from "preact/hooks";
import CfTurnstile from "$turnstile/components/CfTurnstile.tsx";
// ...
const turnstileElementId = useId();
const [turnstileToken, setTurnstileToken] = useState("");
// ...
<CfTurnstile id={turnstileElementId} sitekey="..." callback={setTurnstileToken} />
By simply adding the callback, you can easily access the token at your turnstileToken
state variable, which will be set as soon as the Turnstile widget decides that the actor is a human.
However, the token can only be validated at most once, so if the form submission fails for some reason when validating on the server side, you will need a way to retrieve another token. That's where the ID is useful. Get access to the turnstile
object with the getTurnstileAsync
helper function and call reset with the ID.
import { getTurnstileAsync } from "$turnstile/plugin.ts";
// ... imagine this is inside your form submission action
try {
if (!turnstileToken) {
throw new Error("Turnstile is not ready");
}
await submit_the_form_with_ajax_or_fetch({
// other form fields
"cf-turnstile-response": turnstileToken
});
} catch (e) {
// form submission fails, get a new Turnstile token to be ready for the next try
setTurnstileToken("");
const turnstile = await getTurnstileAsync();
turnstile.reset(turnstileElementId);
}
Now your form is ready for the next submission without having to reload / re-render the page.
Use Case 3: custom interactions.
By now you should have had a fairly good idea on how to do this already, but for completeness's sake let's put it down in writing so that there won't be any misunderstanding.
Similar to the previous case, use the component with an ID and a callback. To demonstrate the idea that you can use the component as freely as any preact component you write yourself, I will use a signal in this example instead of a state variable.
import { useId } from "preact/hooks";
import { useSignal } from "@preact/signals";
import CfTurnstile from "$turnstile/components/CfTurnstile.tsx";
// ...
const turnstileElementId = useId();
const turnstileToken = useSignal("");
// ...
<CfTurnstile id={turnstileElementId} sitekey="..." callback={(token) => turnstileToken.value = token} />
And again, inspect your signal and use the getTurnstileAsync
be able to reset the widget as needed.
import { getTurnstileAsync } from "$turnstile/plugin.ts";
// ... imagine this is inside your custom interaction
try {
if (!turnstileToken.value) {
throw new Error("Turnstile is not ready");
}
await validate_token_on_server_side({
"token": turnstileToken.value}
);
} catch (e) {
// validation fails, get a new Turnstile token to be ready for the next try
turnstileToken.value = "";
const turnstile = await getTurnstileAsync();
turnstile.reset(turnstileElementId);
}
Note that in order to validate the token, you need to make a call to Turnstile API with your Turnstile site's secret key (see official instructions here). It is not a good idea to retrieve this secret client-side at any point in time. Instead, I highly recommend sending the token to the server side to perform the validation step.
BONUS: explicit rendering
The token from Turnstile has a relatively short validity period (5 minutes in v0). Therefore, if you want to use it with a blog post, an email, or an otherwise time-consuming form, you may want to reset the widget right before form submission.
Alternatively, you may choose to control when the widget gets rendered and time it according to your specificities. The idea is similar to what we've done before: leveraging the useId
hook and the getTurnstileAsync
helper. I am going to show a very simple example resembling use case 1 except that our user needs to click the "Check my form" button first before zhe can "submit" it.
import { useId } from "preact/hooks";
import { useSignal, computed } from "@preact/signals";
// ...
const turnstileElementId = useId();
const turnstileToken = useSignal("");
const formReady = computed(() => turnstileToken !== "");
const startTurnstile = async () => {
const turnstile = await getTurnstileAsync();
turnstile.render(
turnstileElementId,
{
sitekey: "...",
callback: (token) => turnstileToken.value = token
}
);
// ...
<form action="..." method="POST">
// other form fields such as name, email, message, etc.
<div id={turnstileElementId} />
<button type="button" onClick={startTurnstile}>Check my form</button>
<input type="submit" disabled={!formReady} />
</form>
Once the "check my form" button is clicked, the widget rendering will kick in. Once it has determined that the actor is a human, it will invoke the callback with the token, which is sent to the turnstileToken
signal, which is chained to the formReady
signal, turning the "submit" button from its disabled state into enabled. At this point, you have the token to use dynamically as needed; you also have the hidden input named cf-turnstile-token
inside your <div/>
ready to be submitted alongside your other form fields.
This approach is very flexible. I'm sure you will find a creative way to solve most of your captcha use cases with this approach if the basic use cases don't fit your bill. Feel free to drop a line and share your insight, or prove me wrong with an unsolvable use case, I challenge you 😉.
That's it, my friends. Thank you for staying with me till the end of it. As I've stated in the beginning this is the first release, so I am sure there are a lot more things I have not covered. Feel free to start a discussion or file an issue! You may also hit me up on dev.to messaging anytime too!
Top comments (1)
Can a fresh app be hosted on cloudflare?