From fa362505b4f22fbe855131ad0ee557907d0eaa1d Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Sun, 11 Oct 2026 02:08:19 +0300 Subject: [PATCH] =?UTF-8?q?feat(geri-bildirim):=20Cloudflare=20Turnstile?= =?UTF-8?q?=20bot=20korumas=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Form token üretir, sunucu aksiyonu Cloudflare'e doğrulatır. Betik metin alanına ilk odaklanışta yüklenir; onay kutusu yalnız Cloudflare elle onay istediğinde görünür. TURNSTILE_SITE_KEY / TURNSTILE_SECRET_KEY çağrı anında okunur; biri eksikse koruma kapalıdır. Gizlilik ve çerez metinleri sağlayıcıyı anacak şekilde güncellendi. Co-Authored-By: Claude Opus 5.5 --- src/app/cerez-politikasi/page.tsx | 8 +- src/app/gizlilik/page.tsx | 15 ++- .../components/geri-bildirim-formu.tsx | 100 +++++++++++++++++- .../geri-bildirim/geri-bildirim-actions.ts | 23 ++++ src/lib/turnstile.ts | 62 +++++++++++ 5 files changed, 198 insertions(+), 10 deletions(-) create mode 100644 src/lib/turnstile.ts diff --git a/src/app/cerez-politikasi/page.tsx b/src/app/cerez-politikasi/page.tsx index 85b40f6..128614f 100644 --- a/src/app/cerez-politikasi/page.tsx +++ b/src/app/cerez-politikasi/page.tsx @@ -32,7 +32,7 @@ export default function CerezPolitikasiPage() {

Çerez Politikası

- Son güncelleme: 5 Ekim 2026 + Son güncelleme: 11 Ekim 2026

@@ -117,7 +117,11 @@ export default function CerezPolitikasiPage() { Paket satın alırken kart formunu iyzico gösterir ve ödeme güvenliği için kendi çerezlerini kullanabilir. Google ile giriş yaparsan Google’ın sayfasına yönlendirilirsin; orada - Google’ın çerez politikası geçerlidir. Bunların dışında + Google’ın çerez politikası geçerlidir. Geri bildirim + kutusuna yazmaya başladığında otomatik gönderimleri ayırt etmek + için Cloudflare Turnstile’ın doğrulama betiği yüklenir; bu + betik seni siteler arasında izlemez ve reklam amacıyla + kullanılmaz. Bunların dışında sitede başka şirketlere ait çerez, reklam ya da sosyal medya eklentisi yoktur.

diff --git a/src/app/gizlilik/page.tsx b/src/app/gizlilik/page.tsx index 52ab90a..a851af6 100644 --- a/src/app/gizlilik/page.tsx +++ b/src/app/gizlilik/page.tsx @@ -30,7 +30,7 @@ export default function GizlilikPage() { Gizlilik ve Kişisel Verilerin Korunması

- Son güncelleme: 5 Ekim 2026 + Son güncelleme: 11 Ekim 2026

@@ -179,10 +179,19 @@ export default function GizlilikPage() { programlar ve kaydettiğin tercihler. Senin yazdığın mesaj, adın ve e-postan gönderilmez. +
  • + Cloudflare Turnstile (ABD) — geri bildirim + kutusunu otomatik gönderimlerden korur. Kutuya yazmaya + başladığında Cloudflare’in doğrulama betiği yüklenir; IP + adresin ile tarayıcı ve cihaz bilgilerin, karşıdakinin insan + olup olmadığının anlaşılması için Cloudflare’e iletilir. + Yazdığın mesaj Cloudflare’e gönderilmez. +
  • - KVKK açısından e-posta sağlayıcısına, yapay zekâ sağlayıcısına ve - cevap denetimi sağlayıcısına yapılan iletim yurt dışına + KVKK açısından e-posta sağlayıcısına, yapay zekâ sağlayıcısına, + cevap denetimi sağlayıcısına ve bot koruması sağlayıcısına + yapılan iletim yurt dışına aktarımdır ve hizmetin sunulabilmesi için gereklidir. Sağlayıcı değişirse bu bölüm aynı gün güncellenir.

    diff --git a/src/features/geri-bildirim/components/geri-bildirim-formu.tsx b/src/features/geri-bildirim/components/geri-bildirim-formu.tsx index af72c78..9715952 100644 --- a/src/features/geri-bildirim/components/geri-bildirim-formu.tsx +++ b/src/features/geri-bildirim/components/geri-bildirim-formu.tsx @@ -2,28 +2,112 @@ // Sürüm notları sayfasının altındaki geri bildirim kutusu: başlık, metin // alanı, buton. Mesaj geriBildirimGonder sunucu aksiyonuyla destek kutusuna -// e-posta olarak gider. +// e-posta olarak gider. Bot koruması Cloudflare Turnstile: betik ancak metin +// alanına ilk odaklanıldığında yüklenir, kutusu yalnız Cloudflare elle onay +// istediğinde görünür (çoğu ziyaretçi hiç görmez). -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { ArrowRight } from "lucide-react"; import { toast } from "@/components/ui/bildirim"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { TypingAnimation } from "@/components/ui/typing-animation"; -import { geriBildirimGonder } from "@/features/geri-bildirim/geri-bildirim-actions"; +import { + geriBildirimGonder, + geriBildirimKorumasi, +} from "@/features/geri-bildirim/geri-bildirim-actions"; const BASLIK = "Bir hata mı gördün?"; const DEVAM = "Yalın üretim metodunu benimsiyor ve fikirlerine önem veriyoruz."; +const TURNSTILE_BETIGI = + "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"; + +type Turnstile = { + render: (kap: HTMLElement, ayar: Record) => string; + reset: (kimlik: string) => void; + remove: (kimlik: string) => void; +}; + +declare global { + interface Window { + turnstile?: Turnstile; + } +} + +let betikSozu: Promise | null = null; + +function turnstileBetiginiYukle(): Promise { + betikSozu ??= new Promise((coz, reddet) => { + const betik = document.createElement("script"); + betik.src = TURNSTILE_BETIGI; + betik.async = true; + betik.onload = () => coz(); + betik.onerror = () => { + betikSozu = null; + reddet(new Error("Turnstile betiği yüklenemedi")); + }; + document.head.appendChild(betik); + }); + return betikSozu; +} + export function GeriBildirimFormu({ className = "" }: { className?: string }) { const [mesaj, setMesaj] = useState(""); const [pending, setPending] = useState(false); + // Koruma açık ve token henüz gelmediyse buton bekler. Betik yüklenemezse + // (engelleyici, ağ) korumaAcik false kalır; son sözü sunucu söyler. + const [korumaAcik, setKorumaAcik] = useState(false); + const [token, setToken] = useState(null); + const [onayGorunur, setOnayGorunur] = useState(false); + const korumaKabi = useRef(null); + const widget = useRef(null); + const korumaBasladi = useRef(false); + + async function korumayiBaslat() { + if (korumaBasladi.current) return; + korumaBasladi.current = true; + try { + const { siteAnahtari } = await geriBildirimKorumasi(); + if (!siteAnahtari) return; + await turnstileBetiginiYukle(); + if (!korumaKabi.current || !window.turnstile) return; + widget.current = window.turnstile.render(korumaKabi.current, { + sitekey: siteAnahtari, + appearance: "interaction-only", + size: "flexible", + language: "tr", + theme: document.documentElement.classList.contains("dark") + ? "dark" + : "light", + callback: (yeni: string) => setToken(yeni), + "before-interactive-callback": () => setOnayGorunur(true), + "after-interactive-callback": () => setOnayGorunur(false), + "expired-callback": () => setToken(null), + "error-callback": () => setToken(null), + }); + setKorumaAcik(true); + } catch { + korumaBasladi.current = false; + } + } + + useEffect(() => { + return () => { + if (widget.current) window.turnstile?.remove(widget.current); + }; + }, []); async function gonder(e: React.FormEvent) { e.preventDefault(); setPending(true); try { - const sonuc = await geriBildirimGonder(mesaj); + const sonuc = await geriBildirimGonder(mesaj, token); + // Token tek kullanımlık: sonuç ne olursa olsun yenisi istenir. + if (widget.current) { + setToken(null); + window.turnstile?.reset(widget.current); + } if (sonuc.ok) { toast.success("Geri bildirimin ulaştı, teşekkürler."); setMesaj(""); @@ -73,17 +157,23 @@ export function GeriBildirimFormu({ className = "" }: { className?: string }) { aria-labelledby="geri-bildirim-baslik" value={mesaj} onChange={(e) => setMesaj(e.target.value)} + onFocus={korumayiBaslat} maxLength={2000} placeholder="Çekinme, aklına geleni yaz…" disabled={pending} className="min-h-28 resize-none border-0 bg-transparent px-3 py-2 focus-visible:ring-0 disabled:bg-transparent" /> +