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