@thind9xdev/react-turnstile 1.0.0 → 1.0.2
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/README.md +433 -118
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
# React Cloudflare Turnstile
|
|
2
2
|
|
|
3
|
-
A clean
|
|
3
|
+
A modern and clean React library for integrating Cloudflare Turnstile.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 📦 Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm
|
|
8
|
+
npm install @thind9xdev/react-turnstile
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
## Import
|
|
11
|
+
## 🚀 Import into React
|
|
12
12
|
|
|
13
13
|
```tsx
|
|
14
|
-
import { useTurnstile } from "@thind9xdev/react-turnstile";
|
|
14
|
+
import { useTurnstile, TurnstileComponent } from "@thind9xdev/react-turnstile";
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## 📝 How to Use the Hook (useTurnstile)
|
|
18
|
+
|
|
19
|
+
### Basic Usage with Hook
|
|
18
20
|
|
|
19
21
|
```tsx
|
|
20
22
|
import React from "react";
|
|
21
23
|
import { useTurnstile } from "@thind9xdev/react-turnstile";
|
|
22
24
|
|
|
23
|
-
const
|
|
24
|
-
const siteKey = "YOUR_SITE_KEY";
|
|
25
|
+
const MyComponent = () => {
|
|
26
|
+
const siteKey = "YOUR_SITE_KEY"; // Replace with your actual site key
|
|
25
27
|
const { ref, token, error, isLoading } = useTurnstile(siteKey);
|
|
26
28
|
|
|
27
29
|
if (isLoading) {
|
|
@@ -32,7 +34,7 @@ const YourComponent = () => {
|
|
|
32
34
|
return <div>Error: {error}</div>;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
// You can use token to send
|
|
37
|
+
// You can use the token to send requests to your API
|
|
36
38
|
return (
|
|
37
39
|
<div>
|
|
38
40
|
<div ref={ref}></div>
|
|
@@ -41,16 +43,16 @@ const YourComponent = () => {
|
|
|
41
43
|
);
|
|
42
44
|
};
|
|
43
45
|
|
|
44
|
-
export default
|
|
46
|
+
export default MyComponent;
|
|
45
47
|
```
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
### Advanced Usage with Hook
|
|
48
50
|
|
|
49
51
|
```tsx
|
|
50
52
|
import React from "react";
|
|
51
53
|
import { useTurnstile, TurnstileOptions } from "@thind9xdev/react-turnstile";
|
|
52
54
|
|
|
53
|
-
const
|
|
55
|
+
const AdvancedComponent = () => {
|
|
54
56
|
const siteKey = "YOUR_SITE_KEY";
|
|
55
57
|
const options: TurnstileOptions = {
|
|
56
58
|
theme: "light",
|
|
@@ -75,74 +77,231 @@ const YourComponent = () => {
|
|
|
75
77
|
try {
|
|
76
78
|
const currentToken = getResponse();
|
|
77
79
|
if (currentToken) {
|
|
78
|
-
// Send request to
|
|
80
|
+
// Send request to API with token
|
|
79
81
|
console.log("Current token:", currentToken);
|
|
82
|
+
|
|
83
|
+
// Example API call
|
|
84
|
+
const response = await fetch('/api/verify', {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'Content-Type': 'application/json' },
|
|
87
|
+
body: JSON.stringify({ token: currentToken })
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const result = await response.json();
|
|
91
|
+
console.log("Verification result:", result);
|
|
80
92
|
} else {
|
|
81
|
-
// Execute Turnstile if no token
|
|
93
|
+
// Execute Turnstile if no token yet
|
|
82
94
|
execute();
|
|
83
95
|
}
|
|
84
96
|
} catch (err) {
|
|
85
|
-
console.error("
|
|
97
|
+
console.error("Unable to get Turnstile token:", err);
|
|
86
98
|
}
|
|
87
99
|
};
|
|
88
100
|
|
|
89
101
|
const handleReset = () => {
|
|
90
|
-
reset();
|
|
102
|
+
reset(); // Reset widget to initial state
|
|
91
103
|
};
|
|
92
104
|
|
|
93
105
|
return (
|
|
94
106
|
<div>
|
|
95
107
|
<div ref={ref}></div>
|
|
96
108
|
<button onClick={handleSubmit} disabled={isLoading}>
|
|
97
|
-
{isLoading ? "
|
|
109
|
+
{isLoading ? "Verifying..." : "Submit"}
|
|
98
110
|
</button>
|
|
99
111
|
<button onClick={handleReset} disabled={isLoading}>
|
|
100
112
|
Reset Turnstile
|
|
101
113
|
</button>
|
|
102
114
|
{error && <p style={{ color: "red" }}>Error: {error}</p>}
|
|
103
|
-
{token && <p style={{ color: "green" }}>Token ready!</p>}
|
|
115
|
+
{token && <p style={{ color: "green" }}>Token is ready!</p>}
|
|
104
116
|
</div>
|
|
105
117
|
);
|
|
106
118
|
};
|
|
107
119
|
|
|
108
|
-
export default
|
|
120
|
+
export default AdvancedComponent;
|
|
109
121
|
```
|
|
110
122
|
|
|
111
|
-
##
|
|
123
|
+
## 🧩 How to Use the Component (TurnstileComponent)
|
|
124
|
+
|
|
125
|
+
### Basic Usage with Component
|
|
112
126
|
|
|
113
127
|
```tsx
|
|
114
|
-
import React from "react";
|
|
115
|
-
import {
|
|
128
|
+
import React, { useRef } from "react";
|
|
129
|
+
import { TurnstileComponent, TurnstileComponentRef } from "@thind9xdev/react-turnstile";
|
|
116
130
|
|
|
117
|
-
const
|
|
131
|
+
const ComponentExample = () => {
|
|
132
|
+
const turnstileRef = useRef<TurnstileComponentRef>(null);
|
|
118
133
|
const siteKey = "YOUR_SITE_KEY";
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
134
|
+
|
|
135
|
+
const handleSubmit = () => {
|
|
136
|
+
const token = turnstileRef.current?.getResponse();
|
|
137
|
+
if (token) {
|
|
138
|
+
console.log("Token from component:", token);
|
|
139
|
+
// Send token to your API
|
|
140
|
+
} else {
|
|
141
|
+
console.log("No token yet, executing verification...");
|
|
142
|
+
turnstileRef.current?.execute();
|
|
143
|
+
}
|
|
122
144
|
};
|
|
145
|
+
|
|
146
|
+
const handleReset = () => {
|
|
147
|
+
turnstileRef.current?.reset();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div>
|
|
152
|
+
<h3>Using TurnstileComponent</h3>
|
|
153
|
+
|
|
154
|
+
<TurnstileComponent
|
|
155
|
+
ref={turnstileRef}
|
|
156
|
+
siteKey={siteKey}
|
|
157
|
+
theme="auto"
|
|
158
|
+
size="normal"
|
|
159
|
+
className="my-turnstile"
|
|
160
|
+
style={{ margin: "20px 0" }}
|
|
161
|
+
/>
|
|
162
|
+
|
|
163
|
+
<div>
|
|
164
|
+
<button onClick={handleSubmit}>
|
|
165
|
+
Submit Form
|
|
166
|
+
</button>
|
|
167
|
+
<button onClick={handleReset}>
|
|
168
|
+
Reset
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export default ComponentExample;
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Component Usage with Advanced Options
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
import React, { useRef, useState } from "react";
|
|
182
|
+
import { TurnstileComponent, TurnstileComponentRef } from "@thind9xdev/react-turnstile";
|
|
183
|
+
|
|
184
|
+
const AdvancedComponentExample = () => {
|
|
185
|
+
const turnstileRef = useRef<TurnstileComponentRef>(null);
|
|
186
|
+
const [status, setStatus] = useState<string>("");
|
|
187
|
+
const siteKey = "YOUR_SITE_KEY";
|
|
188
|
+
|
|
189
|
+
const handleSuccess = (token: string) => {
|
|
190
|
+
setStatus(`Verification successful! Token: ${token.substring(0, 20)}...`);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const handleError = (error?: string) => {
|
|
194
|
+
setStatus(`Verification error: ${error || "Unknown"}`);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const handleLoad = () => {
|
|
198
|
+
setStatus("Turnstile loaded");
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div>
|
|
203
|
+
<h3>Component with callback handlers</h3>
|
|
204
|
+
|
|
205
|
+
<TurnstileComponent
|
|
206
|
+
ref={turnstileRef}
|
|
207
|
+
siteKey={siteKey}
|
|
208
|
+
theme="dark"
|
|
209
|
+
size="compact"
|
|
210
|
+
language="en"
|
|
211
|
+
onSuccess={handleSuccess}
|
|
212
|
+
onError={handleError}
|
|
213
|
+
onLoad={handleLoad}
|
|
214
|
+
className="custom-turnstile"
|
|
215
|
+
style={{
|
|
216
|
+
border: "1px solid #ddd",
|
|
217
|
+
borderRadius: "8px",
|
|
218
|
+
padding: "10px"
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
|
|
222
|
+
{status && (
|
|
223
|
+
<div style={{
|
|
224
|
+
marginTop: "10px",
|
|
225
|
+
padding: "10px",
|
|
226
|
+
backgroundColor: "#f5f5f5",
|
|
227
|
+
borderRadius: "4px"
|
|
228
|
+
}}>
|
|
229
|
+
{status}
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export default AdvancedComponentExample;
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## 🔍 Invisible Mode
|
|
240
|
+
|
|
241
|
+
### Using Invisible Mode with Hook
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
import React, { useState } from "react";
|
|
245
|
+
import { useTurnstile } from "@thind9xdev/react-turnstile";
|
|
246
|
+
|
|
247
|
+
const InvisibleTurnstile = () => {
|
|
248
|
+
const [email, setEmail] = useState("");
|
|
249
|
+
const siteKey = "YOUR_SITE_KEY";
|
|
123
250
|
|
|
124
|
-
const { ref, token, error, isLoading, execute } = useTurnstile(siteKey,
|
|
251
|
+
const { ref, token, error, isLoading, execute } = useTurnstile(siteKey, {
|
|
252
|
+
appearance: "execute", // Invisible mode
|
|
253
|
+
execution: "execute",
|
|
254
|
+
theme: "light"
|
|
255
|
+
});
|
|
125
256
|
|
|
126
|
-
const
|
|
257
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
127
258
|
e.preventDefault();
|
|
128
|
-
|
|
259
|
+
|
|
129
260
|
if (!token) {
|
|
130
261
|
// Execute Turnstile verification
|
|
262
|
+
console.log("Verifying...");
|
|
131
263
|
execute();
|
|
132
264
|
return;
|
|
133
265
|
}
|
|
134
266
|
|
|
135
|
-
//
|
|
136
|
-
|
|
267
|
+
// Submit form with token
|
|
268
|
+
try {
|
|
269
|
+
const response = await fetch("/api/submit", {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: { "Content-Type": "application/json" },
|
|
272
|
+
body: JSON.stringify({ email, token })
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
if (response.ok) {
|
|
276
|
+
console.log("Form submitted successfully!");
|
|
277
|
+
setEmail("");
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
console.error("Form submission error:", err);
|
|
281
|
+
}
|
|
137
282
|
};
|
|
138
283
|
|
|
139
284
|
return (
|
|
140
|
-
<form onSubmit={
|
|
141
|
-
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
285
|
+
<form onSubmit={handleSubmit}>
|
|
286
|
+
{/* Hidden container for Turnstile */}
|
|
287
|
+
<div ref={ref} style={{ display: "none" }}></div>
|
|
288
|
+
|
|
289
|
+
<div>
|
|
290
|
+
<label htmlFor="email">Email:</label>
|
|
291
|
+
<input
|
|
292
|
+
type="email"
|
|
293
|
+
id="email"
|
|
294
|
+
value={email}
|
|
295
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
296
|
+
required
|
|
297
|
+
disabled={isLoading}
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<button type="submit" disabled={isLoading || !email}>
|
|
302
|
+
{isLoading ? "Verifying..." : "Register"}
|
|
145
303
|
</button>
|
|
304
|
+
|
|
146
305
|
{error && <p style={{ color: "red" }}>{error}</p>}
|
|
147
306
|
</form>
|
|
148
307
|
);
|
|
@@ -151,7 +310,7 @@ const InvisibleTurnstile = () => {
|
|
|
151
310
|
export default InvisibleTurnstile;
|
|
152
311
|
```
|
|
153
312
|
|
|
154
|
-
## API
|
|
313
|
+
## 📚 API Documentation
|
|
155
314
|
|
|
156
315
|
### `useTurnstile(siteKey, options?)`
|
|
157
316
|
|
|
@@ -160,66 +319,101 @@ export default InvisibleTurnstile;
|
|
|
160
319
|
- `options` (TurnstileOptions, optional): Configuration options
|
|
161
320
|
|
|
162
321
|
#### Options (TurnstileOptions):
|
|
163
|
-
- `theme` ('light' | 'dark' | 'auto'
|
|
164
|
-
- `size` ('normal' | 'compact'
|
|
165
|
-
- `language` (string
|
|
166
|
-
- `retry` ('auto' | 'never'
|
|
167
|
-
- `retry-interval` (number
|
|
168
|
-
- `refresh-expired` ('auto' | 'manual' | 'never'
|
|
169
|
-
- `appearance` ('always' | 'execute' | 'interaction-only'
|
|
170
|
-
- `execution` ('render' | 'execute'
|
|
322
|
+
- `theme` ('light' | 'dark' | 'auto'): Widget theme (default: 'auto')
|
|
323
|
+
- `size` ('normal' | 'compact'): Widget size (default: 'normal')
|
|
324
|
+
- `language` (string): Language code (default: 'auto')
|
|
325
|
+
- `retry` ('auto' | 'never'): Retry behavior (default: 'auto')
|
|
326
|
+
- `retry-interval` (number): Retry interval (milliseconds)
|
|
327
|
+
- `refresh-expired` ('auto' | 'manual' | 'never'): Token refresh behavior (default: 'auto')
|
|
328
|
+
- `appearance` ('always' | 'execute' | 'interaction-only'): When to show the widget (default: 'always')
|
|
329
|
+
- `execution` ('render' | 'execute'): Execution mode (default: 'render')
|
|
330
|
+
- `onLoad` (function): Callback when widget loads
|
|
331
|
+
- `onSuccess` (function): Callback on successful verification
|
|
332
|
+
- `onError` (function): Callback on error
|
|
333
|
+
- `onExpire` (function): Callback when token expires
|
|
334
|
+
- `onTimeout` (function): Callback on timeout
|
|
171
335
|
|
|
172
336
|
#### Returns:
|
|
173
|
-
- `ref` (React.RefObject): Ref to attach to the container div
|
|
174
|
-
- `token` (string | null):
|
|
175
|
-
- `error` (string | null): Error message if
|
|
337
|
+
- `ref` (React.RefObject): Ref to attach to the container div
|
|
338
|
+
- `token` (string | null): Turnstile token
|
|
339
|
+
- `error` (string | null): Error message if any
|
|
176
340
|
- `isLoading` (boolean): Loading state
|
|
177
|
-
- `reset` (function):
|
|
178
|
-
- `execute` (function):
|
|
179
|
-
- `getResponse` (function):
|
|
180
|
-
- `widgetId` (string | null):
|
|
341
|
+
- `reset` (function): Reset the widget
|
|
342
|
+
- `execute` (function): Manually execute Turnstile (for invisible mode)
|
|
343
|
+
- `getResponse` (function): Get the current token
|
|
344
|
+
- `widgetId` (string | null): Widget ID returned by Turnstile
|
|
181
345
|
|
|
182
|
-
|
|
346
|
+
### `TurnstileComponent`
|
|
183
347
|
|
|
184
|
-
|
|
348
|
+
#### Props:
|
|
349
|
+
- `siteKey` (string): Your Cloudflare Turnstile site key
|
|
350
|
+
- `className` (string, optional): CSS class for the container
|
|
351
|
+
- `style` (React.CSSProperties, optional): Inline styles for the container
|
|
352
|
+
- All options from `TurnstileOptions`
|
|
353
|
+
|
|
354
|
+
#### Ref Methods:
|
|
355
|
+
- `reset()`: Reset the widget to its initial state
|
|
356
|
+
- `execute()`: Manually execute verification
|
|
357
|
+
- `getResponse()`: Get the current token
|
|
358
|
+
|
|
359
|
+
## 🎨 TypeScript Support
|
|
360
|
+
|
|
361
|
+
This library includes full TypeScript support with exported interfaces:
|
|
185
362
|
|
|
186
363
|
```tsx
|
|
187
|
-
import {
|
|
364
|
+
import {
|
|
365
|
+
useTurnstile,
|
|
366
|
+
TurnstileComponent,
|
|
367
|
+
TurnstileResponse,
|
|
368
|
+
TurnstileOptions,
|
|
369
|
+
TurnstileComponentProps,
|
|
370
|
+
TurnstileComponentRef
|
|
371
|
+
} from "@thind9xdev/react-turnstile";
|
|
188
372
|
```
|
|
189
373
|
|
|
190
|
-
##
|
|
374
|
+
## 🎭 Appearance and Display Modes
|
|
191
375
|
|
|
192
376
|
### Themes
|
|
193
377
|
- `light`: Light theme
|
|
194
|
-
- `dark`: Dark theme
|
|
195
|
-
- `auto`:
|
|
378
|
+
- `dark`: Dark theme
|
|
379
|
+
- `auto`: Follows user's system settings
|
|
196
380
|
|
|
197
381
|
### Sizes
|
|
198
|
-
- `normal`: Standard size
|
|
199
|
-
- `compact`:
|
|
382
|
+
- `normal`: Standard widget size
|
|
383
|
+
- `compact`: Compact widget size
|
|
200
384
|
|
|
201
385
|
### Appearance Modes
|
|
202
|
-
- `always`: Widget
|
|
203
|
-
- `execute`: Invisible mode - widget only
|
|
386
|
+
- `always`: Widget always visible (default)
|
|
387
|
+
- `execute`: Invisible mode - widget appears only when executed
|
|
204
388
|
- `interaction-only`: Widget appears only when user interaction is required
|
|
205
389
|
|
|
206
|
-
## Features
|
|
390
|
+
## ✨ Features
|
|
207
391
|
|
|
208
|
-
- ✅
|
|
392
|
+
- ✅ Modern, clean React hook
|
|
209
393
|
- ✅ Full TypeScript support
|
|
210
|
-
- ✅
|
|
394
|
+
- ✅ Auto script loading and cleanup
|
|
211
395
|
- ✅ Error handling
|
|
212
|
-
- ✅ Loading
|
|
396
|
+
- ✅ Loading state
|
|
213
397
|
- ✅ Manual token refresh and reset
|
|
214
|
-
- ✅
|
|
215
|
-
- ✅
|
|
216
|
-
- ✅
|
|
398
|
+
- ✅ Invisible mode support
|
|
399
|
+
- ✅ Customizable appearance and size
|
|
400
|
+
- ✅ Multi-language support
|
|
217
401
|
- ✅ Comprehensive widget lifecycle management
|
|
218
|
-
- ✅
|
|
402
|
+
- ✅ No dependencies (peer dependency: React >=16.8.0)
|
|
403
|
+
|
|
404
|
+
## 🔧 Get Your Site Key
|
|
405
|
+
|
|
406
|
+
1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com/)
|
|
407
|
+
2. Navigate to "Turnstile"
|
|
408
|
+
3. Create a new site
|
|
409
|
+
4. Copy your **Site Key** and **Secret Key**
|
|
219
410
|
|
|
220
|
-
|
|
411
|
+
### Test Site Key
|
|
412
|
+
For testing, you can use: `1x00000000000000000000AA`
|
|
221
413
|
|
|
222
|
-
##
|
|
414
|
+
## 🔧 Backend Integration
|
|
415
|
+
|
|
416
|
+
### Verify Turnstile token with Node.js/Express:
|
|
223
417
|
|
|
224
418
|
```javascript
|
|
225
419
|
const express = require('express');
|
|
@@ -228,13 +422,16 @@ const app = express();
|
|
|
228
422
|
|
|
229
423
|
app.use(express.json());
|
|
230
424
|
|
|
231
|
-
const TURNSTILE_SECRET_KEY = 'YOUR_SECRET_KEY';
|
|
425
|
+
const TURNSTILE_SECRET_KEY = 'YOUR_SECRET_KEY'; // Replace with your actual secret key
|
|
232
426
|
|
|
233
427
|
app.post('/verify-turnstile', async (req, res) => {
|
|
234
428
|
const { token, remoteip } = req.body;
|
|
235
429
|
|
|
236
430
|
if (!token) {
|
|
237
|
-
return res.status(400).json({
|
|
431
|
+
return res.status(400).json({
|
|
432
|
+
success: false,
|
|
433
|
+
message: 'Missing token'
|
|
434
|
+
});
|
|
238
435
|
}
|
|
239
436
|
|
|
240
437
|
try {
|
|
@@ -247,7 +444,10 @@ app.post('/verify-turnstile', async (req, res) => {
|
|
|
247
444
|
const { success, error_codes } = response.data;
|
|
248
445
|
|
|
249
446
|
if (success) {
|
|
250
|
-
res.json({
|
|
447
|
+
res.json({
|
|
448
|
+
success: true,
|
|
449
|
+
message: 'Verification successful'
|
|
450
|
+
});
|
|
251
451
|
} else {
|
|
252
452
|
res.status(400).json({
|
|
253
453
|
success: false,
|
|
@@ -257,33 +457,41 @@ app.post('/verify-turnstile', async (req, res) => {
|
|
|
257
457
|
}
|
|
258
458
|
} catch (error) {
|
|
259
459
|
console.error('Turnstile verification error:', error);
|
|
260
|
-
res.status(500).json({
|
|
460
|
+
res.status(500).json({
|
|
461
|
+
success: false,
|
|
462
|
+
message: 'Internal server error'
|
|
463
|
+
});
|
|
261
464
|
}
|
|
262
465
|
});
|
|
466
|
+
|
|
467
|
+
app.listen(3000, () => {
|
|
468
|
+
console.log('Server running on port 3000');
|
|
469
|
+
});
|
|
263
470
|
```
|
|
264
471
|
|
|
265
|
-
|
|
472
|
+
### Verify Turnstile token with NestJS:
|
|
266
473
|
|
|
267
|
-
|
|
474
|
+
#### Create TurnstileGuard:
|
|
268
475
|
```bash
|
|
269
|
-
nest generate
|
|
476
|
+
nest generate guard turnstile
|
|
270
477
|
```
|
|
271
478
|
|
|
272
|
-
|
|
479
|
+
#### Add code for the guard:
|
|
273
480
|
```typescript
|
|
274
|
-
import { Injectable,
|
|
275
|
-
import { Request
|
|
481
|
+
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
|
482
|
+
import { Request } from 'express';
|
|
276
483
|
import axios from 'axios';
|
|
277
484
|
|
|
278
485
|
@Injectable()
|
|
279
|
-
export class
|
|
280
|
-
private secretKey = 'YOUR_SECRET_KEY';
|
|
486
|
+
export class TurnstileGuard implements CanActivate {
|
|
487
|
+
private readonly secretKey = 'YOUR_SECRET_KEY'; // Replace with your actual secret key
|
|
281
488
|
|
|
282
|
-
async
|
|
283
|
-
const
|
|
489
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
490
|
+
const request = context.switchToHttp().getRequest<Request>();
|
|
491
|
+
const turnstileToken = request.body.token;
|
|
284
492
|
|
|
285
493
|
if (!turnstileToken) {
|
|
286
|
-
|
|
494
|
+
throw new UnauthorizedException('Missing Turnstile token');
|
|
287
495
|
}
|
|
288
496
|
|
|
289
497
|
try {
|
|
@@ -292,78 +500,185 @@ export class TurnstileMiddleware implements NestMiddleware {
|
|
|
292
500
|
{
|
|
293
501
|
secret: this.secretKey,
|
|
294
502
|
response: turnstileToken,
|
|
295
|
-
remoteip:
|
|
503
|
+
remoteip: request.ip
|
|
296
504
|
}
|
|
297
505
|
);
|
|
298
506
|
|
|
299
507
|
const { success, error_codes } = response.data;
|
|
300
508
|
|
|
301
509
|
if (!success) {
|
|
302
|
-
|
|
303
|
-
message: 'Invalid
|
|
510
|
+
throw new UnauthorizedException({
|
|
511
|
+
message: 'Invalid Turnstile token',
|
|
304
512
|
error_codes
|
|
305
513
|
});
|
|
306
514
|
}
|
|
307
515
|
|
|
308
|
-
|
|
516
|
+
return true;
|
|
309
517
|
} catch (error) {
|
|
310
518
|
console.error('Turnstile verification error:', error);
|
|
311
|
-
|
|
519
|
+
throw new UnauthorizedException('Turnstile verification failed');
|
|
312
520
|
}
|
|
313
521
|
}
|
|
314
522
|
}
|
|
315
523
|
```
|
|
316
524
|
|
|
317
|
-
|
|
525
|
+
#### Use the Guard in Controller:
|
|
526
|
+
```typescript
|
|
527
|
+
import { Controller, Post, UseGuards, Body } from '@nestjs/common';
|
|
528
|
+
import { TurnstileGuard } from './turnstile.guard';
|
|
529
|
+
|
|
530
|
+
@Controller('api')
|
|
531
|
+
export class AppController {
|
|
532
|
+
@Post('submit')
|
|
533
|
+
@UseGuards(TurnstileGuard)
|
|
534
|
+
submitForm(@Body() body: any) {
|
|
535
|
+
// Handle form logic after Turnstile verification
|
|
536
|
+
return { message: 'Form submitted successfully!' };
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### Verification with PHP (Laravel):
|
|
542
|
+
|
|
543
|
+
```php
|
|
544
|
+
<?php
|
|
545
|
+
|
|
546
|
+
use Illuminate\Http\Request;
|
|
547
|
+
use Illuminate\Support\Facades\Http;
|
|
548
|
+
|
|
549
|
+
class TurnstileController extends Controller
|
|
550
|
+
{
|
|
551
|
+
public function verify(Request $request)
|
|
552
|
+
{
|
|
553
|
+
$token = $request->input('token');
|
|
554
|
+
$secretKey = env('TURNSTILE_SECRET_KEY'); // Add to .env
|
|
555
|
+
|
|
556
|
+
if (!$token) {
|
|
557
|
+
return response()->json([
|
|
558
|
+
'success' => false,
|
|
559
|
+
'message' => 'Missing token'
|
|
560
|
+
], 400);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
$response = Http::post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
|
|
564
|
+
'secret' => $secretKey,
|
|
565
|
+
'response' => $token,
|
|
566
|
+
'remoteip' => $request->ip()
|
|
567
|
+
]);
|
|
568
|
+
|
|
569
|
+
$result = $response->json();
|
|
570
|
+
|
|
571
|
+
if ($result['success']) {
|
|
572
|
+
return response()->json([
|
|
573
|
+
'success' => true,
|
|
574
|
+
'message' => 'Verification successful'
|
|
575
|
+
]);
|
|
576
|
+
} else {
|
|
577
|
+
return response()->json([
|
|
578
|
+
'success' => false,
|
|
579
|
+
'message' => 'Verification failed',
|
|
580
|
+
'error_codes' => $result['error_codes'] ?? []
|
|
581
|
+
], 400);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
## 🚀 Getting Started with Cloudflare Turnstile
|
|
318
588
|
|
|
319
|
-
1. **Sign up for Cloudflare**:
|
|
320
|
-
2. **Navigate to Turnstile**:
|
|
589
|
+
1. **Sign up for Cloudflare**: Go to [Cloudflare Dashboard](https://dash.cloudflare.com/)
|
|
590
|
+
2. **Navigate to Turnstile**: Find "Turnstile" in the sidebar
|
|
321
591
|
3. **Create a Site**: Click "Add Site" and configure your domain
|
|
322
592
|
4. **Get your keys**: Copy your Site Key and Secret Key
|
|
323
|
-
5. **Configure your site**: Set
|
|
593
|
+
5. **Configure your site**: Set allowed domains and other settings
|
|
324
594
|
|
|
325
|
-
## Error Handling
|
|
595
|
+
## ⚠️ Error Handling
|
|
326
596
|
|
|
327
597
|
Common error codes and their meanings:
|
|
328
598
|
|
|
329
|
-
- `missing-input-secret`:
|
|
330
|
-
- `invalid-input-secret`:
|
|
331
|
-
- `missing-input-response`:
|
|
332
|
-
- `invalid-input-response`:
|
|
333
|
-
- `bad-request`:
|
|
334
|
-
- `timeout-or-duplicate`:
|
|
599
|
+
- `missing-input-secret`: Missing secret parameter
|
|
600
|
+
- `invalid-input-secret`: Invalid or malformed secret parameter
|
|
601
|
+
- `missing-input-response`: Missing response parameter
|
|
602
|
+
- `invalid-input-response`: Invalid or malformed response parameter
|
|
603
|
+
- `bad-request`: Invalid or malformed request
|
|
604
|
+
- `timeout-or-duplicate`: Response parameter has already been validated
|
|
605
|
+
|
|
606
|
+
### Error Handling Example:
|
|
607
|
+
|
|
608
|
+
```tsx
|
|
609
|
+
import { useTurnstile } from "@thind9xdev/react-turnstile";
|
|
610
|
+
|
|
611
|
+
const ErrorHandlingExample = () => {
|
|
612
|
+
const { ref, token, error, isLoading, reset } = useTurnstile("YOUR_SITE_KEY");
|
|
613
|
+
|
|
614
|
+
const getErrorMessage = (error: string) => {
|
|
615
|
+
switch (error) {
|
|
616
|
+
case 'timeout-or-duplicate':
|
|
617
|
+
return 'Token has been used or timed out. Please try again.';
|
|
618
|
+
case 'invalid-input-response':
|
|
619
|
+
return 'Invalid response. Please refresh the page.';
|
|
620
|
+
default:
|
|
621
|
+
return `Verification error: ${error}`;
|
|
622
|
+
}
|
|
623
|
+
};
|
|
335
624
|
|
|
336
|
-
|
|
625
|
+
return (
|
|
626
|
+
<div>
|
|
627
|
+
<div ref={ref}></div>
|
|
628
|
+
{error && (
|
|
629
|
+
<div style={{ color: 'red', marginTop: '10px' }}>
|
|
630
|
+
<p>{getErrorMessage(error)}</p>
|
|
631
|
+
<button onClick={reset}>Try Again</button>
|
|
632
|
+
</div>
|
|
633
|
+
)}
|
|
634
|
+
{token && <p style={{ color: 'green' }}>✅ Verification successful!</p>}
|
|
635
|
+
</div>
|
|
636
|
+
);
|
|
637
|
+
};
|
|
638
|
+
```
|
|
337
639
|
|
|
338
|
-
|
|
640
|
+
## 🌐 Browser Support
|
|
641
|
+
|
|
642
|
+
Cloudflare Turnstile works on all modern browsers supporting:
|
|
339
643
|
- ES6 Promises
|
|
340
644
|
- Fetch API or XMLHttpRequest
|
|
341
645
|
- Modern JavaScript features
|
|
342
646
|
|
|
343
|
-
## Migration from reCAPTCHA
|
|
647
|
+
## 🔄 Migration from reCAPTCHA
|
|
344
648
|
|
|
345
|
-
If you
|
|
649
|
+
If you are migrating from Google reCAPTCHA, the main differences are:
|
|
346
650
|
|
|
347
|
-
1. **Script URL**:
|
|
651
|
+
1. **Script URL**: Use Cloudflare CDN instead of Google
|
|
348
652
|
2. **API Methods**: Different method names and parameters
|
|
349
|
-
3. **Verification endpoint**:
|
|
653
|
+
3. **Verification endpoint**: Use Cloudflare's verification API
|
|
350
654
|
4. **Configuration options**: Different theme and customization options
|
|
351
|
-
5. **Privacy**: Better privacy
|
|
655
|
+
5. **Privacy**: Better privacy as Cloudflare does not track users
|
|
656
|
+
|
|
657
|
+
### Comparison Table:
|
|
658
|
+
|
|
659
|
+
| reCAPTCHA | Turnstile |
|
|
660
|
+
|-----------|-----------|
|
|
661
|
+
| `grecaptcha.render()` | `turnstile.render()` |
|
|
662
|
+
| `grecaptcha.reset()` | `turnstile.reset()` |
|
|
663
|
+
| `grecaptcha.getResponse()` | `turnstile.getResponse()` |
|
|
664
|
+
| Google CDN | Cloudflare CDN |
|
|
665
|
+
| Tracks users | Privacy-focused |
|
|
352
666
|
|
|
353
|
-
## Contributing
|
|
667
|
+
## 🤝 Contributing
|
|
354
668
|
|
|
355
|
-
Contributions are welcome! Please
|
|
669
|
+
Contributions are welcome! Please open a Pull Request.
|
|
356
670
|
|
|
357
|
-
## License
|
|
671
|
+
## 📄 License
|
|
358
672
|
|
|
359
673
|
This project is licensed under the MIT License.
|
|
360
674
|
|
|
361
|
-
## Author
|
|
675
|
+
## 👨💻 Author
|
|
362
676
|
|
|
363
|
-
Copyright
|
|
677
|
+
Copyright 2025 thind9xdev
|
|
364
678
|
|
|
365
|
-
## Links
|
|
679
|
+
## 🔗 Links
|
|
366
680
|
|
|
367
681
|
- [Cloudflare Turnstile Documentation](https://developers.cloudflare.com/turnstile/)
|
|
368
|
-
- [GitHub Repository](https://github.com/thind9xdev/react-
|
|
369
|
-
- [NPM Package](https://www.npmjs.com/package/@thind9xdev/react-turnstile)
|
|
682
|
+
- [GitHub Repository](https://github.com/thind9xdev/react-turnstile)
|
|
683
|
+
- [NPM Package](https://www.npmjs.com/package/@thind9xdev/react-turnstile)
|
|
684
|
+
- [Quick Start Guide](./QUICK_START.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thind9xdev/react-turnstile",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A React hook for Cloudflare Turnstile integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"clean": "rm -rf dist",
|
|
10
10
|
"prebuild": "npm run clean",
|
|
11
|
-
"
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"react",
|