Skip to content

Instantly share code, notes, and snippets.

@OptoCloud
Created May 4, 2023 17:39
Show Gist options
  • Save OptoCloud/19628eeb1f9523e254f5b83240bd9e59 to your computer and use it in GitHub Desktop.
Save OptoCloud/19628eeb1f9523e254f5b83240bd9e59 to your computer and use it in GitHub Desktop.
SvelteKit CF Turnstile
import { Turnstile } from '$lib/server/cloudflare/index.js';
import { TurnstileUserErrorMessage } from '$lib/server/cloudflare/turnstile.js';
import { fail } from '@sveltejs/kit';
/** @type {import('./$types').Actions} */
export const actions = {
default: async ({ request }) => {
const body = await request.formData();
// Validate turnstile
const cfResponse = await Turnstile.ValidateToken(body, request.headers);
if (!cfResponse.success) {
return fail(400, {
error: true,
message: TurnstileUserErrorMessage(cfResponse),
});
}
return {
success: true,
};
}
};
<script lang="ts">
import Turnstile from '$components/Turnstile.svelte';
let turnstileToken: string | null = null;
</script>
<form class="flex flex-col space-y-4" method="post">
<Turnstile action="action-name" bind:response={turnstileToken} />
<button type="submit">
<span>Submit</span>
</button>
</form>
/// <reference types="svelte" />
/// <reference types="vite/client" />
import type { TurnstileInstance } from '$types/TurnstileInstance';
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
interface Window {
turnstile: TurnstileInstance | undefined;
}
}
export {};
<script lang="ts">
import { PUBLIC_TURNSTILE_SITE_KEY } from '$env/static/public';
import type { TurnstileInstance } from '$types/TurnstileInstance';
import { modeCurrent } from '@skeletonlabs/skeleton';
import { onMount } from 'svelte';
export let action: string;
export let cData: string | undefined = undefined;
export let response: string | null = null;
let element: HTMLDivElement;
function resetResponse() {
response = null;
// Reset the widget after 1 second to prevent the user from spamming the button
setTimeout(() => turnstile?.reset(element), 1000);
}
// If turstile doesnt load, then the index.html is proabably missing the script tag (https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#explicitly-render-the-turnstile-widget)
let turnstile: TurnstileInstance | undefined;
onMount(() => (turnstile = window.turnstile));
let isLoaded = false;
$: if (turnstile && !isLoaded) turnstile.ready(() => (isLoaded = true));
$: if (turnstile && isLoaded) {
turnstile.render(element, {
sitekey: PUBLIC_TURNSTILE_SITE_KEY,
action,
cData,
theme: $modeCurrent ? 'light' : 'dark',
callback: (token) => (response = token),
'expired-callback': resetResponse,
'timeout-callback': resetResponse,
'error-callback': resetResponse,
});
}
</script>
<!-- see: https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#widget-size -->
<div class="h-[65px] w-[300px]" bind:this={element}>
{#if !isLoaded}
<p>Loading...</p>
{/if}
</div>
import { TURNSTILE_SECRET_KEY } from '$env/static/private';
type TurnStileErrorCode
= 'missing-input-secret'
| 'invalid-input-secret'
| 'missing-input-response'
| 'invalid-input-response'
| 'bad-request'
| 'timeout-or-duplicate'
| 'internal-error';
interface TurnstileResponse {
success: boolean;
challenge_ts?: string;
hostname?: string;
'error-codes': TurnStileErrorCode[];
action?: string;
cdata?: string;
}
// https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
async function ValidateToken(
body: FormData,
headers: Headers
): Promise<TurnstileResponse> {
// Turnstile injects a token in "cf-turnstile-response".
const token = body.get('cf-turnstile-response')?.toString();
if (!token) {
return { success: false, 'error-codes': ['missing-input-response'] };
}
const ip = headers.get('CF-Connecting-IP')?.toString();
if (!ip) {
console.error('CF-Connecting-IP header is missing');
return { success: false, 'error-codes': ['bad-request'] };
}
// Validate the token by calling the
// "/siteverify" API endpoint.
const formData = new FormData();
formData.append('secret', TURNSTILE_SECRET_KEY);
formData.append('response', token);
formData.append('remoteip', ip);
let retry = false;
let retryCount = 0;
let outcome: TurnstileResponse;
do {
const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
const result = await fetch(url, {
body: formData,
method: 'POST',
});
outcome = (await result.json()) as TurnstileResponse;
if (!outcome.success) {
// If we got a error without error-codes, it's an internal error.
const errorCodes = outcome['error-codes'];
if (!errorCodes || errorCodes.length === 0) {
return { success: false, 'error-codes': ['internal-error'] };
}
retry = errorCodes.includes('internal-error') && retryCount++ < 3;
}
} while (retry);
return outcome;
}
function TurnstileUserErrorMessage(response: TurnstileResponse): string {
if (response.success) {
return 'Success';
}
const errorCodes = response['error-codes'];
if (errorCodes.includes('internal-error')) {
return 'Internal Server Error';
}
let message;
switch (errorCodes[0]) {
case 'missing-input-response':
message = 'Missing turnstile response';
break;
case 'invalid-input-response':
message = 'Invalid turnstile response';
break;
case 'bad-request':
message = 'Bad request';
break;
default:
message = 'Unknown error';
break;
}
return message;
}
export { ValidateToken, TurnstileUserErrorMessage, type TurnstileResponse };
import type { TurnstileRenderParameters } from './TurnstileRenderParameters';
export interface TurnstileInstance {
execute: (
container: string | HTMLElement,
jsParams: TurnstileRenderParameters
) => Promise<string>;
getResponse: (container: string | HTMLElement) => string;
implicitRender: () => void;
ready: (callback: (token: string) => void) => void;
remove: (container: string | HTMLElement) => void;
render: (
container: string | HTMLElement,
parameters: TurnstileRenderParameters
) => void;
reset: (container: string | HTMLElement) => void;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment