nextjs-turnstile
Version:
Integrate Cloudflare Turnstile CAPTCHA in Next.js applications
336 lines (330 loc) • 11.2 kB
JavaScript
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
};