UNPKG

nextjs-turnstile

Version:

Integrate Cloudflare Turnstile CAPTCHA in Next.js applications

336 lines (330 loc) 11.2 kB
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // src/components/TurnstileImplicit.tsx import { useEffect, useRef } from "react"; // src/utils/index.ts var usedContainerIds = /* @__PURE__ */ new Set(); var usedResponseFieldNames = /* @__PURE__ */ new Set(); function loadTurnstileScript(mode = "implicit") { if (typeof window === "undefined") return Promise.resolve(); const cacheKey = `__turnstile_promise_${mode}`; if (window[cacheKey]) { return window[cacheKey]; } window[cacheKey] = new Promise((resolve, reject) => { const srcBase = "https://challenges.cloudflare.com/turnstile/v0/api.js"; if (document.querySelector(`script[src^="${srcBase}"]`)) { const interval = setInterval(() => { if (window.turnstile) { clearInterval(interval); resolve(); } }, 100); return; } const script = document.createElement("script"); script.async = true; script.defer = true; script.onerror = () => reject(new Error("Turnstile script failed to load")); if (mode === "explicit") { const onloadCallbackName = `__cfTurnstileOnload_${Math.random().toString(36).slice(2)}`; script.src = `${srcBase}?render=explicit&onload=${onloadCallbackName}`; window[onloadCallbackName] = () => { resolve(); delete window[onloadCallbackName]; }; } else { script.src = srcBase; script.onload = () => resolve(); } document.head.appendChild(script); }); return window[cacheKey]; } function isTurnstileLoaded() { return typeof window !== "undefined" && typeof window.turnstile !== "undefined"; } function resetTurnstile(widget) { if (!isTurnstileLoaded()) return; try { window.turnstile.reset(widget); } catch { } } function executeTurnstile(widget) { if (!isTurnstileLoaded()) return; let resp; try { resp = window.turnstile.getResponse(widget); } catch (err) { window.turnstile.render(widget); } } function getTurnstileResponse(widget) { if (!isTurnstileLoaded()) return null; try { return window.turnstile.getResponse(widget) || null; } catch (err) { console.error("Turnstile getResponse error:", err); return null; } } function removeTurnstile(ref) { if (!isTurnstileLoaded()) return; try { window.turnstile.remove(ref); } catch { } } // src/components/TurnstileImplicit.tsx import { Fragment, jsx } from "react/jsx-runtime"; function TurnstileImplicit({ siteKey, theme = "auto", size = "normal", responseFieldName = "cf-turnstile-response", refreshExpired = "auto", refreshTimeout = "auto", className, onSuccess, onExpire, onTimeout, onError }) { var _a; const hostRef = useRef(null); const widgetId = useRef(null); const resolvedKey = (_a = siteKey != null ? siteKey : process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY) != null ? _a : ""; const containerId = `cft-${responseFieldName}`; const CB_PREFIX = `turnstile_${responseFieldName}`; const cbNames = { verify: `${CB_PREFIX}_verify`, error: `${CB_PREFIX}_error`, expire: `${CB_PREFIX}_expire`, timeout: `${CB_PREFIX}_timeout` }; useEffect(() => { if (usedResponseFieldNames.has(responseFieldName)) { throw new Error( `Duplicate responseFieldName "${responseFieldName}" detected. Each <TurnstileImplicit> on the page must use a unique name.` ); } usedResponseFieldNames.add(responseFieldName); if (usedContainerIds.has(containerId)) { throw new Error( `Duplicate containerId "${containerId}" detected. Each <TurnstileImplicit> on the page must use a unique responseFieldName.` ); } usedContainerIds.add(containerId); window[cbNames.verify] = (token) => onSuccess == null ? void 0 : onSuccess(token); window[cbNames.error] = () => onError == null ? void 0 : onError(); window[cbNames.expire] = () => { onExpire == null ? void 0 : onExpire(); }; window[cbNames.timeout] = () => { onTimeout == null ? void 0 : onTimeout(); }; if (!hostRef.current) return; loadTurnstileScript("implicit").then(() => { }).catch((err) => onError == null ? void 0 : onError()); return () => { usedResponseFieldNames.delete(responseFieldName); usedContainerIds.delete(containerId); if (widgetId.current) { window.turnstile.remove(widgetId.current); widgetId.current = null; } window[cbNames.verify] = void 0; window[cbNames.error] = void 0; window[cbNames.expire] = void 0; window[cbNames.timeout] = void 0; }; }, [ resolvedKey, theme, size, responseFieldName, refreshExpired, refreshTimeout, onSuccess, onError, onExpire, onTimeout, cbNames.verify, cbNames.error, cbNames.expire, cbNames.timeout ]); return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx( "div", { id: containerId, ref: hostRef, className: `cf-turnstile ${className != null ? className : ""}`, "data-sitekey": resolvedKey, "data-theme": theme, "data-size": size, "data-response-field-name": responseFieldName, "data-callback": cbNames.verify, "data-error-callback": cbNames.error, "data-expired-callback": cbNames.expire, "data-timeout-callback": cbNames.timeout } ) }); } // src/components/TurnstileExplicit.tsx import { useEffect as useEffect2, useRef as useRef2, useState } from "react"; import { jsx as jsx2 } from "react/jsx-runtime"; function TurnstileExplicit({ containerId: customContainerId, siteKey: customSiteKey, theme = "auto", size = "normal", onSuccess, onError, onExpire, onTimeout, refreshExpired = "auto", refreshTimeout = "auto", responseFieldName }) { const siteKey = customSiteKey || process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY; const [id] = useState(() => customContainerId || `cf-turnstile-${Math.random().toString(36).slice(2)}`); const containerRef = useRef2(null); const [widgetId, setWidgetId] = useState(""); if (!siteKey) { throw new Error( `TurnstileExplicit: Missing 'siteKey' prop or environment variable 'NEXT_PUBLIC_TURNSTILE_SITE_KEY'.` ); } if (!siteKey.startsWith("0x") && !siteKey.startsWith("1x")) { console.warn(`TurnstileExplicit: Site key '${siteKey}' doesn't match expected format (should start with 0x or 1x)`); } useEffect2(() => { if (usedContainerIds.has(id)) { throw new Error( `Duplicate containerId "${id}" detected. Each <TurnstileExplicit> on the page must use a unique containerId.` ); } usedContainerIds.add(id); if (responseFieldName) { if (usedResponseFieldNames.has(responseFieldName)) { throw new Error( `Duplicate responseFieldName "${responseFieldName}" detected. Each <TurnstileExplicit> on the page must use a unique responseFieldName.` ); } usedResponseFieldNames.add(responseFieldName); } const container = containerRef.current; if (!container) { return; } let isMounted = true; let localWidgetId; loadTurnstileScript("explicit").then(() => { if (!isMounted || !containerRef.current) return; const turnstile = window.turnstile; if (!turnstile) { console.error("Turnstile failed to load, object not found on window."); onError == null ? void 0 : onError(); return; } const renderedWidgetId = turnstile.render(containerRef.current, { sitekey: siteKey, theme, size, ...responseFieldName && { "response-field-name": responseFieldName }, "refresh-expired": refreshExpired, "refresh-timeout": refreshTimeout, callback: (token) => onSuccess == null ? void 0 : onSuccess(token), "error-callback": (error) => onError == null ? void 0 : onError(), "expired-callback": () => onExpire == null ? void 0 : onExpire(), "timeout-callback": () => onTimeout == null ? void 0 : onTimeout() }); if (renderedWidgetId) { setWidgetId(renderedWidgetId); localWidgetId = renderedWidgetId; } else { console.error("Turnstile render failed. Check your sitekey and other widget configuration."); onError == null ? void 0 : onError(); } }).catch((error) => { console.error("Failed to load Turnstile script:", error); onError == null ? void 0 : onError(); }); return () => { isMounted = false; usedContainerIds.delete(id); if (responseFieldName) { usedResponseFieldNames.delete(responseFieldName); } if (localWidgetId) { removeTurnstile(localWidgetId); } }; }, [id, siteKey, theme, size, onSuccess, onError, onExpire, onTimeout, refreshExpired, refreshTimeout, responseFieldName]); return /* @__PURE__ */ jsx2("div", { ref: containerRef, id, className: "cf-turnstile" }); } // src/utils/verifyTurnstile.ts async function verifyTurnstile(token, options = {}) { var _a, _b; const secret = (_a = options.secretKey) != null ? _a : process.env.TURNSTILE_SECRET_KEY; if (!secret) throw new Error("Turnstile Secret key not provided"); const ip = (_b = options.ip) != null ? _b : await getClientIp(options == null ? void 0 : options.headers); const body = { secret, response: token }; if (ip) body["remoteip"] = ip; const res = await fetch( "https://challenges.cloudflare.com/turnstile/v0/siteverify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } ); if (!res.ok) { throw new Error( `[nextjs\u2011turnstile] Verification request failed: ${res.status} ${res.statusText}` ); } const json = await res.json(); return Boolean(json.success); } async function getClientIp(initHeaders) { var _a, _b, _c, _d; try { const { headers } = __require("next/headers"); let h; try { h = await headers(); } catch { h = headers(); } const ip = ((_b = (_a = h.get("x-forwarded-for")) == null ? void 0 : _a.split(",")[0]) == null ? void 0 : _b.trim()) || h.get("cf-connecting-ip") || h.get("x-real-ip"); if (ip) return ip; } catch { } if (initHeaders) { const get = (name) => { var _a2; if (initHeaders instanceof Headers) return (_a2 = initHeaders.get(name)) != null ? _a2 : void 0; const val = initHeaders[name]; return Array.isArray(val) ? val[0] : val; }; return ((_d = (_c = get("x-forwarded-for")) == null ? void 0 : _c.split(",")[0]) == null ? void 0 : _d.trim()) || get("cf-connecting-ip") || get("x-real-ip") || void 0; } return void 0; } export { TurnstileExplicit, TurnstileImplicit, executeTurnstile, getTurnstileResponse, isTurnstileLoaded, resetTurnstile, verifyTurnstile };