turnstile added

This commit is contained in:
2026-05-28 10:24:23 -04:00
parent 8b6551330f
commit fbc8215410
6 changed files with 165 additions and 10 deletions

View File

@@ -19,9 +19,10 @@ import {
fetchWithRetry,
NetworkError,
TimeoutError,
APIError
APIError,
verifyTurnstileToken
} from "~/server/fetch-utils";
import { NETWORK_CONFIG, COOLDOWN_TIMERS, VALIDATION_CONFIG } from "~/config";
import { NETWORK_CONFIG, COOLDOWN_TIMERS, VALIDATION_CONFIG, TURNSTILE_CONFIG } from "~/config";
const assets: Record<string, string> = {
"shapes-with-abigail": "shapes-with-abigail.apk",
"magic-delve": "magic-delve.apk",
@@ -304,10 +305,27 @@ export const miscRouter = createTRPCRouter({
message: z
.string()
.min(1)
.max(VALIDATION_CONFIG.MAX_CONTACT_MESSAGE_LENGTH)
.max(VALIDATION_CONFIG.MAX_CONTACT_MESSAGE_LENGTH),
turnstileToken: z.string().min(1, "Please complete the security check")
})
)
.mutation(async ({ input }) => {
// Verify Cloudflare Turnstile token
const turnstileValid = await verifyTurnstileToken(
input.turnstileToken,
env.TURNSTILE_SECRET_KEY,
TURNSTILE_CONFIG.VERIFY_URL,
TURNSTILE_CONFIG.RESPONSE_TIMEOUT_MS
);
if (!turnstileValid) {
console.error("Turnstile verification failed for contact form submission");
throw new TRPCError({
code: "FORBIDDEN",
message: "Security verification failed. Please refresh the page and try again."
});
}
const contactExp = getCookie("contactRequestSent");
let remaining = 0;

View File

@@ -135,3 +135,56 @@ export async function fetchWithRetry<T>(
throw lastError;
}
// ============================================================
// CLOUDFLARE TURNSTILE VERIFICATION
// ============================================================
interface TurnstileResponse {
success: boolean;
"challenge-ts"?: string;
action?: string;
cdata?: string;
"error-codes"?: string[];
}
export async function verifyTurnstileToken(
token: string,
secretKey: string,
verifyUrl: string = "https://challenges.cloudflare.com/turnstile/v0/siteverify",
timeoutMs: number = 10000
): Promise<boolean> {
if (!token || token.trim() === "") {
return false;
}
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
const response = await fetch(verifyUrl, {
method: "POST",
body: new URLSearchParams({
secret: secretKey,
response: token,
}),
headers: {
"content-type": "application/x-www-form-urlencoded",
},
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
console.error(`Turnstile verification failed with status ${response.status}`);
return false;
}
const data = (await response.json()) as TurnstileResponse;
return data.success === true;
} catch (error) {
console.error("Turnstile verification error:", error);
return false;
}
}