Added claudflare Turnstile captcha support
This commit is contained in:
Generated
+1
-1
@@ -3353,7 +3353,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-petting"
|
name = "web-petting"
|
||||||
version = "0.1.9"
|
version = "0.1.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
|
|||||||
@@ -798,6 +798,8 @@ struct SettingsForm {
|
|||||||
timezone: String,
|
timezone: String,
|
||||||
site_domain: String,
|
site_domain: String,
|
||||||
seo_keywords: String,
|
seo_keywords: String,
|
||||||
|
turnstile_site_key: String,
|
||||||
|
turnstile_secret_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_settings(request: Request, session: Session, db: Database) -> cot::Result<Response> {
|
async fn save_settings(request: Request, session: Session, db: Database) -> cot::Result<Response> {
|
||||||
@@ -814,6 +816,8 @@ async fn save_settings(request: Request, session: Session, db: Database) -> cot:
|
|||||||
("timezone", form.timezone),
|
("timezone", form.timezone),
|
||||||
("site_domain", form.site_domain),
|
("site_domain", form.site_domain),
|
||||||
("seo_keywords", form.seo_keywords),
|
("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 k = key.to_string();
|
||||||
let existing = query!(Setting, $key == k).get(&db).await?;
|
let existing = query!(Setting, $key == k).get(&db).await?;
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ pub struct Translations {
|
|||||||
pub settings_timezone: &'static str,
|
pub settings_timezone: &'static str,
|
||||||
pub settings_site_domain: &'static str,
|
pub settings_site_domain: &'static str,
|
||||||
pub settings_seo_keywords: &'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_contact_label: &'static str,
|
||||||
pub landing_pricing_title: &'static str,
|
pub landing_pricing_title: &'static str,
|
||||||
|
|
||||||
@@ -348,6 +350,8 @@ static RU: Translations = Translations {
|
|||||||
settings_timezone: "Часовой пояс (например Asia/Vladivostok)",
|
settings_timezone: "Часовой пояс (например Asia/Vladivostok)",
|
||||||
settings_site_domain: "Домен сайта (например https://example.com)",
|
settings_site_domain: "Домен сайта (например https://example.com)",
|
||||||
settings_seo_keywords: "SEO-ключевые слова (через запятую, отображаются на сайте и в мета-теге keywords)",
|
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_contact_label: "Или свяжитесь с нами напрямую",
|
||||||
landing_pricing_title: "Стоимость",
|
landing_pricing_title: "Стоимость",
|
||||||
|
|
||||||
@@ -551,6 +555,8 @@ static EN: Translations = Translations {
|
|||||||
settings_timezone: "Timezone (e.g. Asia/Vladivostok)",
|
settings_timezone: "Timezone (e.g. Asia/Vladivostok)",
|
||||||
settings_site_domain: "Site domain (e.g. https://example.com)",
|
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_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_contact_label: "Or contact us directly",
|
||||||
landing_pricing_title: "Pricing",
|
landing_pricing_title: "Pricing",
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ struct LandingTemplate<'a> {
|
|||||||
testimonials: Vec<Testimonial>,
|
testimonials: Vec<Testimonial>,
|
||||||
site_domain: String,
|
site_domain: String,
|
||||||
review_count: usize,
|
review_count: usize,
|
||||||
|
turnstile_site_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Template)]
|
#[derive(Debug, Template)]
|
||||||
@@ -138,6 +139,12 @@ async fn landing_page(request: Request, db: Database) -> cot::Result<Response> {
|
|||||||
.await?
|
.await?
|
||||||
.map(|s| s.value)
|
.map(|s| s.value)
|
||||||
.unwrap_or_default();
|
.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?;
|
let mut testimonials = Testimonial::objects().all(&db).await?;
|
||||||
testimonials.retain(|t| t.status == "active");
|
testimonials.retain(|t| t.status == "active");
|
||||||
testimonials.sort_by(|a, b| a.sort_order.cmp(&b.sort_order));
|
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<Response> {
|
|||||||
testimonials,
|
testimonials,
|
||||||
site_domain,
|
site_domain,
|
||||||
review_count,
|
review_count,
|
||||||
|
turnstile_site_key,
|
||||||
}
|
}
|
||||||
.render()?;
|
.render()?;
|
||||||
html_response(body, lang)
|
html_response(body, lang)
|
||||||
@@ -161,6 +169,8 @@ struct LeadForm {
|
|||||||
name: String,
|
name: String,
|
||||||
phone: Option<String>,
|
phone: Option<String>,
|
||||||
comment: Option<String>,
|
comment: Option<String>,
|
||||||
|
#[serde(default, rename = "cf-turnstile-response")]
|
||||||
|
cf_turnstile_response: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn submit_lead(request: Request, db: Database) -> cot::Result<Response> {
|
async fn submit_lead(request: Request, db: Database) -> cot::Result<Response> {
|
||||||
@@ -170,6 +180,37 @@ async fn submit_lead(request: Request, db: Database) -> cot::Result<Response> {
|
|||||||
let form: LeadForm =
|
let form: LeadForm =
|
||||||
serde_html_form::from_bytes(&bytes).map_err(|e| cot::Error::internal(e.to_string()))?;
|
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::<serde_json::Value>()
|
||||||
|
.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 {
|
let mut lead = Lead {
|
||||||
id: Auto::auto(),
|
id: Auto::auto(),
|
||||||
name: form.name,
|
name: form.name,
|
||||||
|
|||||||
@@ -55,6 +55,19 @@
|
|||||||
<p style="font-size:0.78rem;color:#aaa;margin-top:0.3rem;">Каждая фраза между запятыми — отдельное ключевое слово</p>
|
<p style="font-size:0.78rem;color:#aaa;margin-top:0.3rem;">Каждая фраза между запятыми — отдельное ключевое слово</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ t.settings_turnstile_site_key }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" name="turnstile_site_key" value="{% for s in &settings %}{% if s.key == "turnstile_site_key" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ t.settings_turnstile_secret_key }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" name="turnstile_secret_key" value="{% for s in &settings %}{% if s.key == "turnstile_secret_key" %}{{ s.value }}{% endif %}{% endfor %}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="button is-primary">{{ t.settings_save }}</button>
|
<button type="submit" class="button is-primary">{{ t.settings_save }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,6 +50,10 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{% if !turnstile_site_key.is_empty() %}
|
||||||
|
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* ── Reset & Base ── */
|
/* ── Reset & Base ── */
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
@@ -464,6 +468,9 @@
|
|||||||
<input type="checkbox" id="consent" name="consent" required style="margin-top:0.2rem;width:auto;flex-shrink:0;">
|
<input type="checkbox" id="consent" name="consent" required style="margin-top:0.2rem;width:auto;flex-shrink:0;">
|
||||||
<label for="consent" style="font-size:0.82rem;font-weight:400;color:#7a7599;cursor:pointer;display:inline;">{{ t.landing_form_consent }}</label>
|
<label for="consent" style="font-size:0.82rem;font-weight:400;color:#7a7599;cursor:pointer;display:inline;">{{ t.landing_form_consent }}</label>
|
||||||
</div>
|
</div>
|
||||||
|
{% if !turnstile_site_key.is_empty() %}
|
||||||
|
<div class="cf-turnstile" data-sitekey="{{ turnstile_site_key }}" data-theme="light" style="margin-bottom:1.25rem;"></div>
|
||||||
|
{% endif %}
|
||||||
<button type="submit" class="form-submit">{{ t.landing_form_submit }}</button>
|
<button type="submit" class="form-submit">{{ t.landing_form_submit }}</button>
|
||||||
</form>
|
</form>
|
||||||
{% if !contact_info.is_empty() %}
|
{% if !contact_info.is_empty() %}
|
||||||
|
|||||||
Reference in New Issue
Block a user