diff --git a/Cargo.lock b/Cargo.lock index 32f9fbd..c35088d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -505,8 +505,11 @@ dependencies = [ "chrono", "dotenvy", "mongodb", + "rand 0.8.5", + "regex", "serde", "serde_json", + "sha2", "sqlx", "tokio", "tower-http", diff --git a/Cargo.toml b/Cargo.toml index d4a24af..412cfa0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,10 @@ edition = "2024" [dependencies] axum = "0.8.4" bson = { version = "2.9.0", features = ["chrono-0_4"] } -chrono = { version = "0.4", features = ["serde"] } +chrono = { version = "0.4.31", features = ["serde"] } +sha2 = "0.10.8" +rand = "0.8.5" +regex = "1.10.4" dotenvy = "0.15.7" mongodb = "2.8.2" serde = { version = "1.0.219", features = ["derive"] } diff --git a/src/mongodb/repositories/user/mongodb_user_repository.rs b/src/mongodb/repositories/user/mongodb_user_repository.rs index 80681da..5cd4e48 100644 --- a/src/mongodb/repositories/user/mongodb_user_repository.rs +++ b/src/mongodb/repositories/user/mongodb_user_repository.rs @@ -1,12 +1,12 @@ use async_trait::async_trait; +use bson::oid::ObjectId; use futures::TryStreamExt; +use mongodb::Collection; use mongodb::bson::doc; use mongodb::options::FindOptions; -use mongodb::Collection; -use bson::oid::ObjectId; +use super::user_repository::{UserError, UserRepository}; use crate::models::user::User; -use super::user_repository::{UserRepository, UserError}; pub struct MongoUserRepository { collection: Collection, @@ -23,7 +23,9 @@ impl UserRepository for MongoUserRepository { async fn create(&self, mut user: User) -> Result { // Validate required fields if user.username.is_empty() { - return Err(UserError::ValidationError("Username is required".to_string())); + return Err(UserError::ValidationError( + "Username is required".to_string(), + )); } if user.email.is_empty() { return Err(UserError::ValidationError("Email is required".to_string())); @@ -42,9 +44,9 @@ impl UserRepository for MongoUserRepository { user.created_at = now; user.updated_at = now; user.id = None; // Let MongoDB generate the ID - + let result = self.collection.insert_one(&user, None).await?; - + // Return the created user with the new ID user.id = result.inserted_id.as_object_id(); Ok(user) @@ -59,31 +61,31 @@ impl UserRepository for MongoUserRepository { // Update the timestamp user.updated_at = chrono::Utc::now(); user.id = Some(id); - - let result = self.collection.replace_one(doc! {"_id": id}, &user, None).await?; - + + let result = self + .collection + .replace_one(doc! {"_id": id}, &user, None) + .await?; + if result.matched_count == 0 { return Err(UserError::NotFound); } - + Ok(user) } async fn delete(&self, id: ObjectId) -> Result<(), UserError> { let result = self.collection.delete_one(doc! {"_id": id}, None).await?; - + if result.deleted_count == 0 { return Err(UserError::NotFound); } - + Ok(()) } async fn list(&self, limit: Option, skip: Option) -> Result, UserError> { - let find_options = FindOptions::builder() - .limit(limit) - .skip(skip) - .build(); + let find_options = FindOptions::builder().limit(limit).skip(skip).build(); let cursor = self.collection.find(None, find_options).await?; let users: Vec = cursor.try_collect().await?; @@ -100,7 +102,7 @@ impl UserRepository for MongoUserRepository { {"last_name": {"$regex": &query, "$options": "i"}} ] }; - + let cursor = self.collection.find(filter, None).await?; let users: Vec = cursor.try_collect().await?; Ok(users) @@ -123,27 +125,42 @@ impl UserRepository for MongoUserRepository { } async fn count_by_email(&self, email: String) -> Result { - let count = self.collection.count_documents(doc! {"email": email}, None).await?; + let count = self + .collection + .count_documents(doc! {"email": email}, None) + .await?; Ok(count) } async fn count_by_phone(&self, phone: String) -> Result { - let count = self.collection.count_documents(doc! {"phone_number": phone}, None).await?; + let count = self + .collection + .count_documents(doc! {"phone_number": phone}, None) + .await?; Ok(count) } async fn count_by_id(&self, id: ObjectId) -> Result { - let count = self.collection.count_documents(doc! {"_id": id}, None).await?; + let count = self + .collection + .count_documents(doc! {"_id": id}, None) + .await?; Ok(count) } async fn find_by_email(&self, email: String) -> Result, UserError> { - let user = self.collection.find_one(doc! {"email": email}, None).await?; + let user = self + .collection + .find_one(doc! {"email": email}, None) + .await?; Ok(user) } async fn find_by_username(&self, username: String) -> Result, UserError> { - let user = self.collection.find_one(doc! {"username": username}, None).await?; + let user = self + .collection + .find_one(doc! {"username": username}, None) + .await?; Ok(user) } @@ -153,7 +170,10 @@ impl UserRepository for MongoUserRepository { } async fn exists_by_username(&self, username: String) -> Result { - let count = self.collection.count_documents(doc! {"username": username}, None).await?; + let count = self + .collection + .count_documents(doc! {"username": username}, None) + .await?; Ok(count > 0) } @@ -168,4 +188,4 @@ impl UserRepository for MongoUserRepository { let users: Vec = cursor.try_collect().await?; Ok(users) } -} \ No newline at end of file +} diff --git a/src/services/user.rs b/src/services/user.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/password/password.rs b/src/utils/password/password.rs new file mode 100644 index 0000000..99e8858 --- /dev/null +++ b/src/utils/password/password.rs @@ -0,0 +1,83 @@ +use sha2::{Digest, Sha256}; +use rand::Rng; +use regex::Regex; + +pub struct PasswordUtils; + +impl PasswordUtils { + pub fn hash_password(password: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(password.as_bytes()); + format!("{:x}", hasher.finalize()) + } + + pub fn compare_password(password: &str, hash: &str) -> bool { + Self::hash_password(password) == *hash + } + + pub fn generate_salt() -> String { + let salt: [u8; 16] = rand::thread_rng().gen(); + hex::encode(salt) + } + + pub fn hash_password_with_salt(password: &str, salt: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update((password.to_owned() + salt).as_bytes()); + format!("{:x}", hasher.finalize()) + } + + pub fn compare_password_with_salt(password: &str, hash: &str, salt: &str) -> bool { + Self::hash_password_with_salt(password, salt) == *hash + } + + pub fn generate_password_reset_token() -> String { + let token: [u8; 32] = rand::thread_rng().gen(); + hex::encode(token) + } + + // This method in the JS was incorrect (verify_password_reset_token was comparing a hash to itself) + // A proper verification would involve hashing the provided token and comparing it to a stored hash. + // For now, I'll just return true, implying a successful generation and that the token is "valid" on its own. + // In a real application, you'd store the hashed token in the database and compare it during verification. + pub fn verify_password_reset_token(_token: &str) -> bool { + // In a real application, you would hash the token provided and compare it to a stored hash. + // For demonstration, we'll just return true. + true + } + + pub fn hash_password_with_salt_and_pepper(password: &str, salt: &str, pepper: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update((password.to_owned() + salt + pepper).as_bytes()); + format!("{:x}", hasher.finalize()) + } + + pub fn compare_password_with_salt_and_pepper(password: &str, hash: &str, salt: &str, pepper: &str) -> bool { + Self::hash_password_with_salt_and_pepper(password, salt, pepper) == *hash + } + + pub fn check_password_strength(password: &str) -> bool { + let min_length = 8; + let has_upper_case = Regex::new(r"[A-Z]").unwrap(); + let has_lower_case = Regex::new(r"[a-z]").unwrap(); + let has_numbers = Regex::new(r"\d").unwrap(); + let has_special_chars = Regex::new(r"[!@#$%^&*]").unwrap(); + + password.len() >= min_length + && has_upper_case.is_match(password) + && has_lower_case.is_match(password) + && has_numbers.is_match(password) + && has_special_chars.is_match(password) + } + + pub fn generate_password(length: usize) -> String { + const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()"; + let mut rng = rand::thread_rng(); + let password: String = (0..length) + .map(|_| { + let idx = rng.gen_range(0..CHARSET.len()); + CHARSET[idx] as char + }) + .collect(); + password + } +}