nextjs-turnstile 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/License +19 -0
- package/README.md +157 -0
- package/package.json +34 -0
- package/src/components/TurnstileExplicit.js +73 -0
- package/src/components/TurnstileImplicit.js +47 -0
- package/src/index.js +3 -0
- package/src/utils/TurnstileVerify.js +52 -0
package/License
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Next.js Turnstile CAPTCHA Package
|
|
2
|
+

|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
This package provides components and utilities to integrate Cloudflare Turnstile CAPTCHA into your Next.js applications. It supports both implicit and explicit CAPTCHA modes.
|
|
7
|
+
|
|
8
|
+
You can find the document of Cloudflare Turnstile [here](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/).
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install nextjs-turnstile
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
### Components
|
|
19
|
+
**TurnstileImplicit:**
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
import React from 'react';
|
|
23
|
+
import { TurnstileImplicit, verifyTurnstile } from 'nextjs-turnstile';
|
|
24
|
+
|
|
25
|
+
export default function MyForm() {
|
|
26
|
+
const handleSubmit = async (e) => {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
const token1 = e.target['cf-turnstile-response-1'].value;
|
|
29
|
+
const token2 = e.target['cf-turnstile-response-2'].value;
|
|
30
|
+
|
|
31
|
+
// Verify the first CAPTCHA response
|
|
32
|
+
const success1 = await verifyTurnstile(token1);
|
|
33
|
+
if (!success1) {
|
|
34
|
+
alert('First CAPTCHA verification failed');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Verify the second CAPTCHA response
|
|
39
|
+
const success2 = await verifyTurnstile(token2);
|
|
40
|
+
if (!success2) {
|
|
41
|
+
alert('Second CAPTCHA verification failed');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<form onSubmit={handleSubmit}>
|
|
48
|
+
<h2>Turnstile Implicit CAPTCHA Example</h2>
|
|
49
|
+
|
|
50
|
+
{/* First CAPTCHA */}
|
|
51
|
+
<TurnstileImplicit
|
|
52
|
+
theme="dark"
|
|
53
|
+
size="normal"
|
|
54
|
+
responseFieldName="cf-turnstile-response-1"
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
{/* Second CAPTCHA */}
|
|
58
|
+
<TurnstileImplicit
|
|
59
|
+
theme="light"
|
|
60
|
+
size="compact"
|
|
61
|
+
responseFieldName="cf-turnstile-response-2"
|
|
62
|
+
/>
|
|
63
|
+
|
|
64
|
+
<button type="submit">Submit</button>
|
|
65
|
+
</form>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**TurnstileExplicit:**
|
|
71
|
+
```javascript
|
|
72
|
+
import React from 'react';
|
|
73
|
+
import { TurnstileExplicit, verifyTurnstile } from 'nextjs-turnstile';
|
|
74
|
+
|
|
75
|
+
export default function MyForm() {
|
|
76
|
+
const handleSubmit = async (e) => {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
const token1 = e.target['cf-turnstile-response-3'].value;
|
|
79
|
+
const token2 = e.target['cf-turnstile-response-4'].value;
|
|
80
|
+
|
|
81
|
+
// Verify the first CAPTCHA response
|
|
82
|
+
const success1 = await verifyTurnstile(token1);
|
|
83
|
+
if (!success1) {
|
|
84
|
+
alert('First CAPTCHA verification failed');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Verify the second CAPTCHA response
|
|
89
|
+
const success2 = await verifyTurnstile(token2);
|
|
90
|
+
if (!success2) {
|
|
91
|
+
alert('Second CAPTCHA verification failed');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<form onSubmit={handleSubmit}>
|
|
98
|
+
<h2>Turnstile Explicit CAPTCHA Example</h2>
|
|
99
|
+
|
|
100
|
+
{/* Developers must place the divs in their HTML */}
|
|
101
|
+
|
|
102
|
+
{/* First CAPTCHA */}
|
|
103
|
+
<div id="cf-turnstile-response-3"></div>
|
|
104
|
+
<TurnstileExplicit
|
|
105
|
+
theme="dark"
|
|
106
|
+
size="normal"
|
|
107
|
+
responseFieldName="cf-turnstile-response-3"
|
|
108
|
+
/>
|
|
109
|
+
|
|
110
|
+
{/* Second CAPTCHA */}
|
|
111
|
+
<div id="cf-turnstile-response-4"></div>
|
|
112
|
+
<TurnstileExplicit
|
|
113
|
+
theme="light"
|
|
114
|
+
size="compact"
|
|
115
|
+
responseFieldName="cf-turnstile-response-4"
|
|
116
|
+
/>
|
|
117
|
+
|
|
118
|
+
<button type="submit">Submit</button>
|
|
119
|
+
</form>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Verification Utility
|
|
125
|
+
In your API routes:
|
|
126
|
+
ß
|
|
127
|
+
```javascript
|
|
128
|
+
import { verifyTurnstile } from 'nextjs-turnstile';
|
|
129
|
+
|
|
130
|
+
export default async function handler(req, res) {
|
|
131
|
+
const { token } = req.body;
|
|
132
|
+
|
|
133
|
+
const success = await verifyTurnstile(token, false, req);
|
|
134
|
+
// const success = await verifyTurnstile(token, req.connection.remoteAddress);
|
|
135
|
+
|
|
136
|
+
if (success) {
|
|
137
|
+
return res.status(200).json({ success: true });
|
|
138
|
+
} else {
|
|
139
|
+
return res.status(400).json({ success: false, message: 'CAPTCHA verification failed' });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Environment Variables
|
|
145
|
+
You need to add the following environment variables to your .env.local file:
|
|
146
|
+
|
|
147
|
+
```plaintext
|
|
148
|
+
NEXT_PUBLIC_TURNSTILE_SITE_KEY=your_site_key_here
|
|
149
|
+
TURNSTILE_SECRET_KEY=your_secret_key_here
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
This project is licensed under the MIT License - see the [License](./License) file for details.
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
## Author
|
|
157
|
+
Davod Mozafari - [Twitter](https://twitter.com/davodmozafari)
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nextjs-turnstile",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Integrate Cloudflare Turnstile CAPTCHA in Next.js applications",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"test": "jest"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"node-fetch": "^3.3.0"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"react": "^18.2.0",
|
|
15
|
+
"react-dom": "^18.2.0",
|
|
16
|
+
"next": "^12.0.0"
|
|
17
|
+
},
|
|
18
|
+
"author": "Davod Mozafari",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/davodm/nextjs-turnstile.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/davodm/nextjs-turnstile/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/davodm/nextjs-turnstile",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"nextjs",
|
|
30
|
+
"cloudflare",
|
|
31
|
+
"turnstile",
|
|
32
|
+
"captcha"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
let explicitFields = []; // Array to track the explicit CAPTCHA fields
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TurnstileExplicit component registers and initializes the Cloudflare Turnstile CAPTCHA in explicit mode.
|
|
7
|
+
*
|
|
8
|
+
* This component registers CAPTCHA instances, which must be manually initialized using the provided
|
|
9
|
+
* response field name as the ID for the corresponding `<div>` element. The initialization script will
|
|
10
|
+
* render the CAPTCHA based on the specified theme and size.
|
|
11
|
+
*
|
|
12
|
+
* @component
|
|
13
|
+
* @param {string} [theme='light'] - The theme of the CAPTCHA widget. Options are 'light' or 'dark'.
|
|
14
|
+
* @param {string} [size='normal'] - The size of the CAPTCHA widget. Options are 'normal' or 'compact'.
|
|
15
|
+
* @param {string} [responseFieldName='cf-turnstile-response'] - The name of the hidden input field and the ID of the `<div>` that will contain the CAPTCHA widget.
|
|
16
|
+
* @param {string} [siteKey=process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY] - The site key for Turnstile CAPTCHA. Defaults to the value in the environment variables.
|
|
17
|
+
* @returns {null} No visible component is rendered, only registers the field for later initialization.
|
|
18
|
+
*/
|
|
19
|
+
const TurnstileExplicit = ({
|
|
20
|
+
theme = "light",
|
|
21
|
+
size = "normal",
|
|
22
|
+
responseFieldName = "cf-turnstile-response",
|
|
23
|
+
}) => {
|
|
24
|
+
const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
|
|
25
|
+
const JSUrl = "https://challenges.cloudflare.com/turnstile/v0/api.js?onload=turnstileReady";
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
// Register the field for later initialization
|
|
29
|
+
// Check if the field is already registered
|
|
30
|
+
if (explicitFields.some((field) => field.fieldName === responseFieldName)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
explicitFields.push({
|
|
34
|
+
fieldName: responseFieldName,
|
|
35
|
+
size,
|
|
36
|
+
theme,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
// Cleanup: Remove the field if the component unmounts
|
|
41
|
+
explicitFields = explicitFields.filter(
|
|
42
|
+
(field) => field.fieldName !== responseFieldName
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
}, [responseFieldName, size, theme]);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
// Ensure the Turnstile script is loaded only once
|
|
49
|
+
if (!document.querySelector('script[src="' + JSUrl + '"]')) {
|
|
50
|
+
const script = document.createElement("script");
|
|
51
|
+
script.src = JSUrl;
|
|
52
|
+
script.async = true;
|
|
53
|
+
document.body.appendChild(script);
|
|
54
|
+
|
|
55
|
+
script.onload = () => {
|
|
56
|
+
window.turnstileReady = function () {
|
|
57
|
+
explicitFields.forEach(function (field) {
|
|
58
|
+
window.turnstile.render(`#${field.fieldName}`, {
|
|
59
|
+
sitekey: siteKey,
|
|
60
|
+
"response-field-name": field.fieldName,
|
|
61
|
+
size: field.size,
|
|
62
|
+
theme: field.theme,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
return null; // No need to render anything, just register the field
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default TurnstileExplicit;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TurnstileImplicit component renders the Cloudflare Turnstile CAPTCHA in implicit mode.
|
|
5
|
+
*
|
|
6
|
+
* This component automatically loads the Turnstile script and renders the CAPTCHA based on the specified
|
|
7
|
+
* theme, size, and response field name.
|
|
8
|
+
*
|
|
9
|
+
* @component
|
|
10
|
+
* @param {string} [theme='light'] - The theme of the CAPTCHA widget. Options are 'light' or 'dark'.
|
|
11
|
+
* @param {string} [size='normal'] - The size of the CAPTCHA widget. Options are 'normal' or 'compact'.
|
|
12
|
+
* @param {string} [responseFieldName='cf-turnstile-response'] - The name of the hidden input field that stores the CAPTCHA response token.
|
|
13
|
+
* @returns {JSX.Element} The TurnstileImplicit component
|
|
14
|
+
*/
|
|
15
|
+
const TurnstileImplicit = ({ theme = 'light', size = 'normal', responseFieldName = 'cf-turnstile-response' }) => {
|
|
16
|
+
const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
|
|
17
|
+
const JSUrl="https://challenges.cloudflare.com/turnstile/v0/api.js";
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
// Load the Turnstile script only once
|
|
21
|
+
if (!document.querySelector('script[src="' + JSUrl + '"]')) {
|
|
22
|
+
const script = document.createElement('script');
|
|
23
|
+
script.src = JSUrl;
|
|
24
|
+
script.async = true;
|
|
25
|
+
document.body.appendChild(script);
|
|
26
|
+
|
|
27
|
+
return () => {
|
|
28
|
+
document.body.removeChild(script);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
<div
|
|
36
|
+
className="cf-turnstile"
|
|
37
|
+
data-sitekey={siteKey}
|
|
38
|
+
data-theme={theme}
|
|
39
|
+
data-size={size}
|
|
40
|
+
data-response-field={responseFieldName}
|
|
41
|
+
>
|
|
42
|
+
</div>
|
|
43
|
+
</>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default TurnstileImplicit;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifies the Cloudflare Turnstile CAPTCHA response token with the Turnstile API.
|
|
3
|
+
*
|
|
4
|
+
* @async
|
|
5
|
+
* @function verifyTurnstile
|
|
6
|
+
* @param {string} token - The CAPTCHA response token provided by the client-side Turnstile widget.
|
|
7
|
+
* @param {string} [userIP] - The IP address of the user. If not provided, the function will attempt to extract it from the request headers.
|
|
8
|
+
* @param {Object} [req] - The request object (optional). If provided, it is used to extract the user's IP address from headers like 'cf-connecting-ip' or 'x-forwarded-for'.
|
|
9
|
+
* @returns {Promise<boolean>} - Returns `true` if the CAPTCHA verification was successful, otherwise `false`.
|
|
10
|
+
*
|
|
11
|
+
* @throws {Error} If the request to the Turnstile API fails or if the CAPTCHA verification fails, an error is thrown with a descriptive message.
|
|
12
|
+
*/
|
|
13
|
+
export const verifyTurnstile = async (token, userIP, req=undefined) => {
|
|
14
|
+
const secretKey = process.env.TURNSTILE_SECRET_KEY;
|
|
15
|
+
|
|
16
|
+
// Fetch the user's IP address if not provided
|
|
17
|
+
let userIP = ipAddress;
|
|
18
|
+
if (!userIP && req?.headers) {
|
|
19
|
+
userIP = req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetch(`https://challenges.cloudflare.com/turnstile/v0/siteverify`, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
secret: secretKey,
|
|
30
|
+
response: token,
|
|
31
|
+
remoteip: userIP,
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Check for a successful response
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`Verification request failed with status ${response.status}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
|
|
42
|
+
// Check if the success field is true
|
|
43
|
+
if (!data.success) {
|
|
44
|
+
throw new Error(`Verification failed: ${data['error-codes'] ? data['error-codes'].join(', ') : 'Unknown error'}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return true;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Error verifying Turnstile CAPTCHA:', error.message);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
};
|