From 26832acc313b62ee8862136dba072ee72a304e7e Mon Sep 17 00:00:00 2001 From: django Date: Sun, 17 Aug 2025 16:36:07 -0400 Subject: [PATCH] implement MongoDB user repository with async support --- src/handlers/user/register.rs | 2 +- src/mongodb/models/user.rs | 117 ++++++++---- .../repositories/mongodb_user_repository.rs | 171 ++++++++++++++++++ src/mongodb/repositories/user_repository.rs | 53 ++++++ 4 files changed, 308 insertions(+), 35 deletions(-) create mode 100644 src/mongodb/repositories/mongodb_user_repository.rs create mode 100644 src/mongodb/repositories/user_repository.rs diff --git a/src/handlers/user/register.rs b/src/handlers/user/register.rs index 79231ec..14916c3 100644 --- a/src/handlers/user/register.rs +++ b/src/handlers/user/register.rs @@ -19,7 +19,7 @@ impl RegisterPayload { } pub async fn register(Json(_payload): Json) -> impl IntoResponse { - // TODO: Implement register logic + // TODO: Implement user registration logic ( StatusCode::OK, diff --git a/src/mongodb/models/user.rs b/src/mongodb/models/user.rs index d3c515e..0a2f840 100644 --- a/src/mongodb/models/user.rs +++ b/src/mongodb/models/user.rs @@ -1,49 +1,98 @@ +// models/user.rs use bson::oid::ObjectId; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct User { - #[serde(rename = "_id")] - id: ObjectId, - username: String, - email: String, - first_name: String, - last_name: String, - age: u32, - is_active: bool, - phone_number: String, - password: String, - salt: String, - created_at: DateTime, - updated_at: DateTime, - last_login: DateTime, - role: String, - profile: Option, - preferences: Option, - stats: Option, + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + pub username: String, + pub email: String, + pub first_name: String, + pub last_name: String, + pub age: u32, + pub is_active: bool, + pub phone_number: String, + pub password: String, + pub salt: String, + #[serde(default = "chrono::Utc::now")] + pub created_at: DateTime, + #[serde(default = "chrono::Utc::now")] + pub updated_at: DateTime, + pub last_login: Option>, + pub role: String, + pub profile: Option, + pub preferences: Option, + pub stats: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Profile { - avatar_url: String, - bio: String, - location: String, - website: String, + pub avatar_url: String, + pub bio: String, + pub location: String, + pub website: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Preferences { - theme: String, - language: String, - notifications_enabled: bool, - email_verified: bool, + pub theme: String, + pub language: String, + pub notifications_enabled: bool, + pub email_verified: bool, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Stats { - total_posts: u32, - total_comments: u32, - total_likes: u32, - account_age_days: u32, + pub total_posts: u32, + pub total_comments: u32, + pub total_likes: u32, + pub account_age_days: u32, +} + +impl User { + pub fn new(username: String, email: String, password: String, salt: String) -> Self { + let now = chrono::Utc::now(); + + Self { + id: None, + username, + email, + first_name: String::new(), + last_name: String::new(), + age: 0, + is_active: true, + phone_number: String::new(), + password, + salt, + created_at: now, + updated_at: now, + last_login: None, + role: "user".to_string(), + profile: None, + preferences: None, + stats: None, + } + } + + pub fn with_profile(mut self, profile: Profile) -> Self { + self.profile = Some(profile); + self + } + + pub fn with_preferences(mut self, preferences: Preferences) -> Self { + self.preferences = Some(preferences); + self + } + + pub fn with_stats(mut self, stats: Stats) -> Self { + self.stats = Some(stats); + self + } + + pub fn update_last_login(&mut self) { + self.last_login = Some(chrono::Utc::now()); + self.updated_at = chrono::Utc::now(); + } } diff --git a/src/mongodb/repositories/mongodb_user_repository.rs b/src/mongodb/repositories/mongodb_user_repository.rs new file mode 100644 index 0000000..80681da --- /dev/null +++ b/src/mongodb/repositories/mongodb_user_repository.rs @@ -0,0 +1,171 @@ +use async_trait::async_trait; +use futures::TryStreamExt; +use mongodb::bson::doc; +use mongodb::options::FindOptions; +use mongodb::Collection; +use bson::oid::ObjectId; + +use crate::models::user::User; +use super::user_repository::{UserRepository, UserError}; + +pub struct MongoUserRepository { + collection: Collection, +} + +impl MongoUserRepository { + pub fn new(collection: Collection) -> Self { + Self { collection } + } +} + +#[async_trait] +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())); + } + if user.email.is_empty() { + return Err(UserError::ValidationError("Email is required".to_string())); + } + + // Check for existing users + if self.exists_by_username(user.username.clone()).await? { + return Err(UserError::DuplicateKey("username".to_string())); + } + if self.exists_by_email(user.email.clone()).await? { + return Err(UserError::DuplicateKey("email".to_string())); + } + + // Set timestamps + let now = chrono::Utc::now(); + 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) + } + + async fn get(&self, id: ObjectId) -> Result { + let user = self.collection.find_one(doc! {"_id": id}, None).await?; + user.ok_or(UserError::NotFound) + } + + async fn update(&self, id: ObjectId, mut user: User) -> Result { + // 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?; + + 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 cursor = self.collection.find(None, find_options).await?; + let users: Vec = cursor.try_collect().await?; + Ok(users) + } + + async fn search(&self, query: String) -> Result, UserError> { + // Use regex for partial matching or text search + let filter = doc! { + "$or": [ + {"username": {"$regex": &query, "$options": "i"}}, + {"email": {"$regex": &query, "$options": "i"}}, + {"first_name": {"$regex": &query, "$options": "i"}}, + {"last_name": {"$regex": &query, "$options": "i"}} + ] + }; + + let cursor = self.collection.find(filter, None).await?; + let users: Vec = cursor.try_collect().await?; + Ok(users) + } + + async fn count(&self) -> Result { + let count = self.collection.count_documents(None, None).await?; + Ok(count) + } + + async fn count_by_name(&self, name: String) -> Result { + let filter = doc! { + "$or": [ + {"first_name": &name}, + {"last_name": &name} + ] + }; + let count = self.collection.count_documents(filter, None).await?; + Ok(count) + } + + async fn count_by_email(&self, email: String) -> Result { + 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?; + Ok(count) + } + + async fn count_by_id(&self, id: ObjectId) -> Result { + 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?; + Ok(user) + } + + async fn find_by_username(&self, username: String) -> Result, UserError> { + let user = self.collection.find_one(doc! {"username": username}, None).await?; + Ok(user) + } + + async fn exists_by_email(&self, email: String) -> Result { + let count = self.count_by_email(email).await?; + Ok(count > 0) + } + + async fn exists_by_username(&self, username: String) -> Result { + let count = self.collection.count_documents(doc! {"username": username}, None).await?; + Ok(count > 0) + } + + async fn get_active_users(&self) -> Result, UserError> { + let cursor = self.collection.find(doc! {"is_active": true}, None).await?; + let users: Vec = cursor.try_collect().await?; + Ok(users) + } + + async fn get_users_by_role(&self, role: String) -> Result, UserError> { + let cursor = self.collection.find(doc! {"role": role}, None).await?; + let users: Vec = cursor.try_collect().await?; + Ok(users) + } +} \ No newline at end of file diff --git a/src/mongodb/repositories/user_repository.rs b/src/mongodb/repositories/user_repository.rs new file mode 100644 index 0000000..8b57116 --- /dev/null +++ b/src/mongodb/repositories/user_repository.rs @@ -0,0 +1,53 @@ +use async_trait::async_trait; +use bson::oid::ObjectId; +use crate::models::user::User; + +// Define custom error type +#[derive(Debug)] +pub enum UserError { + MongoError(mongodb::error::Error), + NotFound, + ValidationError(String), + DuplicateKey(String), +} + +impl std::fmt::Display for UserError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UserError::MongoError(e) => write!(f, "MongoDB error: {}", e), + UserError::NotFound => write!(f, "User not found"), + UserError::ValidationError(msg) => write!(f, "Validation error: {}", msg), + UserError::DuplicateKey(field) => write!(f, "Duplicate key error: {}", field), + } + } +} + +impl std::error::Error for UserError {} + +impl From for UserError { + fn from(error: mongodb::error::Error) -> Self { + UserError::MongoError(error) + } +} + +// Repository trait +#[async_trait] +pub trait UserRepository { + async fn create(&self, user: User) -> Result; + async fn get(&self, id: ObjectId) -> Result; + async fn update(&self, id: ObjectId, user: User) -> Result; + async fn delete(&self, id: ObjectId) -> Result<(), UserError>; + async fn list(&self, limit: Option, skip: Option) -> Result, UserError>; + async fn search(&self, query: String) -> Result, UserError>; + async fn count(&self) -> Result; + async fn count_by_name(&self, name: String) -> Result; + async fn count_by_email(&self, email: String) -> Result; + async fn count_by_phone(&self, phone: String) -> Result; + async fn count_by_id(&self, id: ObjectId) -> Result; + async fn find_by_email(&self, email: String) -> Result, UserError>; + async fn find_by_username(&self, username: String) -> Result, UserError>; + async fn exists_by_email(&self, email: String) -> Result; + async fn exists_by_username(&self, username: String) -> Result; + async fn get_active_users(&self) -> Result, UserError>; + async fn get_users_by_role(&self, role: String) -> Result, UserError>; +}