From 2848140a0782b88ee35a4fee613a192de569bf04 Mon Sep 17 00:00:00 2001 From: Leo Developer Date: Thu, 29 Sep 2022 21:37:07 +0200 Subject: [PATCH] initial commit --- .gitignore | 130 +++++++++++++++++++++++++++ LICENSE | 21 +++++ README.md | 50 +++++++++++ package-lock.json | 217 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 31 +++++++ src/index.tsx | 145 +++++++++++++++++++++++++++++++ tsconfig.json | 30 +++++++ 7 files changed, 624 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/index.tsx create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a7d6d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,130 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..031fb11 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 LeoDeveloper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1072e9a --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# react-turnstile + +A very simple React library for [Cloudflare Turnstile](https://challenges.cloudflare.com). + +## Installation + +```sh +npm i react-turnstile +``` + +## Usage + +```jsx +import Turnstile from "react-turnstile"; + +// ... + +function TurnstileWidget() { + return ( + alert(token)} + /> + ); +} +``` + +## Documentation + +Turnstile takes the following arguments: + +| name | type | description | +| -------- | ------ | ---------------------------------- | +| sitekey | string | sitekey of your website (REQUIRED) | +| action | string | - | +| cData | string | - | +| theme | string | one of "light", "dark", "auto" | +| tabIndex | number | +| id | string | id of the div | + +And the following callbacks: + +| name | arguments | description | +| -------- | --------- | ------------------------------------------ | +| onVerify | token | called when challenge is passed (REQUIRED) | +| onLoad | - | called when the widget is loaded | +| onError | error | called when an error occurs | +| onExpire | - | called when the challenge expires | + +For more details on what each argument does, see the [Cloudflare Documentation](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#configurations). diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cc7c039 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,217 @@ +{ + "name": "react-turnstile", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "react-turnstile", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@types/react": "^18.0.21", + "prettier": "^2.7.1", + "typescript": "^4.8.4" + }, + "peerDependencies": { + "react": ">= 17.0.0", + "react-dom": ">= 17.0.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.0.21", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.21.tgz", + "integrity": "sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + } + }, + "dependencies": { + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "@types/react": { + "version": "18.0.21", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.21.tgz", + "integrity": "sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, + "csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "dev": true + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..00271b1 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "react-turnstile", + "version": "1.0.0", + "description": "React library for Cloudflare's Turnstile CAPTCHA alternative", + "main": "dist/index.cjs", + "scripts": { + "prepublishOnly": "npm run build", + "build": "tsc", + "style-fix": "npx prettier -w ." + }, + "exports": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "types": "./dist/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/Le0developer/react-turnstile" + }, + "peerDependencies": { + "react": ">= 17.0.0", + "react-dom": ">= 17.0.0" + }, + "author": "Leo Developer", + "license": "MIT", + "devDependencies": { + "@types/react": "^18.0.21", + "prettier": "^2.7.1", + "typescript": "^4.8.4" + } +} diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..e8f1f1b --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,145 @@ +import React, { useEffect, createRef } from "react"; + +const global = globalThis ?? window; +let turnstileState = + typeof (global as any).turnstile !== "undefined" ? "ready" : "unloaded"; +let ensureTurnstile: () => Promise; + +// Functions responsible for loading the turnstile api, while also making sure +// to only load it once +{ + const TURNSTILE_LOAD_FUNCTION = "cf__reactTurnstileOnLoad"; + const TURNSTILE_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js"; + + let turnstileLoad: { + resolve: (value?: any) => void; + reject: (reason?: any) => void; + }; + const turnstileLoadPromise = new Promise((resolve, reject) => { + turnstileLoad = { resolve, reject }; + if (turnstileState === "ready") resolve(undefined); + }); + (global as any)[TURNSTILE_LOAD_FUNCTION] = () => { + turnstileLoad.resolve(); + turnstileState = "ready"; + }; + + ensureTurnstile = () => { + if (turnstileState === "unloaded") { + turnstileState = "loading"; + const url = `${TURNSTILE_SRC}?onload=${TURNSTILE_LOAD_FUNCTION}`; + const script = document.createElement("script"); + script.src = url; + script.async = true; + script.addEventListener("error", () => { + turnstileLoad.reject("Failed to load Turnstile."); + }); + document.head.appendChild(script); + } + return turnstileLoadPromise; + }; +} + +export default function Turnstile({ + id, + className, + sitekey, + action, + cData, + theme, + tabIndex, + onVerify, + onLoad, + onError, + onExpire, +}: TurnstileProps) { + const ref: React.RefObject = createRef(); + + useEffect(() => { + if (!ref.current) return; + ref.current.innerHTML = ""; // remove old widget + (async () => { + if (!ref.current) return; + // load turnstile + if (turnstileState !== "ready") { + try { + await ensureTurnstile(); + } catch (e) { + onError?.(e); + return; + } + } + onLoad?.(); + // turnstile is loaded, render the widget + + const turnstileOptions: RawTurnstileOptions = { + sitekey, + action, + cData, + theme, + tabindex: tabIndex, + callback: onVerify, + "error-callback": onError, + "expired-callback": onExpire, + }; + + window.turnstile.render(ref.current, turnstileOptions); + })(); + }, [ + sitekey, + action, + cData, + theme, + tabIndex, + // reloading on the following causes an infinite loop + // ref, + // onVerify, + // onLoad, + // onError, + // onExpire, + ]); + + return
; +} + +interface TurnstileProps { + sitekey: string; + action?: string; + cData?: string; + theme?: "light" | "dark" | "auto"; + tabIndex?: number; + + id?: string; + className?: string; + + onVerify: (token: string) => void; + onLoad?: () => void; + onError?: (error?: Error | any) => void; + onExpire?: () => void; +} + +// Generic typescript definitions of the turnstile api + +declare global { + interface Window { + turnstile: { + render: ( + element: string | HTMLElement, + options: RawTurnstileOptions + ) => string; + reset: (widgetId: string) => void; + getResponse: (widgetId: string) => string | undefined; + }; + } +} + +interface RawTurnstileOptions { + sitekey: string; + action?: string; + cData?: string; + callback?: (token: string) => void; + "error-callback"?: () => void; + "expired-callback"?: () => void; + theme?: "light" | "dark" | "auto"; + tabindex?: number; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3c60405 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "importHelpers": true, + "declaration": true, + "sourceMap": true, + "rootDir": "./src", + "strict": true, + "outDir": "./dist", + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "downlevelIteration": true, + "moduleResolution": "node", + "baseUrl": "./", + "paths": { + "*": ["src/*", "node_modules/*"] + }, + "jsx": "react", + "esModuleInterop": true, + "target": "ES2019", + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "exclude": ["node_modules", "dist"] +}