mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-08 04:57:36 +00:00
refactor(turnstile-spin): tighten auth handling, expand documentation, and add tldraw-offline matcher
This commit is contained in:
1 parent
20dc5f4489
commit
03ef5c94ea
14 files changed
+1559
-499
No files matched your search
@@ -1,70 +1,100 @@
|
||||
# SvelteKit
|
||||
|
||||
For SvelteKit projects. Widget renders in the page; siteverify can be called from a form action server-side, or directly against the Worker from the client.
|
||||
For SvelteKit projects. The widget renders in the page; siteverify is called from a SvelteKit form action (or a `+server.ts` endpoint) server-side.
|
||||
|
||||
```svelte title="src/routes/signup/+page.svelte"
|
||||
<script lang="ts">
|
||||
<script>
|
||||
import { enhance } from "$app/forms";
|
||||
import { onMount } from "svelte";
|
||||
let token = "";
|
||||
|
||||
let turnstileContainer;
|
||||
let signupWidgetId;
|
||||
|
||||
onMount(() => {
|
||||
(window as any).onTurnstileSuccess = (t: string) => (token = t);
|
||||
});
|
||||
const render = () => {
|
||||
signupWidgetId = window.turnstile.render(turnstileContainer, {
|
||||
sitekey: "YOUR_SITEKEY",
|
||||
action: "signup",
|
||||
});
|
||||
delete window.onSignupTurnstileLoad;
|
||||
};
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
const res = await fetch("https://YOUR_WORKER_URL/", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
// proceed
|
||||
if (window.turnstile) {
|
||||
render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
window.onSignupTurnstileLoad = render;
|
||||
const script = document.createElement("script");
|
||||
script.src =
|
||||
"https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onSignupTurnstileLoad&render=explicit";
|
||||
script.async = true;
|
||||
document.head.appendChild(script);
|
||||
|
||||
return () => {
|
||||
delete window.onSignupTurnstileLoad;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<script
|
||||
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
|
||||
async
|
||||
defer
|
||||
></script>
|
||||
</svelte:head>
|
||||
|
||||
<form on:submit={handleSubmit}>
|
||||
<form
|
||||
method="POST"
|
||||
use:enhance={() => {
|
||||
return async ({ result, update }) => {
|
||||
try {
|
||||
await update();
|
||||
} finally {
|
||||
if (result.type !== "redirect" && signupWidgetId !== undefined) {
|
||||
window.turnstile.reset(signupWidgetId);
|
||||
}
|
||||
}
|
||||
};
|
||||
}}
|
||||
>
|
||||
<input name="email" type="email" required />
|
||||
<div
|
||||
class="cf-turnstile"
|
||||
data-sitekey="YOUR_SITEKEY"
|
||||
data-action="turnstile-spin-v1"
|
||||
data-callback="onTurnstileSuccess"
|
||||
></div>
|
||||
<button type="submit" disabled={!token}>Sign up</button>
|
||||
<div bind:this={turnstileContainer}></div>
|
||||
<button type="submit">Sign up</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
## Variant: Form action server-side
|
||||
|
||||
If you use SvelteKit's form actions for the signup logic, do siteverify from the action:
|
||||
Form action (canonical siteverify):
|
||||
|
||||
```ts title="src/routes/signup/+page.server.ts"
|
||||
import type { Actions } from "./$types";
|
||||
import { fail } from "@sveltejs/kit";
|
||||
import { TURNSTILE_SECRET, TURNSTILE_HOSTNAMES } from "$env/static/private";
|
||||
|
||||
const expectedHostnames = new Set(
|
||||
(TURNSTILE_HOSTNAMES ?? "")
|
||||
.split(",")
|
||||
.map((h) => h.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request }) => {
|
||||
default: async ({ request, getClientAddress }) => {
|
||||
const data = await request.formData();
|
||||
const token = data.get("cf-turnstile-response") as string;
|
||||
const token = data.get("cf-turnstile-response");
|
||||
if (typeof token !== "string" || expectedHostnames.size === 0) {
|
||||
return fail(403, { error: "Verification failed" });
|
||||
}
|
||||
|
||||
const verify = await fetch("https://YOUR_WORKER_URL/", {
|
||||
const verify = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ token }),
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
secret: TURNSTILE_SECRET,
|
||||
response: token,
|
||||
remoteip: getClientAddress(),
|
||||
}),
|
||||
});
|
||||
const result = await verify.json();
|
||||
if (!result.success) {
|
||||
if (
|
||||
verify.ok !== true ||
|
||||
result.success !== true ||
|
||||
result.action !== "signup" ||
|
||||
!expectedHostnames.has(result.hostname)
|
||||
) {
|
||||
return fail(403, { error: "Verification failed" });
|
||||
}
|
||||
|
||||
@@ -74,11 +104,88 @@ export const actions: Actions = {
|
||||
};
|
||||
```
|
||||
|
||||
With this variant the form posts via SvelteKit's progressive enhancement; the page component drops the explicit `fetch` call and just uses a native `<form method="POST">`.
|
||||
`signup` is the stable action for this surface. Preserve an existing custom migration action and compare the returned action to the same value. Siteverify is mandatory for every widget mode, including pre-clearance. Set `TURNSTILE_HOSTNAMES` to the deployment-specific frontend hostnames; a production value must not include `localhost` or `127.0.0.1`.
|
||||
|
||||
In `.env`:
|
||||
|
||||
```text
|
||||
TURNSTILE_SECRET=YOUR_SECRET
|
||||
```
|
||||
|
||||
The `$env/static/private` import enforces that the secret never reaches the client bundle.
|
||||
|
||||
## Variant: client-side fetch to an endpoint
|
||||
|
||||
If you need a JSON API rather than progressive-enhancement form post, use `+server.ts`:
|
||||
|
||||
```ts title="src/routes/api/signup/+server.ts"
|
||||
import type { RequestHandler } from "./$types";
|
||||
import { TURNSTILE_SECRET, TURNSTILE_HOSTNAMES } from "$env/static/private";
|
||||
|
||||
const expectedHostnames = new Set(
|
||||
(TURNSTILE_HOSTNAMES ?? "")
|
||||
.split(",")
|
||||
.map((h) => h.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
export const POST: RequestHandler = async ({ request, getClientAddress }) => {
|
||||
const { token } = await request.json();
|
||||
if (expectedHostnames.size === 0) {
|
||||
return new Response("forbidden", { status: 403 });
|
||||
}
|
||||
const verify = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
secret: TURNSTILE_SECRET,
|
||||
response: token,
|
||||
remoteip: getClientAddress(),
|
||||
}),
|
||||
});
|
||||
const result = await verify.json();
|
||||
if (
|
||||
verify.ok !== true ||
|
||||
result.success !== true ||
|
||||
result.action !== "signup" ||
|
||||
!expectedHostnames.has(result.hostname)
|
||||
) {
|
||||
return new Response("forbidden", { status: 403 });
|
||||
}
|
||||
// process signup
|
||||
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
||||
};
|
||||
```
|
||||
|
||||
The explicit renderer above retains `signupWidgetId`. Reset it in `finally` when calling this endpoint so every completion path gets a fresh token:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
async function submit(e) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const token = new FormData(e.currentTarget).get("cf-turnstile-response");
|
||||
const res = await fetch("/api/signup", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (!res.ok || result.ok !== true) throw new Error("Submission failed");
|
||||
// proceed
|
||||
} catch {
|
||||
// surface error
|
||||
} finally {
|
||||
if (signupWidgetId !== undefined) {
|
||||
window.turnstile.reset(signupWidgetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Substitutions
|
||||
|
||||
| Placeholder | Replace with |
|
||||
| ------------------ | ------------------------------------------- |
|
||||
| `YOUR_WORKER_URL` | Deployed Worker URL from Step 5 |
|
||||
| `YOUR_SITEKEY` | Widget site key from Step 4 |
|
||||
| Placeholder | Replace with |
|
||||
| ------------------- | -------------------------------------------------------------------- |
|
||||
| `YOUR_SITEKEY` | The widget site key from Step 8 |
|
||||
| `YOUR_SECRET` | The secret captured in Step 8. Stays in env, never inlined. |
|
||||
Reference in new issue
Block a user