//! OTP storage and rate-limiting. //! //! Keys //! ──── //! `otp:code:{6-digit-code}` → user_id string, TTL 15 min //! `otp:resend:{user_id}` → resend attempt counter, TTL 1 hour (max 3) use redis::AsyncCommands; use crate::RedisPool; const OTP_TTL_SECS: u64 = 900; // 15 minutes const RESEND_WINDOW_SECS: i64 = 3_600; // 1 hour const RESEND_MAX: i64 = 3; // ── Store / verify ──────────────────────────────────────────────────────────── /// Store OTP code keyed by the code itself → user_id. TTL 15 min. pub async fn set(redis: &mut RedisPool, code: &str, user_id: &str) -> Result<(), redis::RedisError> { let key = format!("otp:code:{code}"); redis.set_ex(key, user_id, OTP_TTL_SECS).await } /// Atomically fetch the user_id for this OTP and delete it (single-use). /// Returns `None` if the code doesn't exist or has expired. pub async fn consume(redis: &mut RedisPool, code: &str) -> Result, redis::RedisError> { let key = format!("otp:code:{code}"); // GETDEL: atomic get + delete (Redis ≥ 6.2) redis.get_del(key).await } // ── Resend rate limit ───────────────────────────────────────────────────────── /// Returns `true` if the user is allowed to request another OTP (< 3 in last hour). pub async fn resend_allowed(redis: &mut RedisPool, user_id: &str) -> Result { let key = format!("otp:resend:{user_id}"); let count: i64 = redis.get(&key).await.unwrap_or(0); Ok(count < RESEND_MAX) } /// Increment the resend counter. Call after sending the OTP. pub async fn record_resend(redis: &mut RedisPool, user_id: &str) -> Result<(), redis::RedisError> { let key = format!("otp:resend:{user_id}"); let count: i64 = redis.incr(&key, 1i64).await?; // Only set expiry on first increment so window is fixed from first request if count == 1 { redis.expire::<_, ()>(&key, RESEND_WINDOW_SECS).await?; } Ok(()) }