### /home/asoliver/desarrollo/normogen/backend/src/security/account_lockout.rs ```rust 1: ### /home/asoliver/desarrollo/normogen/backend/src/security/account_lockout.rs 2: ```rust 3: 1: use anyhow::Result; 4: 2: use mongodb::bson::{doc, DateTime}; 5: 3: use mongodb::Collection; 6: 4: use std::sync::Arc; 7: 5: use tokio::sync::RwLock; 8: 6: 9: 7: #[derive(Clone)] 10: 8: pub struct AccountLockout { 11: 9: user_collection: Arc>>, 12: 10: max_attempts: u32, 13: 11: base_duration_minutes: u32, 14: 12: max_duration_minutes: u32, 15: 13: } 16: 14: 17: 15: impl AccountLockout { 18: 16: pub fn new( 19: 17: user_collection: Collection, 20: 18: max_attempts: u32, 21: 19: base_duration_minutes: u32, 22: 20: max_duration_minutes: u32, 23: 21: ) -> Self { 24: 22: Self { 25: 23: user_collection: Arc::new(RwLock::new(user_collection)), 26: 24: max_attempts, 27: 25: base_duration_minutes, 28: 26: max_duration_minutes, 29: 27: } 30: 28: } 31: 29: 32: 30: pub async fn check_lockout(&self, email: &str) -> Result { 33: 31: let collection = self.user_collection.read().await; 34: 32: let user = collection.find_one(doc! { "email": email }, None).await?; 35: 33: 36: 34: if let Some(user_doc) = user { 37: 35: if let Some(locked_until_val) = user_doc.get("locked_until") { 38: 36: if let Some(dt) = locked_until_val.as_datetime() { 39: 37: let now = DateTime::now(); 40: 38: if dt.timestamp_millis() > now.timestamp_millis() { 41: 39: return Ok(true); // Account is locked 42: 40: } 43: 41: } 44: 42: } 45: 43: } 46: 44: 47: 45: Ok(false) // Account is not locked 48: 46: } 49: 47: 50: 48: pub async fn record_failed_attempt(&self, email: &str) -> Result { 51: 49: let collection = self.user_collection.write().await; 52: 50: 53: 51: // Get current failed attempts 54: 52: let user = collection.find_one(doc! { "email": email }, None).await?; 55: 53: 56: 54: let current_attempts = if let Some(user_doc) = user { 57: 55: user_doc 58: 56: .get("failed_login_attempts") 59: 57: .and_then(|v| v.as_i64()) 60: 58: .unwrap_or(0) as u32 61: 59: } else { 62: 60: 0 63: 61: }; 64: 62: 65: 63: let new_attempts = current_attempts + 1; 66: 64: let should_lock = new_attempts >= self.max_attempts; 67: 65: 68: 66: // Calculate lockout duration 69: 67: let lock_duration = if should_lock { 70: 68: let multiplier = new_attempts.saturating_sub(self.max_attempts).saturating_sub(self.max_attempts) + 1; 71: 69: let duration = self.base_duration_minutes * multiplier; 72: 70: std::cmp::min(duration, self.max_duration_minutes) 73: 71: } else { 74: 72: 0 75: 73: }; 76: 74: 77: 75: let locked_until = if lock_duration > 0 { 78: 76: let now = DateTime::now(); 79: 77: let duration_millis = lock_duration as u64 * 60 * 1000; 80: 78: DateTime::from_millis(now.timestamp_millis() + duration_millis as i64) 81: 79: } else { 82: 80: DateTime::now() 83: 81: }; 84: 82: 85: 83: // Update user 86: 84: collection 87: 85: .update_one( 88: 86: doc! { "email": email }, 89: 87: doc! { 90: 88: "$set": { 91: 89: "failed_login_attempts": new_attempts as i32, 92: 90: "last_failed_login": DateTime::now(), 93: 91: "locked_until": locked_until, 94: 92: } 95: 93: }, 96: 94: None, 97: 95: ) 98: 96: .await?; 99: 97: 100: 98: Ok(should_lock) 101: 99: } 102: 100: 103: 101: pub async fn reset_attempts(&self, email: &str) -> Result<()> { 104: 102: let collection = self.user_collection.write().await; 105: 103: 106: 104: collection 107: 105: .update_one( 108: 106: doc! { "email": email }, 109: 107: doc! { 110: 108: "$set": { 111: 109: "failed_login_attempts": 0, 112: 110: "locked_until": null, 113: 111: } 114: 112: }, 115: 113: None, 116: 114: ) 117: 115: .await?; 118: 116: 119: 117: Ok(()) 120: 118: } 121: 119: } 122: ``` ```