use bson::{doc, Document}; use mongodb::Collection; use serde::{Deserialize, Serialize}; use wither::{ bson::{oid::ObjectId}, IndexModel, IndexOptions, Model, }; use crate::auth::password::{PasswordService, verify_password}; #[derive(Debug, Clone, Serialize, Deserialize, Model)] #[model(collection_name="users")] pub struct User { #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] pub id: Option, #[index(unique = true)] pub email: String, pub username: String, pub password_hash: String, /// Password recovery phrase hash (zero-knowledge) #[serde(skip_serializing_if = "Option::is_none")] pub recovery_phrase_hash: Option, /// Whether password recovery is enabled for this user pub recovery_enabled: bool, /// Token version for invalidating all tokens on password change pub token_version: i32, /// When the user was created pub created_at: chrono::DateTime, /// Last time the user was active pub last_active: chrono::DateTime, /// Email verification status pub email_verified: bool, /// Email verification token (stub for now, will be used in Phase 2.4) #[serde(skip_serializing_if = "Option::is_none")] pub verification_token: Option, /// When the verification token expires #[serde(skip_serializing_if = "Option::is_none")] pub verification_expires: Option>, } impl User { /// Create a new user with password and optional recovery phrase pub fn new( email: String, username: String, password: String, recovery_phrase: Option, ) -> Result { // Hash the password let password_hash = PasswordService::hash_password(&password)?; // Hash the recovery phrase if provided let recovery_phrase_hash = if let Some(phrase) = recovery_phrase { Some(PasswordService::hash_password(&phrase)?) } else { None }; let now = chrono::Utc::now(); Ok(User { id: None, email, username, password_hash, recovery_phrase_hash, recovery_enabled: recovery_phrase_hash.is_some(), token_version: 0, created_at: now, last_active: now, email_verified: false, verification_token: None, verification_expires: None, }) } /// Verify a password against the stored hash pub fn verify_password(&self, password: &str) -> Result { verify_password(password, &self.password_hash) } /// Verify a recovery phrase against the stored hash pub fn verify_recovery_phrase(&self, phrase: &str) -> Result { if !self.recovery_enabled || self.recovery_phrase_hash.is_none() { return Ok(false); } let hash = self.recovery_phrase_hash.as_ref().unwrap(); verify_password(phrase, hash) } /// Update the password hash (increments token_version to invalidate all tokens) pub fn update_password(&mut self, new_password: String) -> Result<(), anyhow::Error> { self.password_hash = PasswordService::hash_password(&new_password)?; self.token_version += 1; Ok(()) } /// Set or update the recovery phrase pub fn set_recovery_phrase(&mut self, phrase: String) -> Result<(), anyhow::Error> { self.recovery_phrase_hash = Some(PasswordService::hash_password(&phrase)?); self.recovery_enabled = true; Ok(()) } /// Remove the recovery phrase (disable password recovery) pub fn remove_recovery_phrase(&mut self) { self.recovery_phrase_hash = None; self.recovery_enabled = false; } /// Increment token version (invalidates all existing tokens) pub fn increment_token_version(&mut self) { self.token_version += 1; } } /// Repository for User operations #[derive(Clone)] pub struct UserRepository { collection: Collection, } impl UserRepository { /// Create a new UserRepository pub fn new(collection: Collection) -> Self { Self { collection } } /// Create a new user pub async fn create(&self, user: &User) -> mongodb::error::Result> { let result = self.collection.insert_one(user, None).await?; Ok(Some(result.inserted_id.as_object_id().unwrap())) } /// Find a user by email pub async fn find_by_email(&self, email: &str) -> mongodb::error::Result> { self.collection .find_one(doc! { "email": email }, None) .await } /// Find a user by ID pub async fn find_by_id(&self, id: &ObjectId) -> mongodb::error::Result> { self.collection .find_one(doc! { "_id": id }, None) .await } /// Find a user by verification token pub async fn find_by_verification_token(&self, token: &str) -> mongodb::error::Result> { self.collection .find_one(doc! { "verification_token": token }, None) .await } /// Update a user pub async fn update(&self, user: &User) -> mongodb::error::Result<()> { self.collection .replace_one(doc! { "_id": &user.id }, user, None) .await?; Ok(()) } /// Update the token version pub async fn update_token_version(&self, user_id: &str, version: i32) -> mongodb::error::Result<()> { let oid = mongodb::bson::oid::ObjectId::parse_str(user_id)?; self.collection .update_one( doc! { "_id": oid }, doc! { "$set": { "token_version": version } }, None, ) .await?; Ok(()) } /// Delete a user pub async fn delete(&self, user_id: &ObjectId) -> mongodb::error::Result<()> { self.collection .delete_one(doc! { "_id": user_id }, None) .await?; Ok(()) } /// Update last active timestamp pub async fn update_last_active(&self, user_id: &ObjectId) -> mongodb::error::Result<()> { self.collection .update_one( doc! { "_id": user_id }, doc! { "$set": { "last_active": chrono::Utc::now() } }, None, ) .await?; Ok(()) } }