From 2551b2923e05dc0f3921be2f0951654e96d3cbe0 Mon Sep 17 00:00:00 2001 From: Vishva Date: Wed, 29 Jul 2026 17:08:48 +0530 Subject: [PATCH] Add TurnstileCaptcha component and update send API handler --- .env.example | 14 ++++ api/send.js | 116 ++++++++++++++++++++++------ server/routes/contact.js | 31 +++++++- src/components/ContactSection.jsx | 37 +++++++-- src/components/TurnstileCaptcha.jsx | 90 +++++++++++++++++++++ vite.config.js | 4 +- 6 files changed, 259 insertions(+), 33 deletions(-) create mode 100644 src/components/TurnstileCaptcha.jsx diff --git a/.env.example b/.env.example index 36eb0ba..f51b8cd 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,20 @@ CORS_ORIGIN=http://localhost:5173 UPLOAD_DIR=./server/uploads DATA_DIR=./server/data +# SMTP Mail Service Settings for Contact Us Form +SMTP_HOST=smtp.gmail.com +SMTP_PORT=465 +SMTP_SECURE=true +SMTP_USER=info@cavinfotech.com +SMTP_PASS=your-smtp-password-or-app-password +SMTP_FROM="Cavin Infotech Contact Form" +SMTP_TO=info@cavinfotech.com +# Cloudflare Turnstile Settings (Use Cloudflare testing keys by default) +# Sitekey for frontend: 1x00000000000000000000AA (Always Passes - Visible Widget) +VITE_TURNSTILE_SITE_KEY=1x00000000000000000000AA +# Secret key for backend: 1x000000000000000000000000000000AA +TURNSTILE_SECRET_KEY=1x000000000000000000000000000000AA + # Production (demo.cavinkare.in/citpl_website): # 1. Copy dist/ + server/ + package.json + .env to the host # 2. npm install --omit=dev && npm run server (or pm2 start server/index.js) diff --git a/api/send.js b/api/send.js index 7ddd167..1124e7e 100644 --- a/api/send.js +++ b/api/send.js @@ -1,5 +1,6 @@ +import nodemailer from 'nodemailer'; + export default async function handler(req, res) { - // Set CORS headers for local development if needed res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); @@ -12,41 +13,106 @@ export default async function handler(req, res) { return res.status(405).json({ error: 'Method Not Allowed' }); } - const { type, name, email, phone, mobile, designation, dateTime, interests, resumeName } = req.body; + const { type, name, email, phone, mobile, subject, message, interests, turnstileToken } = req.body; - const serviceId = process.env.EMAILJS_SERVICE_ID || 'vishva_cavintest'; - const publicKey = process.env.EMAILJS_PUBLIC_KEY || 'YOUR_EMAILJS_PUBLIC_KEY'; - - let templateId = ''; - if (type === 'careers') { - templateId = process.env.EMAILJS_TEMPLATE_ID_CAREERS || 'YOUR_EMAILJS_TEMPLATE_ID_CAREERS'; - } else { - templateId = process.env.EMAILJS_TEMPLATE_ID_STRATEGY || 'YOUR_EMAILJS_TEMPLATE_ID_STRATEGY'; + const contactName = name?.trim(); + const contactEmail = email?.trim(); + const contactPhone = (phone || mobile || '').trim(); + const contactSubject = (subject || interests || type || 'General Inquiry').trim(); + const contactMessage = (message || req.body.comments || '').trim(); + + if (!contactName || !contactEmail) { + return res.status(400).json({ error: 'Name and Email are required.' }); } - const templateParams = { - type: type, - name: name, - email: email, - phone: phone || mobile || '', - mobile: mobile || phone || '', - designation: designation || 'N/A', - dateTime: dateTime || 'N/A', - interests: interests || 'N/A', - resumeName: resumeName || 'None' - }; + // Verify Cloudflare Turnstile Token + if (turnstileToken) { + const secretKey = process.env.TURNSTILE_SECRET_KEY || '1x000000000000000000000000000000AA'; + try { + const formData = new URLSearchParams(); + formData.append('secret', secretKey); + formData.append('response', turnstileToken); + const turnstileRes = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { + method: 'POST', + body: formData + }); + const turnstileOutcome = await turnstileRes.json(); + if (!turnstileOutcome.success) { + return res.status(400).json({ error: 'Cloudflare Turnstile verification failed. Please try again.' }); + } + } catch (err) { + console.error('Turnstile verification error:', err); + } + } + + // 1. Try sending via SMTP (nodemailer) + const smtpHost = process.env.SMTP_HOST; + const smtpUser = process.env.SMTP_USER; + const smtpPass = process.env.SMTP_PASS; + + if (smtpHost && smtpUser && smtpPass) { + try { + const port = parseInt(process.env.SMTP_PORT || '465', 10); + const secure = process.env.SMTP_SECURE === 'true' || port === 465; + + const transporter = nodemailer.createTransport({ + host: smtpHost, + port, + secure, + auth: { user: smtpUser, pass: smtpPass }, + tls: { rejectUnauthorized: false } + }); + + const toEmail = process.env.SMTP_TO || smtpUser; + const fromEmail = process.env.SMTP_FROM || `"Cavin Infotech Contact" <${smtpUser}>`; + + const info = await transporter.sendMail({ + from: fromEmail, + to: toEmail, + replyTo: `"${contactName}" <${contactEmail}>`, + subject: `[Contact Form] ${contactSubject}`, + text: `Name: ${contactName}\nEmail: ${contactEmail}\nPhone: ${contactPhone}\nSubject: ${contactSubject}\n\nMessage:\n${contactMessage}`, + html: ` +
+

Cavin Infotech - Contact Inquiry

+

Name: ${contactName}

+

Email: ${contactEmail}

+

Phone: ${contactPhone || 'Not provided'}

+

Subject: ${contactSubject}

+
+

${contactMessage}

+
+
+ ` + }); + + return res.status(200).json({ success: true, messageId: info.messageId, message: 'Message sent via SMTP!' }); + } catch (smtpErr) { + console.error('SMTP Send Error:', smtpErr); + return res.status(500).json({ error: `SMTP Send Error: ${smtpErr.message}` }); + } + } + + // 2. Fallback to EmailJS API if SMTP env vars are not set + const serviceId = process.env.EMAILJS_SERVICE_ID || 'vishva_cavintest'; + const publicKey = process.env.EMAILJS_PUBLIC_KEY || 'YOUR_EMAILJS_PUBLIC_KEY'; + const templateId = process.env.EMAILJS_TEMPLATE_ID_STRATEGY || 'YOUR_EMAILJS_TEMPLATE_ID_STRATEGY'; try { const response = await fetch('https://api.emailjs.com/api/v1.0/email/send', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ service_id: serviceId, template_id: templateId, user_id: publicKey, - template_params: templateParams + template_params: { + name: contactName, + email: contactEmail, + phone: contactPhone, + interests: contactSubject, + message: contactMessage + } }), }); diff --git a/server/routes/contact.js b/server/routes/contact.js index d431c1f..39ff20f 100644 --- a/server/routes/contact.js +++ b/server/routes/contact.js @@ -4,10 +4,31 @@ import { sendContactEmail } from '../services/mailService.js'; const router = express.Router(); +async function verifyTurnstile(token, remoteip) { + const secretKey = process.env.TURNSTILE_SECRET_KEY || '1x000000000000000000000000000000AA'; + if (!token) return { success: false, 'error-codes': ['missing-input-response'] }; + + try { + const formData = new URLSearchParams(); + formData.append('secret', secretKey); + formData.append('response', token); + if (remoteip) formData.append('remoteip', remoteip); + + const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { + method: 'POST', + body: formData + }); + return await res.json(); + } catch (err) { + console.error('Turnstile verification exception:', err); + return { success: true }; + } +} + // POST /api/contact - Handle Contact Us Form submission via SMTP router.post('/', async (req, res) => { try { - const { name, email, phone, mobile, subject, message, interests, type } = req.body; + const { name, email, phone, mobile, subject, message, interests, type, turnstileToken } = req.body; const contactName = name?.trim(); const contactEmail = email?.trim(); @@ -19,6 +40,14 @@ router.post('/', async (req, res) => { return res.status(400).json({ error: 'Name and Email are required.' }); } + // Verify Cloudflare Turnstile Token + if (turnstileToken) { + const turnstileRes = await verifyTurnstile(turnstileToken, req.ip); + if (!turnstileRes.success) { + return res.status(400).json({ error: 'Cloudflare Turnstile security verification failed. Please try again.' }); + } + } + // Save lead record in database const lead = saveLead({ name: contactName, diff --git a/src/components/ContactSection.jsx b/src/components/ContactSection.jsx index 431398a..4ec7856 100644 --- a/src/components/ContactSection.jsx +++ b/src/components/ContactSection.jsx @@ -1,7 +1,8 @@ -import React, { useState } from 'react'; +import React, { useState, useCallback } from 'react'; import { motion } from 'framer-motion'; import { assetUrl } from '../lib/assetUrl'; -import { Mail, Phone, Clock, ArrowUpRight, Check } from 'lucide-react'; +import { Mail, Phone, Clock, ArrowUpRight, Check, ShieldCheck } from 'lucide-react'; +import TurnstileCaptcha from './TurnstileCaptcha'; export default function ContactSection({ isMobile, onOpenStrategy }) { const [formData, setFormData] = useState({ @@ -11,10 +12,21 @@ export default function ContactSection({ isMobile, onOpenStrategy }) { subject: '', message: '' }); + + const [turnstileToken, setTurnstileToken] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [submitted, setSubmitted] = useState(false); const [errorMsg, setErrorMsg] = useState(''); + const handleTurnstileVerify = useCallback((token) => { + setTurnstileToken(token); + if (errorMsg) setErrorMsg(''); + }, [errorMsg]); + + const handleTurnstileExpire = useCallback(() => { + setTurnstileToken(''); + }, []); + const handleChange = (e) => { setFormData({ ...formData, [e.target.name]: e.target.value }); if (errorMsg) setErrorMsg(''); @@ -27,6 +39,11 @@ export default function ContactSection({ isMobile, onOpenStrategy }) { return; } + if (!turnstileToken) { + setErrorMsg('Please complete the Cloudflare Turnstile security verification.'); + return; + } + setIsSubmitting(true); setErrorMsg(''); @@ -40,7 +57,8 @@ export default function ContactSection({ isMobile, onOpenStrategy }) { email: formData.email, phone: formData.phone, subject: formData.subject, - message: formData.message + message: formData.message, + turnstileToken }) }); @@ -54,7 +72,8 @@ export default function ContactSection({ isMobile, onOpenStrategy }) { email: formData.email, phone: formData.phone, subject: formData.subject, - message: formData.message + message: formData.message, + turnstileToken }) }); } @@ -64,13 +83,15 @@ export default function ContactSection({ isMobile, onOpenStrategy }) { if (response.ok && resData.success) { setSubmitted(true); setFormData({ name: '', email: '', phone: '', subject: '', message: '' }); + setTurnstileToken(''); } else { - setErrorMsg(resData.error || resData.smtpError || 'Failed to send message. Please check SMTP configuration.'); + setErrorMsg(resData.error || resData.smtpError || 'Cloudflare Turnstile verification failed. Please try again.'); } } catch (err) { console.error('Contact submit error:', err); setSubmitted(true); setFormData({ name: '', email: '', phone: '', subject: '', message: '' }); + setTurnstileToken(''); } finally { setIsSubmitting(false); } @@ -543,6 +564,12 @@ export default function ContactSection({ isMobile, onOpenStrategy }) { /> + {/* Cloudflare Turnstile Security Verification */} + + {/* Submit Button */}