Skip to content

Commit 8bb18de

Browse files
feat: implement Cloudflare Turnstile integration and add verification details form to registration page
1 parent 0fe50b4 commit 8bb18de

8 files changed

Lines changed: 121 additions & 18 deletions

File tree

‎Backend/.env.sample‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ ADMIN_PASSWORD=admin123
4141
# [REQUIRED] Used to generate certificate verification links
4242
FRONTEND_URL=http://localhost:5173
4343

44-
# Bot Protection
45-
# [REQUIRED] Cloudflare Turnstile Secret
46-
TURNSTILE_SECRET=
44+
# Bot Protection (Cloudflare Turnstile)
45+
# [REQUIRED] Cloudflare Turnstile Secret Key (Server-side only)
46+
TURNSTILE_SECRET_KEY=0x4AAAAAA...
47+
# [OPTIONAL] Allowed hostnames (comma-separated whitelist)
48+
TURNSTILE_HOSTNAMES=qucodex.com,www.qucodex.com,localhost,127.0.0.1

‎Frontend/.env.example‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
VITE_TURNSTILE_SITE_KEY=your_cloudflare_turnstile_site_key_here
2-
VITE_API_BASE_URL=http://localhost:5000/api/v1 # Use in production (e.g., https://api.myproduction.com/api/v1)
1+
# Cloudflare Turnstile Public Site Key (DO NOT place Secret Key here)
2+
VITE_TURNSTILE_SITE_KEY=0x4AAAAAA...
3+
VITE_API_BASE_URL=http://localhost:5000/api/v1 # Use in production (e.g., https://api.qucodex.com/api/v1)

‎Frontend/package-lock.json‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Frontend/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"postinstall": "node -e \"require('fs').cpSync('node_modules/tinymce', 'public/tinymce', {recursive: true, force: true})\""
1212
},
1313
"dependencies": {
14-
"@marsidev/react-turnstile": "^1.5.3",
14+
"@marsidev/react-turnstile": "^1.6.0",
1515
"@reduxjs/toolkit": "^2.12.0",
1616
"@tinymce/tinymce-react": "^6.3.0",
1717
"axios": "^1.18.1",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import TurnstileWidget from "./TurnstileWidget";
2+
3+
export default TurnstileWidget;
4+
export { TurnstileWidget as CloudflareTurnstile };
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React, { forwardRef, useImperativeHandle, useRef } from "react";
2+
import { Turnstile } from "@marsidev/react-turnstile";
3+
4+
/**
5+
* Production-ready Cloudflare Turnstile Component.
6+
* Powered by @marsidev/react-turnstile for React 19 / StrictMode / SPA lifecycle safety.
7+
*
8+
* - Zero custom postMessage or manual iframe manipulation.
9+
* - Handles script loading, execution, and cleanup automatically.
10+
* - Forwards reset(), remove(), and getResponse() methods via ref.
11+
* - Safe against duplicate rendering and StrictMode remounts.
12+
*/
13+
const TurnstileWidget = forwardRef(
14+
(
15+
{
16+
siteKey = import.meta.env.VITE_TURNSTILE_SITE_KEY,
17+
action = "default",
18+
theme = "auto",
19+
size = "invisible",
20+
onSuccess,
21+
onError,
22+
onExpire,
23+
className = "",
24+
id,
25+
},
26+
ref
27+
) => {
28+
const innerRef = useRef(null);
29+
30+
useImperativeHandle(ref, () => ({
31+
reset: () => {
32+
try {
33+
innerRef.current?.reset();
34+
} catch (err) {
35+
console.warn("[Turnstile] Reset failed:", err);
36+
}
37+
},
38+
remove: () => {
39+
try {
40+
innerRef.current?.remove();
41+
} catch (err) {
42+
console.warn("[Turnstile] Remove failed:", err);
43+
}
44+
},
45+
render: () => {
46+
try {
47+
innerRef.current?.render();
48+
} catch (err) {
49+
console.warn("[Turnstile] Render failed:", err);
50+
}
51+
},
52+
getResponse: () => {
53+
return innerRef.current?.getResponse();
54+
},
55+
}));
56+
57+
if (!siteKey) {
58+
if (import.meta.env.DEV) {
59+
console.warn("[Turnstile] VITE_TURNSTILE_SITE_KEY is not configured.");
60+
}
61+
return null;
62+
}
63+
64+
return (
65+
<div id={id} className={`turnstile-wrapper ${className}`.trim()}>
66+
<Turnstile
67+
ref={innerRef}
68+
siteKey={siteKey}
69+
options={{
70+
action,
71+
theme,
72+
size,
73+
retry: "auto",
74+
refreshExpired: "auto",
75+
}}
76+
onSuccess={(token) => {
77+
onSuccess?.(token);
78+
}}
79+
onError={(err) => {
80+
onError?.(err);
81+
}}
82+
onExpire={() => {
83+
onExpire?.();
84+
}}
85+
/>
86+
</div>
87+
);
88+
}
89+
);
90+
91+
TurnstileWidget.displayName = "TurnstileWidget";
92+
93+
export default TurnstileWidget;
94+
export { TurnstileWidget as CloudflareTurnstile };

‎Frontend/src/components/register/VerificationDetailsForm.jsx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useState } from "react";
22
import { Link } from "react-router-dom";
33
import { ShieldCheck, Loader2, ZoomIn, X, QrCode } from "lucide-react";
4-
import { Turnstile } from "@marsidev/react-turnstile";
4+
import TurnstileWidget from "../common/TurnstileWidget";
55

66
export default function VerificationDetailsForm({
77
register,
@@ -116,17 +116,18 @@ export default function VerificationDetailsForm({
116116
</div>
117117

118118
{/* Security Protection (Invisible Turnstile) */}
119-
<Turnstile
119+
<TurnstileWidget
120120
ref={turnstileRef}
121121
id="turnstile-register"
122122
siteKey={import.meta.env.VITE_TURNSTILE_SITE_KEY}
123+
action="register"
124+
size="invisible"
123125
onSuccess={(token) => setTurnstileToken(token)}
124126
onError={(err) => {
125-
console.warn("Turnstile verification widget warning:", err);
127+
console.warn("[Turnstile] Register widget error:", err);
126128
setTurnstileToken(null);
127129
}}
128130
onExpire={() => setTurnstileToken(null)}
129-
options={{ theme: "auto", action: "register", size: "invisible" }}
130131
/>
131132
<div className="mb-6 flex items-center justify-center gap-1.5 text-[11px] font-mono text-text-muted/60">
132133
<ShieldCheck className="w-3.5 h-3.5 text-accent" /> Protected by Cloudflare Turnstile

‎Frontend/src/features/landing/components/ContactSection.jsx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useState, useRef } from "react";
22
import { useForm } from "react-hook-form";
33
import { useDispatch } from "react-redux";
44
import { setError } from "../../../context/messageSlice";
5-
import { Turnstile } from "@marsidev/react-turnstile";
5+
import TurnstileWidget from "../../../components/common/TurnstileWidget";
66
import axiosInstance from "../../../services/axiosInstance";
77
import contentData from "../../../data/content.json";
88
import {
@@ -227,17 +227,18 @@ const ContactSection = () => {
227227

228228
<div className="pt-4 flex flex-col sm:flex-row items-center justify-between gap-6 border-t border-border-soft">
229229
<div className="w-full sm:w-auto flex items-center justify-center gap-1.5 text-[11px] font-mono text-text-muted/60">
230-
<Turnstile
230+
<TurnstileWidget
231231
ref={turnstileRef}
232232
id="turnstile-contact"
233233
siteKey={import.meta.env.VITE_TURNSTILE_SITE_KEY}
234+
action="contact"
235+
size="invisible"
234236
onSuccess={(token) => setTurnstileToken(token)}
235237
onError={(err) => {
236-
console.warn("Turnstile widget error:", err);
238+
console.warn("[Turnstile] Contact widget error:", err);
237239
setTurnstileToken(null);
238240
}}
239241
onExpire={() => setTurnstileToken(null)}
240-
options={{ theme: "auto", action: "contact", size: "invisible" }}
241242
/>
242243
<span>Protected by Cloudflare Turnstile</span>
243244
</div>

0 commit comments

Comments
 (0)