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