feat: add Turnstile anti-bot, email field, and metadata to contact form
- Add Cloudflare Turnstile widget support (site key configurable in config.js, secret key in contact.php — both empty until widget created at dash.cloudflare.com) - Add email input field to contact form (parity with hhivp) - Add company length validation (200 chars) to contact.php - Add IP, country (CF-IPCountry header), and referer metadata to Telegram notifications - Add company and email fields to SMTP email body - Turnstile script loaded in index.html, widget rendered conditionally when TURNSTILE_SITE_KEY is set Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -121,6 +121,9 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Cloudflare Turnstile -->
|
||||||
|
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||||
|
|
||||||
<!-- Google tag (gtag.js) -->
|
<!-- Google tag (gtag.js) -->
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-C9J0D8FFH3"></script>
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-C9J0D8FFH3"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -37,9 +37,11 @@ if (!$data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$name = htmlspecialchars(trim($data['name'] ?? ''), ENT_QUOTES);
|
$name = htmlspecialchars(trim($data['name'] ?? ''), ENT_QUOTES);
|
||||||
|
$company = htmlspecialchars(trim($data['company'] ?? ''), ENT_QUOTES);
|
||||||
$phone = htmlspecialchars(trim($data['phone'] ?? ''), ENT_QUOTES);
|
$phone = htmlspecialchars(trim($data['phone'] ?? ''), ENT_QUOTES);
|
||||||
$email = htmlspecialchars(trim($data['email'] ?? ''), ENT_QUOTES);
|
$email = htmlspecialchars(trim($data['email'] ?? ''), ENT_QUOTES);
|
||||||
$message = htmlspecialchars(trim($data['message'] ?? ''), ENT_QUOTES);
|
$message = htmlspecialchars(trim($data['message'] ?? ''), ENT_QUOTES);
|
||||||
|
$turnstileToken = trim($data['turnstileToken'] ?? '');
|
||||||
|
|
||||||
if (!$name || !$message) {
|
if (!$name || !$message) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
@@ -54,20 +56,54 @@ if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
if (mb_strlen($name) > 100) { http_response_code(400); echo json_encode(['error' => 'Name too long']); exit; }
|
if (mb_strlen($name) > 100) { http_response_code(400); echo json_encode(['error' => 'Name too long']); exit; }
|
||||||
|
if (mb_strlen($company) > 200) { http_response_code(400); echo json_encode(['error' => 'Company too long']); exit; }
|
||||||
if (mb_strlen($email) > 254) { http_response_code(400); echo json_encode(['error' => 'Email too long']); exit; }
|
if (mb_strlen($email) > 254) { http_response_code(400); echo json_encode(['error' => 'Email too long']); exit; }
|
||||||
if (mb_strlen($phone) > 30) { http_response_code(400); echo json_encode(['error' => 'Phone too long']); exit; }
|
if (mb_strlen($phone) > 30) { http_response_code(400); echo json_encode(['error' => 'Phone too long']); exit; }
|
||||||
if (mb_strlen($message) > 5000) { http_response_code(400); echo json_encode(['error' => 'Message too long']); exit; }
|
if (mb_strlen($message) > 5000) { http_response_code(400); echo json_encode(['error' => 'Message too long']); exit; }
|
||||||
|
|
||||||
|
// ─── Cloudflare Turnstile verification ───────────────────────────────────────
|
||||||
|
$TURNSTILE_SECRET = ''; // TODO: Create widget at dash.cloudflare.com → Turnstile → Add site (sag24.ru)
|
||||||
|
if ($TURNSTILE_SECRET) {
|
||||||
|
if (!$turnstileToken) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Bot verification required.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$ch = curl_init('https://challenges.cloudflare.com/turnstile/v0/siteverify');
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => http_build_query(['secret' => $TURNSTILE_SECRET, 'response' => $turnstileToken, 'remoteip' => $ip]),
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 10,
|
||||||
|
]);
|
||||||
|
$tsResult = json_decode(curl_exec($ch), true);
|
||||||
|
curl_close($ch);
|
||||||
|
if (($tsResult['success'] ?? false) !== true) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Bot verification failed. Please try again.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Telegram (best-effort, not fatal) ───────────────────────────────────────
|
// ─── Telegram (best-effort, not fatal) ───────────────────────────────────────
|
||||||
$BOT_TOKEN = '8138813013:AAElH2L5NspRLSdiFjDz6Qf32n4G24P_cj8';
|
$BOT_TOKEN = '8138813013:AAElH2L5NspRLSdiFjDz6Qf32n4G24P_cj8';
|
||||||
$CHAT_ID = '-5230603582';
|
$CHAT_ID = '-5230603582';
|
||||||
|
|
||||||
|
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
|
||||||
|
$referer = $_SERVER['HTTP_REFERER'] ?? '';
|
||||||
|
$country = $_SERVER['HTTP_CF_IPCOUNTRY'] ?? '';
|
||||||
|
|
||||||
$text = "🔔 <b>Заявка с sag24.ru</b>\n\n";
|
$text = "🔔 <b>Заявка с sag24.ru</b>\n\n";
|
||||||
$text .= "👤 <b>Имя:</b> {$name}\n";
|
$text .= "👤 <b>Имя:</b> {$name}\n";
|
||||||
if ($phone) $text .= "📱 <b>Телефон:</b> {$phone}\n";
|
if ($company) $text .= "🏢 <b>Компания:</b> {$company}\n";
|
||||||
if ($email) $text .= "📧 <b>Email:</b> {$email}\n";
|
if ($email) $text .= "📧 <b>Email:</b> {$email}\n";
|
||||||
|
if ($phone) $text .= "📱 <b>Телефон:</b> {$phone}\n";
|
||||||
$text .= "\n💬 <b>Сообщение:</b>\n{$message}\n";
|
$text .= "\n💬 <b>Сообщение:</b>\n{$message}\n";
|
||||||
$text .= "\n⏰ " . date('d.m.Y H:i', time() + 3 * 3600) . " MSK";
|
$text .= "\n📊 <b>Метаданные:</b>\n";
|
||||||
|
$text .= "🌍 IP: {$ip}\n";
|
||||||
|
if ($country) $text .= "🌐 Страна: {$country}\n";
|
||||||
|
if ($referer) $text .= "🔗 Источник: {$referer}\n";
|
||||||
|
$text .= "⏰ " . date('d.m.Y H:i', time() + 3 * 3600) . " MSK";
|
||||||
|
|
||||||
$ch = curl_init("https://tg-relay.it-resheniya-2018.workers.dev/bot{$BOT_TOKEN}/sendMessage");
|
$ch = curl_init("https://tg-relay.it-resheniya-2018.workers.dev/bot{$BOT_TOKEN}/sendMessage");
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
@@ -89,9 +125,12 @@ $mail_to = 'info@sag24.ru';
|
|||||||
|
|
||||||
$subject = "=?UTF-8?B?" . base64_encode("Заявка с sag24.ru от {$name}") . "?=";
|
$subject = "=?UTF-8?B?" . base64_encode("Заявка с sag24.ru от {$name}") . "?=";
|
||||||
$body = "Имя: {$name}\r\n";
|
$body = "Имя: {$name}\r\n";
|
||||||
if ($phone) $body .= "Телефон: {$phone}\r\n";
|
if ($company) $body .= "Компания: {$company}\r\n";
|
||||||
if ($email) $body .= "Email: {$email}\r\n";
|
if ($email) $body .= "Email: {$email}\r\n";
|
||||||
|
if ($phone) $body .= "Телефон: {$phone}\r\n";
|
||||||
$body .= "\r\nСообщение:\r\n{$message}\r\n";
|
$body .= "\r\nСообщение:\r\n{$message}\r\n";
|
||||||
|
$body .= "\r\nIP: {$ip}";
|
||||||
|
if ($country) $body .= " | Страна: {$country}";
|
||||||
$body .= "\r\n" . date('d.m.Y H:i', time() + 3 * 3600) . " MSK";
|
$body .= "\r\n" . date('d.m.Y H:i', time() + 3 * 3600) . " MSK";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,3 +3,6 @@ export const WHATSAPP_PHONE = '74953637476' // без +
|
|||||||
export const TELEGRAM_USERNAME = 'sag24ru' // @username без @
|
export const TELEGRAM_USERNAME = 'sag24ru' // @username без @
|
||||||
export const FORM_ENDPOINT = '/api/contact.php'
|
export const FORM_ENDPOINT = '/api/contact.php'
|
||||||
export const EMAIL = 'info@sag24.ru'
|
export const EMAIL = 'info@sag24.ru'
|
||||||
|
|
||||||
|
// Cloudflare Turnstile — создать виджет: dash.cloudflare.com → Turnstile → Add site (sag24.ru)
|
||||||
|
export const TURNSTILE_SITE_KEY = ''
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react'
|
|||||||
import { ChevronRight, Server, Shield, Headphones, Phone, Mail, MapPin, CheckCircle2, ChevronDown } from 'lucide-react'
|
import { ChevronRight, Server, Shield, Headphones, Phone, Mail, MapPin, CheckCircle2, ChevronDown } from 'lucide-react'
|
||||||
import { useLanguage } from '../contexts/LanguageContext.jsx'
|
import { useLanguage } from '../contexts/LanguageContext.jsx'
|
||||||
import { useReveal } from '../components/useReveal.js'
|
import { useReveal } from '../components/useReveal.js'
|
||||||
import { FORM_ENDPOINT, EMAIL } from '../config.js'
|
import { FORM_ENDPOINT, EMAIL, TURNSTILE_SITE_KEY } from '../config.js'
|
||||||
|
|
||||||
const serviceIcons = [Server, Shield, Headphones]
|
const serviceIcons = [Server, Shield, Headphones]
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ function FaqItem({ q, a }) {
|
|||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { t } = useLanguage()
|
const { t } = useLanguage()
|
||||||
const [formState, setFormState] = useState({ name: '', company: '', phone: '', message: '' })
|
const [formState, setFormState] = useState({ name: '', company: '', email: '', phone: '', message: '' })
|
||||||
const [submitted, setSubmitted] = useState(false)
|
const [submitted, setSubmitted] = useState(false)
|
||||||
const [sending, setSending] = useState(false)
|
const [sending, setSending] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
@@ -70,10 +70,11 @@ export default function Home() {
|
|||||||
|
|
||||||
if (FORM_ENDPOINT) {
|
if (FORM_ENDPOINT) {
|
||||||
try {
|
try {
|
||||||
|
const turnstileToken = document.querySelector('[name="cf-turnstile-response"]')?.value || ''
|
||||||
const res = await fetch(FORM_ENDPOINT, {
|
const res = await fetch(FORM_ENDPOINT, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||||
body: JSON.stringify(formState),
|
body: JSON.stringify({ ...formState, turnstileToken }),
|
||||||
})
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setSubmitted(true)
|
setSubmitted(true)
|
||||||
@@ -316,6 +317,13 @@ export default function Home() {
|
|||||||
onChange={e => setFormState({ ...formState, company: e.target.value })}
|
onChange={e => setFormState({ ...formState, company: e.target.value })}
|
||||||
className="w-full bg-white/10 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-slate-400 focus:outline-none focus:border-blue-500 transition-colors text-sm"
|
className="w-full bg-white/10 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-slate-400 focus:outline-none focus:border-blue-500 transition-colors text-sm"
|
||||||
/>
|
/>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder={t('contact.formEmail')}
|
||||||
|
value={formState.email}
|
||||||
|
onChange={e => setFormState({ ...formState, email: e.target.value })}
|
||||||
|
className="w-full bg-white/10 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-slate-400 focus:outline-none focus:border-blue-500 transition-colors text-sm"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
placeholder={t('contact.formPhone')}
|
placeholder={t('contact.formPhone')}
|
||||||
@@ -331,6 +339,13 @@ export default function Home() {
|
|||||||
onChange={e => setFormState({ ...formState, message: e.target.value })}
|
onChange={e => setFormState({ ...formState, message: e.target.value })}
|
||||||
className="w-full bg-white/10 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-slate-400 focus:outline-none focus:border-blue-500 transition-colors text-sm resize-none"
|
className="w-full bg-white/10 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-slate-400 focus:outline-none focus:border-blue-500 transition-colors text-sm resize-none"
|
||||||
/>
|
/>
|
||||||
|
{TURNSTILE_SITE_KEY && (
|
||||||
|
<div
|
||||||
|
className="cf-turnstile"
|
||||||
|
data-sitekey={TURNSTILE_SITE_KEY}
|
||||||
|
data-theme="dark"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{error && <p className="text-red-400 text-sm">{error}</p>}
|
{error && <p className="text-red-400 text-sm">{error}</p>}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export const en = {
|
|||||||
addressValue: 'Moscow Region, Pushkino, 38/14',
|
addressValue: 'Moscow Region, Pushkino, 38/14',
|
||||||
formName: 'Your name',
|
formName: 'Your name',
|
||||||
formCompany: 'Company',
|
formCompany: 'Company',
|
||||||
|
formEmail: 'Your email',
|
||||||
formMessage: 'Describe your task',
|
formMessage: 'Describe your task',
|
||||||
formPhone: 'Your phone',
|
formPhone: 'Your phone',
|
||||||
formSubmit: 'Send Request',
|
formSubmit: 'Send Request',
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export const ru = {
|
|||||||
addressValue: 'Московская область, Пушкино, д. 38/14',
|
addressValue: 'Московская область, Пушкино, д. 38/14',
|
||||||
formName: 'Ваше имя',
|
formName: 'Ваше имя',
|
||||||
formCompany: 'Компания',
|
formCompany: 'Компания',
|
||||||
|
formEmail: 'Ваш email',
|
||||||
formMessage: 'Опишите задачу',
|
formMessage: 'Опишите задачу',
|
||||||
formPhone: 'Ваш телефон',
|
formPhone: 'Ваш телефон',
|
||||||
formSubmit: 'Отправить заявку',
|
formSubmit: 'Отправить заявку',
|
||||||
|
|||||||
Reference in New Issue
Block a user