Created
May 4, 2023 17:39
-
-
Save OptoCloud/19628eeb1f9523e254f5b83240bd9e59 to your computer and use it in GitHub Desktop.
SvelteKit CF Turnstile
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
}; | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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 {}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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