//! Generic sliding-window rate limiter. //! //! Key pattern: `rate:{namespace}:{identifier}` //! Returns `Ok(true)` if the request is allowed, `Ok(false)` if rate-limited. use redis::AsyncCommands; use crate::RedisPool; /// Check + increment a rate-limit counter. /// /// * `namespace` – e.g. `"login"`, `"register"`, `"lead"` /// * `identifier` – e.g. email, IP, user_id /// * `max` – maximum requests allowed in `window_secs` /// * `window_secs` – sliding window length in seconds /// /// Returns `Ok(true)` = allowed, `Ok(false)` = blocked. pub async fn check( redis: &mut RedisPool, namespace: &str, identifier: &str, max: i64, window_secs: i64, ) -> Result { let key = format!("rate:{namespace}:{identifier}"); let count: i64 = redis.incr(&key, 1i64).await?; if count == 1 { redis.expire::<_, ()>(&key, window_secs).await?; } Ok(count <= max) } /// Convenience wrappers ─────────────────────────────────────────────────────── /// Register: max 10 per hour per email pub async fn check_register(redis: &mut RedisPool, email: &str) -> Result { check(redis, "register", email, 10, 3_600).await } /// Login: max 10 attempts per 15 min per email pub async fn check_login(redis: &mut RedisPool, email: &str) -> Result { check(redis, "login", email, 10, 900).await } /// Lead request: max 5 per hour per professional pub async fn check_lead(redis: &mut RedisPool, professional_id: &str) -> Result { check(redis, "lead", professional_id, 5, 3_600).await } /// Job post: max 20 per hour per company pub async fn check_job_post(redis: &mut RedisPool, company_id: &str) -> Result { check(redis, "job_post", company_id, 20, 3_600).await }