//! Giriş / kayıt deneme kilidi (IP başına, kullanıcı kararı 2026-11-14). //! //! Üst üste `max` hatalı deneme (yanlış şifre, geçersiz davet kodu, bot doğrulaması…) → //! o IP için giriş **ve** kayıt `lock` süresince kapanır. Başarılı girişte sayaç sıfırlanır. //! IP'ye göre olması bilinçli: e-postaya göre kilit, adresi bilen birinin hane sahibini //! dışarıda bırakmasına izin verirdi. Bellek içi (tek sunucu); yeniden başlatınca sıfırlanır. use std::{ collections::HashMap, sync::Mutex, time::{Duration, Instant}, }; pub const MAX_FAILURES: u32 = 3; pub const LOCK_FOR: Duration = Duration::from_secs(15 * 60); /// Kilitsiz ve bu kadar süredir dokunulmamış kayıtlar silinir (bellek büyümesin) const FORGET_AFTER: Duration = Duration::from_secs(60 * 60); #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Outcome { /// Kilide kalan deneme Remaining(u32), /// Kilitlendi; kalan süre (sn) Locked(u64), } struct Entry { failures: u32, locked_until: Option, last: Instant, } pub struct AttemptGuard { map: Mutex>, max: u32, lock: Duration, } impl Default for AttemptGuard { fn default() -> Self { Self::new(MAX_FAILURES, LOCK_FOR) } } impl AttemptGuard { pub fn new(max: u32, lock: Duration) -> Self { Self { map: Mutex::new(HashMap::new()), max, lock, } } /// Kilitliyse kalan süre (sn, en az 1) pub fn locked_for(&self, key: &str) -> Option { let map = self.map.lock().unwrap_or_else(|e| e.into_inner()); map.get(key) .and_then(|e| e.locked_until) .and_then(|until| until.checked_duration_since(Instant::now())) .map(|left| left.as_secs().max(1)) } pub fn failure(&self, key: &str) -> Outcome { let now = Instant::now(); let mut map = self.map.lock().unwrap_or_else(|e| e.into_inner()); map.retain(|_, e| { e.locked_until.is_some_and(|u| u > now) || now.duration_since(e.last) < FORGET_AFTER }); let e = map.entry(key.to_string()).or_insert(Entry { failures: 0, locked_until: None, last: now, }); // Süresi biten kilit: sayaç baştan if e.locked_until.is_some_and(|u| u <= now) { e.locked_until = None; e.failures = 0; } e.failures += 1; e.last = now; if e.failures >= self.max { e.locked_until = Some(now + self.lock); e.failures = 0; Outcome::Locked(self.lock.as_secs()) } else { Outcome::Remaining(self.max - e.failures) } } pub fn success(&self, key: &str) { self.map .lock() .unwrap_or_else(|e| e.into_inner()) .remove(key); } } #[cfg(test)] mod tests { use super::*; #[test] fn three_consecutive_failures_lock() { let g = AttemptGuard::default(); assert_eq!(g.failure("1.2.3.4"), Outcome::Remaining(2)); assert_eq!(g.failure("1.2.3.4"), Outcome::Remaining(1)); assert!(g.locked_for("1.2.3.4").is_none()); assert_eq!(g.failure("1.2.3.4"), Outcome::Locked(900)); assert!(g.locked_for("1.2.3.4").is_some_and(|s| s > 890)); assert!(g.locked_for("5.6.7.8").is_none(), "başka IP etkilenmez"); } #[test] fn success_resets_and_lock_expires() { let g = AttemptGuard::new(3, Duration::from_millis(50)); g.failure("ip"); g.failure("ip"); g.success("ip"); assert_eq!( g.failure("ip"), Outcome::Remaining(2), "başarı sayacı sıfırlar" ); g.failure("ip"); assert!(matches!(g.failure("ip"), Outcome::Locked(_))); std::thread::sleep(Duration::from_millis(60)); assert!(g.locked_for("ip").is_none(), "süre dolunca açılır"); assert_eq!(g.failure("ip"), Outcome::Remaining(2), "yeni tur"); } }