implement MongoDB user repository with async support

This commit is contained in:
2025-08-17 16:36:07 -04:00
parent f004dcf0c9
commit 26832acc31
4 changed files with 308 additions and 35 deletions

View File

@@ -19,7 +19,7 @@ impl RegisterPayload {
} }
pub async fn register(Json(_payload): Json<RegisterPayload>) -> impl IntoResponse { pub async fn register(Json(_payload): Json<RegisterPayload>) -> impl IntoResponse {
// TODO: Implement register logic // TODO: Implement user registration logic
( (
StatusCode::OK, StatusCode::OK,

View File

@@ -1,49 +1,98 @@
// models/user.rs
use bson::oid::ObjectId; use bson::oid::ObjectId;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct User { pub struct User {
#[serde(rename = "_id")] #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
id: ObjectId, pub id: Option<ObjectId>,
username: String, pub username: String,
email: String, pub email: String,
first_name: String, pub first_name: String,
last_name: String, pub last_name: String,
age: u32, pub age: u32,
is_active: bool, pub is_active: bool,
phone_number: String, pub phone_number: String,
password: String, pub password: String,
salt: String, pub salt: String,
created_at: DateTime<Utc>, #[serde(default = "chrono::Utc::now")]
updated_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
last_login: DateTime<Utc>, #[serde(default = "chrono::Utc::now")]
role: String, pub updated_at: DateTime<Utc>,
profile: Option<Profile>, pub last_login: Option<DateTime<Utc>>,
preferences: Option<Preferences>, pub role: String,
stats: Option<Stats>, pub profile: Option<Profile>,
pub preferences: Option<Preferences>,
pub stats: Option<Stats>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Profile { pub struct Profile {
avatar_url: String, pub avatar_url: String,
bio: String, pub bio: String,
location: String, pub location: String,
website: String, pub website: String,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Preferences { pub struct Preferences {
theme: String, pub theme: String,
language: String, pub language: String,
notifications_enabled: bool, pub notifications_enabled: bool,
email_verified: bool, pub email_verified: bool,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Stats { pub struct Stats {
total_posts: u32, pub total_posts: u32,
total_comments: u32, pub total_comments: u32,
total_likes: u32, pub total_likes: u32,
account_age_days: 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();
}
} }

View File

@@ -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<User>,
}
impl MongoUserRepository {
pub fn new(collection: Collection<User>) -> Self {
Self { collection }
}
}
#[async_trait]
impl UserRepository for MongoUserRepository {
async fn create(&self, mut user: User) -> Result<User, UserError> {
// 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<User, UserError> {
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<User, UserError> {
// 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<i64>, skip: Option<u64>) -> Result<Vec<User>, UserError> {
let find_options = FindOptions::builder()
.limit(limit)
.skip(skip)
.build();
let cursor = self.collection.find(None, find_options).await?;
let users: Vec<User> = cursor.try_collect().await?;
Ok(users)
}
async fn search(&self, query: String) -> Result<Vec<User>, 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<User> = cursor.try_collect().await?;
Ok(users)
}
async fn count(&self) -> Result<u64, UserError> {
let count = self.collection.count_documents(None, None).await?;
Ok(count)
}
async fn count_by_name(&self, name: String) -> Result<u64, UserError> {
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<u64, UserError> {
let count = self.collection.count_documents(doc! {"email": email}, None).await?;
Ok(count)
}
async fn count_by_phone(&self, phone: String) -> Result<u64, UserError> {
let count = self.collection.count_documents(doc! {"phone_number": phone}, None).await?;
Ok(count)
}
async fn count_by_id(&self, id: ObjectId) -> Result<u64, UserError> {
let count = self.collection.count_documents(doc! {"_id": id}, None).await?;
Ok(count)
}
async fn find_by_email(&self, email: String) -> Result<Option<User>, UserError> {
let user = self.collection.find_one(doc! {"email": email}, None).await?;
Ok(user)
}
async fn find_by_username(&self, username: String) -> Result<Option<User>, UserError> {
let user = self.collection.find_one(doc! {"username": username}, None).await?;
Ok(user)
}
async fn exists_by_email(&self, email: String) -> Result<bool, UserError> {
let count = self.count_by_email(email).await?;
Ok(count > 0)
}
async fn exists_by_username(&self, username: String) -> Result<bool, UserError> {
let count = self.collection.count_documents(doc! {"username": username}, None).await?;
Ok(count > 0)
}
async fn get_active_users(&self) -> Result<Vec<User>, UserError> {
let cursor = self.collection.find(doc! {"is_active": true}, None).await?;
let users: Vec<User> = cursor.try_collect().await?;
Ok(users)
}
async fn get_users_by_role(&self, role: String) -> Result<Vec<User>, UserError> {
let cursor = self.collection.find(doc! {"role": role}, None).await?;
let users: Vec<User> = cursor.try_collect().await?;
Ok(users)
}
}

View File

@@ -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<mongodb::error::Error> 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<User, UserError>;
async fn get(&self, id: ObjectId) -> Result<User, UserError>;
async fn update(&self, id: ObjectId, user: User) -> Result<User, UserError>;
async fn delete(&self, id: ObjectId) -> Result<(), UserError>;
async fn list(&self, limit: Option<i64>, skip: Option<u64>) -> Result<Vec<User>, UserError>;
async fn search(&self, query: String) -> Result<Vec<User>, UserError>;
async fn count(&self) -> Result<u64, UserError>;
async fn count_by_name(&self, name: String) -> Result<u64, UserError>;
async fn count_by_email(&self, email: String) -> Result<u64, UserError>;
async fn count_by_phone(&self, phone: String) -> Result<u64, UserError>;
async fn count_by_id(&self, id: ObjectId) -> Result<u64, UserError>;
async fn find_by_email(&self, email: String) -> Result<Option<User>, UserError>;
async fn find_by_username(&self, username: String) -> Result<Option<User>, UserError>;
async fn exists_by_email(&self, email: String) -> Result<bool, UserError>;
async fn exists_by_username(&self, username: String) -> Result<bool, UserError>;
async fn get_active_users(&self) -> Result<Vec<User>, UserError>;
async fn get_users_by_role(&self, role: String) -> Result<Vec<User>, UserError>;
}