diff --git a/Cargo.lock b/Cargo.lock index 8800297..e095c4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3353,7 +3353,7 @@ dependencies = [ [[package]] name = "web-petting" -version = "0.1.9" +version = "0.1.10" dependencies = [ "chrono", "chrono-tz", diff --git a/src/admin.rs b/src/admin.rs index 62f3b75..56dc195 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -798,6 +798,8 @@ struct SettingsForm { timezone: String, site_domain: String, seo_keywords: String, + turnstile_site_key: String, + turnstile_secret_key: String, } async fn save_settings(request: Request, session: Session, db: Database) -> cot::Result { @@ -814,6 +816,8 @@ async fn save_settings(request: Request, session: Session, db: Database) -> cot: ("timezone", form.timezone), ("site_domain", form.site_domain), ("seo_keywords", form.seo_keywords), + ("turnstile_site_key", form.turnstile_site_key), + ("turnstile_secret_key", form.turnstile_secret_key), ] { let k = key.to_string(); let existing = query!(Setting, $key == k).get(&db).await?; diff --git a/src/i18n.rs b/src/i18n.rs index 1e99fa3..f17a151 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -135,6 +135,8 @@ pub struct Translations { pub settings_timezone: &'static str, pub settings_site_domain: &'static str, pub settings_seo_keywords: &'static str, + pub settings_turnstile_site_key: &'static str, + pub settings_turnstile_secret_key: &'static str, pub landing_contact_label: &'static str, pub landing_pricing_title: &'static str, @@ -348,6 +350,8 @@ static RU: Translations = Translations { settings_timezone: "Часовой пояс (например Asia/Vladivostok)", settings_site_domain: "Домен сайта (например https://example.com)", settings_seo_keywords: "SEO-ключевые слова (через запятую, отображаются на сайте и в мета-теге keywords)", + settings_turnstile_site_key: "Cloudflare Turnstile — Site Key (ключ виджета)", + settings_turnstile_secret_key: "Cloudflare Turnstile — Secret Key (секретный ключ)", landing_contact_label: "Или свяжитесь с нами напрямую", landing_pricing_title: "Стоимость", @@ -551,6 +555,8 @@ static EN: Translations = Translations { settings_timezone: "Timezone (e.g. Asia/Vladivostok)", settings_site_domain: "Site domain (e.g. https://example.com)", settings_seo_keywords: "SEO keywords (comma-separated, shown on site and in keywords meta tag)", + settings_turnstile_site_key: "Cloudflare Turnstile — Site Key", + settings_turnstile_secret_key: "Cloudflare Turnstile — Secret Key", landing_contact_label: "Or contact us directly", landing_pricing_title: "Pricing", diff --git a/src/public.rs b/src/public.rs index 4c33ba8..4363df5 100644 --- a/src/public.rs +++ b/src/public.rs @@ -76,6 +76,7 @@ struct LandingTemplate<'a> { testimonials: Vec, site_domain: String, review_count: usize, + turnstile_site_key: String, } #[derive(Debug, Template)] @@ -138,6 +139,12 @@ async fn landing_page(request: Request, db: Database) -> cot::Result { .await? .map(|s| s.value) .unwrap_or_default(); + let turnstile_key = "turnstile_site_key".to_string(); + let turnstile_site_key = query!(Setting, $key == turnstile_key) + .get(&db) + .await? + .map(|s| s.value) + .unwrap_or_default(); let mut testimonials = Testimonial::objects().all(&db).await?; testimonials.retain(|t| t.status == "active"); testimonials.sort_by(|a, b| a.sort_order.cmp(&b.sort_order)); @@ -151,6 +158,7 @@ async fn landing_page(request: Request, db: Database) -> cot::Result { testimonials, site_domain, review_count, + turnstile_site_key, } .render()?; html_response(body, lang) @@ -161,6 +169,8 @@ struct LeadForm { name: String, phone: Option, comment: Option, + #[serde(default, rename = "cf-turnstile-response")] + cf_turnstile_response: Option, } async fn submit_lead(request: Request, db: Database) -> cot::Result { @@ -170,6 +180,37 @@ async fn submit_lead(request: Request, db: Database) -> cot::Result { let form: LeadForm = serde_html_form::from_bytes(&bytes).map_err(|e| cot::Error::internal(e.to_string()))?; + // Turnstile CAPTCHA verification (only when secret key is configured) + let secret_key_name = "turnstile_secret_key".to_string(); + let secret_key = query!(Setting, $key == secret_key_name) + .get(&db) + .await? + .map(|s| s.value) + .filter(|s| !s.is_empty()); + if let Some(secret) = secret_key { + let token = form.cf_turnstile_response.as_deref().unwrap_or(""); + let client = reqwest::Client::new(); + let resp = client + .post("https://challenges.cloudflare.com/turnstile/v0/siteverify") + .json(&serde_json::json!({ + "secret": secret, + "response": token + })) + .send() + .await; + let verified = match resp { + Ok(r) => r + .json::() + .await + .map(|v| v["success"].as_bool() == Some(true)) + .unwrap_or(false), + Err(_) => false, + }; + if !verified { + return Redirect::new(format!("/?lang={}", lang.code())).into_response(); + } + } + let mut lead = Lead { id: Auto::auto(), name: form.name, diff --git a/templates/admin/settings.html b/templates/admin/settings.html index d66585b..ff50f23 100644 --- a/templates/admin/settings.html +++ b/templates/admin/settings.html @@ -55,6 +55,19 @@

Каждая фраза между запятыми — отдельное ключевое слово

+
+ +
+ +
+
+
+ +
+ +
+
+ diff --git a/templates/landing.html b/templates/landing.html index fe9f251..aea27b8 100644 --- a/templates/landing.html +++ b/templates/landing.html @@ -50,6 +50,10 @@ } + {% if !turnstile_site_key.is_empty() %} + + {% endif %} +