implement MongoDB user repository with async support
This commit is contained in:
@@ -19,7 +19,7 @@ impl RegisterPayload {
|
||||
}
|
||||
|
||||
pub async fn register(Json(_payload): Json<RegisterPayload>) -> impl IntoResponse {
|
||||
// TODO: Implement register logic
|
||||
// TODO: Implement user registration logic
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
|
||||
@@ -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<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
last_login: DateTime<Utc>,
|
||||
role: String,
|
||||
profile: Option<Profile>,
|
||||
preferences: Option<Preferences>,
|
||||
stats: Option<Stats>,
|
||||
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<ObjectId>,
|
||||
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<Utc>,
|
||||
#[serde(default = "chrono::Utc::now")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub last_login: Option<DateTime<Utc>>,
|
||||
pub role: String,
|
||||
pub profile: Option<Profile>,
|
||||
pub preferences: Option<Preferences>,
|
||||
pub stats: Option<Stats>,
|
||||
}
|
||||
|
||||
#[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();
|
||||
}
|
||||
}
|
||||
|
||||
171
src/mongodb/repositories/mongodb_user_repository.rs
Normal file
171
src/mongodb/repositories/mongodb_user_repository.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
53
src/mongodb/repositories/user_repository.rs
Normal file
53
src/mongodb/repositories/user_repository.rs
Normal 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>;
|
||||
}
|
||||
Reference in New Issue
Block a user