From ee0feb77ef2ea213f1e147bca32bd5efa7091d14 Mon Sep 17 00:00:00 2001 From: goose Date: Wed, 11 Mar 2026 11:16:03 -0300 Subject: [PATCH] style: apply rustfmt to backend codebase - Apply rustfmt to all Rust source files in backend/ - Fix trailing whitespace inconsistencies - Standardize formatting across handlers, models, and services - Improve code readability with consistent formatting These changes are purely stylistic and do not affect functionality. All CI checks now pass with proper formatting. --- backend/src/auth/jwt.rs | 40 +- backend/src/auth/password.rs | 13 +- backend/src/config/mod.rs | 15 +- backend/src/db/mongodb_impl.rs | 175 +++++---- backend/src/handlers/auth.rs | 400 ++++++++++++-------- backend/src/handlers/health.rs | 6 +- backend/src/handlers/health_stats.rs | 99 +++-- backend/src/handlers/interactions.rs | 16 +- backend/src/handlers/medications.rs | 49 ++- backend/src/handlers/mod.rs | 28 +- backend/src/handlers/permissions.rs | 38 +- backend/src/handlers/sessions.rs | 6 +- backend/src/handlers/shares.rs | 303 +++++++++------ backend/src/handlers/users.rs | 246 +++++++----- backend/src/main.rs | 56 +-- backend/src/middleware/auth.rs | 16 +- backend/src/middleware/mod.rs | 16 +- backend/src/middleware/rate_limit.rs | 12 +- backend/src/models/audit_log.rs | 21 +- backend/src/models/family.rs | 14 +- backend/src/models/health_data.rs | 2 +- backend/src/models/health_stats.rs | 15 +- backend/src/models/interactions.rs | 4 +- backend/src/models/lab_result.rs | 14 +- backend/src/models/medication.rs | 88 +++-- backend/src/models/mod.rs | 2 +- backend/src/models/permission.rs | 6 +- backend/src/models/profile.rs | 14 +- backend/src/models/refresh_token.rs | 2 +- backend/src/models/session.rs | 35 +- backend/src/models/share.rs | 42 +- backend/src/models/user.rs | 37 +- backend/src/security/account_lockout.rs | 47 +-- backend/src/security/audit_logger.rs | 11 +- backend/src/security/mod.rs | 4 +- backend/src/security/session_manager.rs | 6 +- backend/src/services/ingredient_mapper.rs | 33 +- backend/src/services/interaction_service.rs | 54 +-- backend/src/services/mod.rs | 6 +- backend/tests/auth_tests.rs | 75 ++-- backend/tests/medication_tests.rs | 19 +- 41 files changed, 1266 insertions(+), 819 deletions(-) diff --git a/backend/src/auth/jwt.rs b/backend/src/auth/jwt.rs index 60289b8..e34cac3 100644 --- a/backend/src/auth/jwt.rs +++ b/backend/src/auth/jwt.rs @@ -23,7 +23,7 @@ impl Claims { pub fn new(user_id: String, email: String, token_version: i32) -> Self { let now = Utc::now(); let exp = now + Duration::minutes(15); // Access token expires in 15 minutes - + Self { sub: user_id.clone(), exp: exp.timestamp() as usize, @@ -49,7 +49,7 @@ impl RefreshClaims { pub fn new(user_id: String, token_version: i32) -> Self { let now = Utc::now(); let exp = now + Duration::days(30); // Refresh token expires in 30 days - + Self { sub: user_id.clone(), exp: exp.timestamp() as usize, @@ -74,7 +74,7 @@ impl JwtService { pub fn new(config: JwtConfig) -> Self { let encoding_key = EncodingKey::from_secret(config.secret.as_ref()); let decoding_key = DecodingKey::from_secret(config.secret.as_ref()); - + Self { config, refresh_tokens: Arc::new(RwLock::new(HashMap::new())), @@ -99,24 +99,16 @@ impl JwtService { /// Validate access token pub fn validate_token(&self, token: &str) -> Result { - let token_data = decode::( - token, - &self.decoding_key, - &Validation::default() - ) - .map_err(|e| anyhow::anyhow!("Invalid token: {}", e))?; + let token_data = decode::(token, &self.decoding_key, &Validation::default()) + .map_err(|e| anyhow::anyhow!("Invalid token: {}", e))?; Ok(token_data.claims) } /// Validate refresh token pub fn validate_refresh_token(&self, token: &str) -> Result { - let token_data = decode::( - token, - &self.decoding_key, - &Validation::default() - ) - .map_err(|e| anyhow::anyhow!("Invalid refresh token: {}", e))?; + let token_data = decode::(token, &self.decoding_key, &Validation::default()) + .map_err(|e| anyhow::anyhow!("Invalid refresh token: {}", e))?; Ok(token_data.claims) } @@ -124,10 +116,11 @@ impl JwtService { /// Store refresh token for a user pub async fn store_refresh_token(&self, user_id: &str, token: &str) -> Result<()> { let mut tokens = self.refresh_tokens.write().await; - tokens.entry(user_id.to_string()) + tokens + .entry(user_id.to_string()) .or_insert_with(Vec::new) .push(token.to_string()); - + // Keep only last 5 tokens per user if let Some(user_tokens) = tokens.get_mut(user_id) { user_tokens.sort(); @@ -136,7 +129,7 @@ impl JwtService { *user_tokens = user_tokens.split_off(user_tokens.len() - 5); } } - + Ok(()) } @@ -151,13 +144,18 @@ impl JwtService { } /// Rotate refresh token (remove old, add new) - pub async fn rotate_refresh_token(&self, user_id: &str, old_token: &str, new_token: &str) -> Result<()> { + pub async fn rotate_refresh_token( + &self, + user_id: &str, + old_token: &str, + new_token: &str, + ) -> Result<()> { // Remove old token self.revoke_refresh_token(old_token).await?; - + // Add new token self.store_refresh_token(user_id, new_token).await?; - + Ok(()) } diff --git a/backend/src/auth/password.rs b/backend/src/auth/password.rs index 9ffc867..04e3bbd 100644 --- a/backend/src/auth/password.rs +++ b/backend/src/auth/password.rs @@ -1,10 +1,7 @@ use anyhow::Result; use pbkdf2::{ - password_hash::{ - rand_core::OsRng, - PasswordHash, PasswordHasher, PasswordVerifier, SaltString - }, - Pbkdf2 + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Pbkdf2, }; pub struct PasswordService; @@ -12,7 +9,8 @@ pub struct PasswordService; impl PasswordService { pub fn hash_password(password: &str) -> Result { let salt = SaltString::generate(&mut OsRng); - let password_hash = Pbkdf2.hash_password(password.as_bytes(), &salt) + let password_hash = Pbkdf2 + .hash_password(password.as_bytes(), &salt) .map_err(|e| anyhow::anyhow!("Failed to hash password: {}", e))?; Ok(password_hash.to_string()) } @@ -21,7 +19,8 @@ impl PasswordService { let parsed_hash = PasswordHash::new(hash) .map_err(|e| anyhow::anyhow!("Failed to parse password hash: {}", e))?; - Pbkdf2.verify_password(password.as_bytes(), &parsed_hash) + Pbkdf2 + .verify_password(password.as_bytes(), &parsed_hash) .map(|_| true) .map_err(|e| anyhow::anyhow!("Password verification failed: {}", e)) } diff --git a/backend/src/config/mod.rs b/backend/src/config/mod.rs index 323c526..ec57c5d 100644 --- a/backend/src/config/mod.rs +++ b/backend/src/config/mod.rs @@ -1,5 +1,5 @@ -use std::sync::Arc; use anyhow::Result; +use std::sync::Arc; #[derive(Clone)] pub struct AppState { @@ -11,7 +11,7 @@ pub struct AppState { pub account_lockout: Option, pub health_stats_repo: Option, pub mongo_client: Option, - + /// Phase 2.8: Interaction checker service pub interaction_service: Option>, } @@ -48,7 +48,7 @@ impl JwtConfig { pub fn access_token_expiry_duration(&self) -> std::time::Duration { std::time::Duration::from_secs(self.access_token_expiry_minutes as u64 * 60) } - + pub fn refresh_token_expiry_duration(&self) -> std::time::Duration { std::time::Duration::from_secs(self.refresh_token_expiry_days as u64 * 86400) } @@ -74,8 +74,10 @@ impl Config { .parse()?, }, database: DatabaseConfig { - uri: std::env::var("MONGODB_URI").unwrap_or_else(|_| "mongodb://localhost:27017".to_string()), - database: std::env::var("MONGODB_DATABASE").unwrap_or_else(|_| "normogen".to_string()), + uri: std::env::var("MONGODB_URI") + .unwrap_or_else(|_| "mongodb://localhost:27017".to_string()), + database: std::env::var("MONGODB_DATABASE") + .unwrap_or_else(|_| "normogen".to_string()), }, jwt: JwtConfig { secret: std::env::var("JWT_SECRET").unwrap_or_else(|_| "secret".to_string()), @@ -87,7 +89,8 @@ impl Config { .parse()?, }, encryption: EncryptionConfig { - key: std::env::var("ENCRYPTION_KEY").unwrap_or_else(|_| "default_key_32_bytes_long!".to_string()), + key: std::env::var("ENCRYPTION_KEY") + .unwrap_or_else(|_| "default_key_32_bytes_long!".to_string()), }, cors: CorsConfig { allowed_origins: std::env::var("CORS_ALLOWED_ORIGINS") diff --git a/backend/src/db/mongodb_impl.rs b/backend/src/db/mongodb_impl.rs index 0b2fbab..6040074 100644 --- a/backend/src/db/mongodb_impl.rs +++ b/backend/src/db/mongodb_impl.rs @@ -1,13 +1,13 @@ -use mongodb::{Client, Database, Collection, bson::doc, options::ClientOptions}; use anyhow::Result; use mongodb::bson::oid::ObjectId; +use mongodb::{bson::doc, options::ClientOptions, Client, Collection, Database}; use std::time::Duration; use crate::models::{ - user::{User, UserRepository}, - share::{Share, ShareRepository}, + medication::{Medication, MedicationDose, MedicationRepository, UpdateMedicationRequest}, permission::Permission, - medication::{Medication, MedicationRepository, MedicationDose, UpdateMedicationRequest}, + share::{Share, ShareRepository}, + user::{User, UserRepository}, }; #[derive(Clone)] @@ -22,7 +22,7 @@ pub struct MongoDb { impl MongoDb { pub async fn new(uri: &str, db_name: &str) -> Result { eprintln!("[MongoDB] Starting connection to: {}", uri); - + // Parse the URI first let mut client_options = match ClientOptions::parse(uri).await { Ok(opts) => { @@ -31,27 +31,35 @@ impl MongoDb { } Err(e) => { eprintln!("[MongoDB] ERROR: Failed to parse URI: {}", e); - + let error_msg = e.to_string().to_lowercase(); - if error_msg.contains("dns") || error_msg.contains("resolution") || error_msg.contains("lookup") { + if error_msg.contains("dns") + || error_msg.contains("resolution") + || error_msg.contains("lookup") + { eprintln!("[MongoDB] DNS RESOLUTION ERROR DETECTED!"); eprintln!("[MongoDB] Cannot resolve hostname in: {}", uri); eprintln!("[MongoDB] Error: {}", e); } - eprintln!("[MongoDB] Will continue in degraded mode (database operations will fail)"); - + eprintln!( + "[MongoDB] Will continue in degraded mode (database operations will fail)" + ); + // Create a minimal configuration that will allow the server to start // but database operations will fail gracefully - let mut opts = ClientOptions::parse("mongodb://localhost:27017").await - .map_err(|e| anyhow::anyhow!("Failed to create fallback client options: {}", e))?; + let mut opts = ClientOptions::parse("mongodb://localhost:27017") + .await + .map_err(|e| { + anyhow::anyhow!("Failed to create fallback client options: {}", e) + })?; opts.server_selection_timeout = Some(Duration::from_secs(1)); opts.connect_timeout = Some(Duration::from_secs(1)); - + let client = Client::with_options(opts) .map_err(|e| anyhow::anyhow!("Failed to create MongoDB client: {}", e))?; - + let database = client.database(db_name); - + return Ok(Self { users: database.collection("users"), shares: database.collection("shares"), @@ -61,13 +69,13 @@ impl MongoDb { }); } }; - + // Set connection timeout with retry logic client_options.server_selection_timeout = Some(Duration::from_secs(10)); client_options.connect_timeout = Some(Duration::from_secs(10)); - + eprintln!("[MongoDB] Connecting to server..."); - + let client = match Client::with_options(client_options) { Ok(c) => { eprintln!("[MongoDB] Client created successfully"); @@ -76,14 +84,17 @@ impl MongoDb { Err(e) => { eprintln!("[MongoDB] ERROR: Failed to create client: {}", e); eprintln!("[MongoDB] Will continue in degraded mode"); - + // Create a fallback client - let fallback_opts = ClientOptions::parse("mongodb://localhost:27017").await - .map_err(|e| anyhow::anyhow!("Failed to create fallback client options: {}", e))?; + let fallback_opts = ClientOptions::parse("mongodb://localhost:27017") + .await + .map_err(|e| { + anyhow::anyhow!("Failed to create fallback client options: {}", e) + })?; let fallback_client = Client::with_options(fallback_opts) .map_err(|e| anyhow::anyhow!("Failed to create MongoDB client: {}", e))?; let database = fallback_client.database(db_name); - + return Ok(Self { users: database.collection("users"), shares: database.collection("shares"), @@ -93,13 +104,13 @@ impl MongoDb { }); } }; - + eprintln!("[MongoDB] Client created, selecting database..."); - + let database = client.database(db_name); - + eprintln!("[MongoDB] Database selected: {}", db_name); - + Ok(Self { users: database.collection("users"), shares: database.collection("shares"), @@ -108,7 +119,7 @@ impl MongoDb { database, }) } - + pub async fn health_check(&self) -> Result { eprintln!("[MongoDB] Health check: pinging database..."); match self.database.run_command(doc! { "ping": 1 }, None).await { @@ -124,82 +135,82 @@ impl MongoDb { } } } - + /// Get a reference to the underlying MongoDB Database /// This is needed for security services in Phase 2.6 pub fn get_database(&self) -> Database { self.database.clone() } - + // ===== User Methods ===== - + pub async fn create_user(&self, user: &User) -> Result> { let repo = UserRepository::new(self.users.clone()); Ok(repo.create(user).await?) } - + pub async fn find_user_by_email(&self, email: &str) -> Result> { let repo = UserRepository::new(self.users.clone()); Ok(repo.find_by_email(email).await?) } - + pub async fn find_user_by_id(&self, id: &ObjectId) -> Result> { let repo = UserRepository::new(self.users.clone()); Ok(repo.find_by_id(id).await?) } - + pub async fn update_user(&self, user: &User) -> Result<()> { let repo = UserRepository::new(self.users.clone()); repo.update(user).await?; Ok(()) } - + pub async fn update_last_active(&self, user_id: &ObjectId) -> Result<()> { let repo = UserRepository::new(self.users.clone()); repo.update_last_active(user_id).await?; Ok(()) } - + pub async fn delete_user(&self, user_id: &ObjectId) -> Result<()> { let repo = UserRepository::new(self.users.clone()); repo.delete(user_id).await?; Ok(()) } - + // ===== Share Methods ===== - + pub async fn create_share(&self, share: &Share) -> Result> { let repo = ShareRepository::new(self.shares.clone()); Ok(repo.create(share).await?) } - + pub async fn get_share(&self, id: &str) -> Result> { let object_id = ObjectId::parse_str(id)?; let repo = ShareRepository::new(self.shares.clone()); Ok(repo.find_by_id(&object_id).await?) } - + pub async fn list_shares_for_user(&self, user_id: &str) -> Result> { let object_id = ObjectId::parse_str(user_id)?; let repo = ShareRepository::new(self.shares.clone()); Ok(repo.find_by_target(&object_id).await?) } - + pub async fn update_share(&self, share: &Share) -> Result<()> { let repo = ShareRepository::new(self.shares.clone()); repo.update(share).await?; Ok(()) } - + pub async fn delete_share(&self, id: &str) -> Result<()> { let object_id = ObjectId::parse_str(id)?; let repo = ShareRepository::new(self.shares.clone()); repo.delete(&object_id).await?; Ok(()) } - + // ===== Permission Methods ===== - + pub async fn check_user_permission( &self, user_id: &str, @@ -209,12 +220,12 @@ impl MongoDb { ) -> Result { let user_oid = ObjectId::parse_str(user_id)?; let resource_oid = ObjectId::parse_str(resource_id)?; - + let repo = ShareRepository::new(self.shares.clone()); let shares = repo.find_by_target(&user_oid).await?; - + for share in shares { - if share.resource_type == resource_type + if share.resource_type == resource_type && share.resource_id.as_ref() == Some(&resource_oid) && share.active && !share.is_expired() @@ -228,16 +239,16 @@ impl MongoDb { "admin" => Permission::Admin, _ => return Ok(false), }; - + if share.has_permission(&perm) { return Ok(true); } } } - + Ok(false) } - + /// Check permission using a simplified interface pub async fn check_permission( &self, @@ -247,74 +258,98 @@ impl MongoDb { ) -> Result { // For now, check all resource types let resource_types = ["profiles", "health_data", "lab_results", "medications"]; - + for resource_type in resource_types { - if self.check_user_permission(user_id, resource_type, resource_id, permission).await? { + if self + .check_user_permission(user_id, resource_type, resource_id, permission) + .await? + { return Ok(true); } } - + Ok(false) } - + // ===== Medication Methods (Fixed for Phase 2.8) ===== - + pub async fn create_medication(&self, medication: &Medication) -> Result> { let repo = MedicationRepository::new(self.medications.clone()); - let created = repo.create(medication.clone()) + let created = repo + .create(medication.clone()) .await .map_err(|e| anyhow::anyhow!("Failed to create medication: {}", e))?; Ok(created.id) } - + pub async fn get_medication(&self, id: &str) -> Result> { let object_id = ObjectId::parse_str(id)?; let repo = MedicationRepository::new(self.medications.clone()); - Ok(repo.find_by_id(&object_id) + Ok(repo + .find_by_id(&object_id) .await .map_err(|e| anyhow::anyhow!("Failed to get medication: {}", e))?) } - - pub async fn list_medications(&self, user_id: &str, profile_id: Option<&str>) -> Result> { + + pub async fn list_medications( + &self, + user_id: &str, + profile_id: Option<&str>, + ) -> Result> { let repo = MedicationRepository::new(self.medications.clone()); if let Some(profile_id) = profile_id { - Ok(repo.find_by_user_and_profile(user_id, profile_id) + Ok(repo + .find_by_user_and_profile(user_id, profile_id) .await .map_err(|e| anyhow::anyhow!("Failed to list medications by profile: {}", e))?) } else { - Ok(repo.find_by_user(user_id) + Ok(repo + .find_by_user(user_id) .await .map_err(|e| anyhow::anyhow!("Failed to list medications: {}", e))?) } } - - pub async fn update_medication(&self, id: &str, updates: UpdateMedicationRequest) -> Result> { + + pub async fn update_medication( + &self, + id: &str, + updates: UpdateMedicationRequest, + ) -> Result> { let object_id = ObjectId::parse_str(id)?; let repo = MedicationRepository::new(self.medications.clone()); - Ok(repo.update(&object_id, updates) + Ok(repo + .update(&object_id, updates) .await .map_err(|e| anyhow::anyhow!("Failed to update medication: {}", e))?) } - + pub async fn delete_medication(&self, id: &str) -> Result { let object_id = ObjectId::parse_str(id)?; let repo = MedicationRepository::new(self.medications.clone()); - Ok(repo.delete(&object_id) + Ok(repo + .delete(&object_id) .await .map_err(|e| anyhow::anyhow!("Failed to delete medication: {}", e))?) } - + pub async fn log_medication_dose(&self, dose: &MedicationDose) -> Result> { // Insert the dose into the medication_doses collection - let result = self.medication_doses.insert_one(dose.clone(), None) + let result = self + .medication_doses + .insert_one(dose.clone(), None) .await .map_err(|e| anyhow::anyhow!("Failed to log dose: {}", e))?; Ok(result.inserted_id.as_object_id()) } - - pub async fn get_medication_adherence(&self, medication_id: &str, days: i64) -> Result { + + pub async fn get_medication_adherence( + &self, + medication_id: &str, + days: i64, + ) -> Result { let repo = MedicationRepository::new(self.medications.clone()); - Ok(repo.calculate_adherence(medication_id, days) + Ok(repo + .calculate_adherence(medication_id, days) .await .map_err(|e| anyhow::anyhow!("Failed to calculate adherence: {}", e))?) } diff --git a/backend/src/handlers/auth.rs b/backend/src/handlers/auth.rs index 95c6c0e..4c2e625 100644 --- a/backend/src/handlers/auth.rs +++ b/backend/src/handlers/auth.rs @@ -1,17 +1,9 @@ -use axum::{ - extract::{State}, - http::StatusCode, - response::IntoResponse, - Json, -}; +use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; use serde::{Deserialize, Serialize}; use validator::Validate; use crate::{ - auth::jwt::Claims, - config::AppState, - models::user::User, - models::audit_log::AuditEventType, + auth::jwt::Claims, config::AppState, models::audit_log::AuditEventType, models::user::User, }; #[derive(Debug, Deserialize, Validate)] @@ -39,28 +31,40 @@ pub async fn register( Json(req): Json, ) -> impl IntoResponse { if let Err(errors) = req.validate() { - return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ - "error": "validation failed", - "details": errors.to_string() - }))).into_response(); + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ + "error": "validation failed", + "details": errors.to_string() + })), + ) + .into_response(); } - + // Check if user already exists match state.db.find_user_by_email(&req.email).await { Ok(Some(_)) => { - return (StatusCode::CONFLICT, Json(serde_json::json!({ - "error": "user already exists" - }))).into_response() + return ( + StatusCode::CONFLICT, + Json(serde_json::json!({ + "error": "user already exists" + })), + ) + .into_response() } - Ok(None) => {}, + Ok(None) => {} Err(e) => { tracing::error!("Failed to check user existence: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "database error" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "database error" + })), + ) + .into_response(); } } - + // Create new user let user = match User::new( req.email.clone(), @@ -71,63 +75,81 @@ pub async fn register( Ok(u) => u, Err(e) => { tracing::error!("Failed to create user: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to create user" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to create user" + })), + ) + .into_response(); } }; - + // Get token_version before saving let token_version = user.token_version; - + // Save user to database let user_id = match state.db.create_user(&user).await { Ok(Some(id)) => { // Log registration (Phase 2.6) if let Some(ref audit) = state.audit_logger { - let _ = audit.log_event( - AuditEventType::LoginSuccess, // Using LoginSuccess as registration event - Some(id), - Some(req.email.clone()), - "0.0.0.0".to_string(), - None, - None, - ).await; + let _ = audit + .log_event( + AuditEventType::LoginSuccess, // Using LoginSuccess as registration event + Some(id), + Some(req.email.clone()), + "0.0.0.0".to_string(), + None, + None, + ) + .await; } id - }, + } Ok(None) => { - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to create user" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to create user" + })), + ) + .into_response() } Err(e) => { tracing::error!("Failed to save user: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "database error" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "database error" + })), + ) + .into_response(); } }; - + // Generate JWT token let claims = Claims::new(user_id.to_string(), user.email.clone(), token_version); let (token, _refresh_token) = match state.jwt_service.generate_tokens(claims) { Ok(t) => t, Err(e) => { tracing::error!("Failed to generate token: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to generate token" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to generate token" + })), + ) + .into_response(); } }; - + let response = AuthResponse { token, user_id: user_id.to_string(), email: user.email, username: user.username, }; - + (StatusCode::CREATED, Json(response)).into_response() } @@ -144,28 +166,36 @@ pub async fn login( Json(req): Json, ) -> impl IntoResponse { if let Err(errors) = req.validate() { - return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ - "error": "validation failed", - "details": errors.to_string() - }))).into_response(); + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ + "error": "validation failed", + "details": errors.to_string() + })), + ) + .into_response(); } - + // Check account lockout status (Phase 2.6) if let Some(ref lockout) = state.account_lockout { match lockout.check_lockout(&req.email).await { Ok(true) => { - return (StatusCode::TOO_MANY_REQUESTS, Json(serde_json::json!({ - "error": "account is temporarily locked due to too many failed attempts", - "retry_after": "please try again later" - }))).into_response() + return ( + StatusCode::TOO_MANY_REQUESTS, + Json(serde_json::json!({ + "error": "account is temporarily locked due to too many failed attempts", + "retry_after": "please try again later" + })), + ) + .into_response() } - Ok(false) => {}, + Ok(false) => {} Err(e) => { tracing::error!("Failed to check lockout status: {}", e); } } } - + // Find user by email let user = match state.db.find_user_by_email(&req.email).await { Ok(Some(u)) => u, @@ -174,37 +204,53 @@ pub async fn login( if let Some(ref lockout) = state.account_lockout { let _ = lockout.record_failed_attempt(&req.email).await; } - + // Log failed login (Phase 2.6) if let Some(ref audit) = state.audit_logger { - let _ = audit.log_event( - AuditEventType::LoginFailed, - None, - Some(req.email.clone()), - "0.0.0.0".to_string(), // TODO: Extract real IP - None, - None, - ).await; + let _ = audit + .log_event( + AuditEventType::LoginFailed, + None, + Some(req.email.clone()), + "0.0.0.0".to_string(), // TODO: Extract real IP + None, + None, + ) + .await; } - - return (StatusCode::UNAUTHORIZED, Json(serde_json::json!({ - "error": "invalid credentials" - }))).into_response() + + return ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({ + "error": "invalid credentials" + })), + ) + .into_response(); } Err(e) => { tracing::error!("Failed to find user: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "database error" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "database error" + })), + ) + .into_response(); } }; - - let user_id = user.id.ok_or_else(|| { - (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "invalid user state" - }))) - }).unwrap(); - + + let user_id = user + .id + .ok_or_else(|| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "invalid user state" + })), + ) + }) + .unwrap(); + // Verify password match user.verify_password(&req.password) { Ok(true) => { @@ -212,70 +258,86 @@ pub async fn login( if let Some(ref lockout) = state.account_lockout { let _ = lockout.reset_attempts(&req.email).await; } - }, + } Ok(false) => { // Record failed attempt (Phase 2.6) if let Some(ref lockout) = state.account_lockout { let _ = lockout.record_failed_attempt(&req.email).await; } - + // Log failed login (Phase 2.6) if let Some(ref audit) = state.audit_logger { - let _ = audit.log_event( - AuditEventType::LoginFailed, - Some(user_id), - Some(req.email.clone()), - "0.0.0.0".to_string(), // TODO: Extract real IP - None, - None, - ).await; + let _ = audit + .log_event( + AuditEventType::LoginFailed, + Some(user_id), + Some(req.email.clone()), + "0.0.0.0".to_string(), // TODO: Extract real IP + None, + None, + ) + .await; } - - return (StatusCode::UNAUTHORIZED, Json(serde_json::json!({ - "error": "invalid credentials" - }))).into_response() + + return ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({ + "error": "invalid credentials" + })), + ) + .into_response(); } Err(e) => { tracing::error!("Failed to verify password: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "authentication error" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "authentication error" + })), + ) + .into_response(); } } - + // Update last active timestamp (TODO: Implement in database layer) - + // Generate JWT token let claims = Claims::new(user_id.to_string(), user.email.clone(), user.token_version); let (token, _refresh_token) = match state.jwt_service.generate_tokens(claims) { Ok(t) => t, Err(e) => { tracing::error!("Failed to generate token: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to generate token" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to generate token" + })), + ) + .into_response(); } }; - + // Log successful login (Phase 2.6) if let Some(ref audit) = state.audit_logger { - let _ = audit.log_event( - AuditEventType::LoginSuccess, - Some(user_id), - Some(req.email.clone()), - "0.0.0.0".to_string(), // TODO: Extract real IP - None, - None, - ).await; + let _ = audit + .log_event( + AuditEventType::LoginSuccess, + Some(user_id), + Some(req.email.clone()), + "0.0.0.0".to_string(), // TODO: Extract real IP + None, + None, + ) + .await; } - + let response = AuthResponse { token, user_id: user_id.to_string(), email: user.email, username: user.username, }; - + (StatusCode::OK, Json(response)).into_response() } @@ -294,78 +356,108 @@ pub async fn recover_password( Json(req): Json, ) -> impl IntoResponse { if let Err(errors) = req.validate() { - return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ - "error": "validation failed", - "details": errors.to_string() - }))).into_response(); + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ + "error": "validation failed", + "details": errors.to_string() + })), + ) + .into_response(); } - + // Find user by email let mut user = match state.db.find_user_by_email(&req.email).await { Ok(Some(u)) => u, Ok(None) => { - return (StatusCode::UNAUTHORIZED, Json(serde_json::json!({ - "error": "invalid credentials" - }))).into_response() + return ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({ + "error": "invalid credentials" + })), + ) + .into_response() } Err(e) => { tracing::error!("Failed to find user: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "database error" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "database error" + })), + ) + .into_response(); } }; - + // Verify recovery phrase match user.verify_recovery_phrase(&req.recovery_phrase) { - Ok(true) => {}, + Ok(true) => {} Ok(false) => { - return (StatusCode::UNAUTHORIZED, Json(serde_json::json!({ - "error": "invalid credentials" - }))).into_response() + return ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({ + "error": "invalid credentials" + })), + ) + .into_response() } Err(e) => { tracing::error!("Failed to verify recovery phrase: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "authentication error" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "authentication error" + })), + ) + .into_response(); } } - + // Update password match user.update_password(req.new_password) { - Ok(_) => {}, + Ok(_) => {} Err(e) => { tracing::error!("Failed to update password: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to update password" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to update password" + })), + ) + .into_response(); } } - + // Save updated user match state.db.update_user(&user).await { Ok(_) => { // Log password recovery (Phase 2.6) if let Some(ref audit) = state.audit_logger { let user_id_for_log = user.id; - let _ = audit.log_event( - AuditEventType::PasswordRecovery, - user_id_for_log, - Some(req.email.clone()), - "0.0.0.0".to_string(), - None, - None, - ).await; + let _ = audit + .log_event( + AuditEventType::PasswordRecovery, + user_id_for_log, + Some(req.email.clone()), + "0.0.0.0".to_string(), + None, + None, + ) + .await; } - + (StatusCode::NO_CONTENT, ()).into_response() - }, + } Err(e) => { tracing::error!("Failed to save user: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "database error" - }))).into_response() + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "database error" + })), + ) + .into_response() } } } diff --git a/backend/src/handlers/health.rs b/backend/src/handlers/health.rs index 2964c14..6b7920e 100644 --- a/backend/src/handlers/health.rs +++ b/backend/src/handlers/health.rs @@ -1,6 +1,6 @@ +use crate::config::AppState; use axum::{extract::State, response::Json}; use serde_json::{json, Value}; -use crate::config::AppState; pub async fn health_check(State(state): State) -> Json { let status = if let Ok(_) = state.db.health_check().await { @@ -8,10 +8,10 @@ pub async fn health_check(State(state): State) -> Json { } else { "error" }; - + // Use timestamp_millis for consistency with other endpoints let timestamp = mongodb::bson::DateTime::now().timestamp_millis(); - + Json(json!({ "status": "ok", "database": status, diff --git a/backend/src/handlers/health_stats.rs b/backend/src/handlers/health_stats.rs index f45a996..4d1e592 100644 --- a/backend/src/handlers/health_stats.rs +++ b/backend/src/handlers/health_stats.rs @@ -1,14 +1,19 @@ -use axum::{Extension, Json, extract::{Path, State, Query}, http::StatusCode, response::IntoResponse}; -use mongodb::bson::oid::ObjectId; -use serde::Deserialize; -use crate::models::health_stats::HealthStatistic; use crate::auth::jwt::Claims; use crate::config::AppState; +use crate::models::health_stats::HealthStatistic; +use axum::{ + extract::{Path, Query, State}, + http::StatusCode, + response::IntoResponse, + Extension, Json, +}; +use mongodb::bson::oid::ObjectId; +use serde::Deserialize; #[derive(Debug, Deserialize)] pub struct CreateHealthStatRequest { pub stat_type: String, - pub value: serde_json::Value, // Support complex values like blood pressure + pub value: serde_json::Value, // Support complex values like blood pressure pub unit: String, pub notes: Option, pub recorded_at: Option, @@ -24,7 +29,7 @@ pub struct UpdateHealthStatRequest { #[derive(Debug, Deserialize)] pub struct HealthTrendsQuery { pub stat_type: String, - pub period: Option, // "7d", "30d", etc. + pub period: Option, // "7d", "30d", etc. } pub async fn create_health_stat( @@ -33,7 +38,7 @@ pub async fn create_health_stat( Json(req): Json, ) -> impl IntoResponse { let repo = state.health_stats_repo.as_ref().unwrap(); - + // Convert complex value to f64 or store as string let value_num = match req.value { serde_json::Value::Number(n) => n.as_f64().unwrap_or(0.0), @@ -41,9 +46,9 @@ pub async fn create_health_stat( // For complex objects like blood pressure, use a default 0.0 } - _ => 0.0 + _ => 0.0, }; - + let stat = HealthStatistic { id: None, user_id: claims.sub.clone(), @@ -61,7 +66,11 @@ pub async fn create_health_stat( Ok(created) => (StatusCode::CREATED, Json(created)).into_response(), Err(e) => { eprintln!("Error creating health stat: {:?}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "Failed to create health stat").into_response() + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to create health stat", + ) + .into_response() } } } @@ -75,7 +84,11 @@ pub async fn list_health_stats( Ok(stats) => (StatusCode::OK, Json(stats)).into_response(), Err(e) => { eprintln!("Error fetching health stats: {:?}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch health stats").into_response() + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to fetch health stats", + ) + .into_response() } } } @@ -101,7 +114,11 @@ pub async fn get_health_stat( Ok(None) => (StatusCode::NOT_FOUND, "Health stat not found").into_response(), Err(e) => { eprintln!("Error fetching health stat: {:?}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch health stat").into_response() + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to fetch health stat", + ) + .into_response() } } } @@ -121,7 +138,13 @@ pub async fn update_health_stat( let mut stat = match repo.find_by_id(&object_id).await { Ok(Some(s)) => s, Ok(None) => return (StatusCode::NOT_FOUND, "Health stat not found").into_response(), - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch health stat").into_response(), + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to fetch health stat", + ) + .into_response() + } }; if stat.user_id != claims.sub { @@ -131,7 +154,7 @@ pub async fn update_health_stat( if let Some(value) = req.value { let value_num = match value { serde_json::Value::Number(n) => n.as_f64().unwrap_or(0.0), - _ => 0.0 + _ => 0.0, }; stat.value = value_num; } @@ -145,7 +168,11 @@ pub async fn update_health_stat( match repo.update(&object_id, &stat).await { Ok(Some(updated)) => (StatusCode::OK, Json(updated)).into_response(), Ok(None) => (StatusCode::NOT_FOUND, "Failed to update").into_response(), - Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Failed to update health stat").into_response(), + Err(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to update health stat", + ) + .into_response(), } } @@ -163,7 +190,13 @@ pub async fn delete_health_stat( let stat = match repo.find_by_id(&object_id).await { Ok(Some(s)) => s, Ok(None) => return (StatusCode::NOT_FOUND, "Health stat not found").into_response(), - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch health stat").into_response(), + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to fetch health stat", + ) + .into_response() + } }; if stat.user_id != claims.sub { @@ -172,7 +205,11 @@ pub async fn delete_health_stat( match repo.delete(&object_id).await { Ok(true) => StatusCode::NO_CONTENT.into_response(), - _ => (StatusCode::INTERNAL_SERVER_ERROR, "Failed to delete health stat").into_response(), + _ => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to delete health stat", + ) + .into_response(), } } @@ -189,21 +226,25 @@ pub async fn get_health_trends( .into_iter() .filter(|s| s.stat_type == query.stat_type) .collect(); - + // Calculate basic trend statistics if filtered.is_empty() { - return (StatusCode::OK, Json(serde_json::json!({ - "stat_type": query.stat_type, - "count": 0, - "data": [] - }))).into_response(); + return ( + StatusCode::OK, + Json(serde_json::json!({ + "stat_type": query.stat_type, + "count": 0, + "data": [] + })), + ) + .into_response(); } - + let values: Vec = filtered.iter().map(|s| s.value).collect(); let avg = values.iter().sum::() / values.len() as f64; let min = values.iter().fold(f64::INFINITY, |a, &b| a.min(b)); let max = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b)); - + let response = serde_json::json!({ "stat_type": query.stat_type, "count": filtered.len(), @@ -212,12 +253,16 @@ pub async fn get_health_trends( "max": max, "data": filtered }); - + (StatusCode::OK, Json(response)).into_response() } Err(e) => { eprintln!("Error fetching health trends: {:?}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch health trends").into_response() + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to fetch health trends", + ) + .into_response() } } } diff --git a/backend/src/handlers/interactions.rs b/backend/src/handlers/interactions.rs index 7dc0897..ce7c2e3 100644 --- a/backend/src/handlers/interactions.rs +++ b/backend/src/handlers/interactions.rs @@ -34,10 +34,12 @@ pub async fn check_interactions( if request.medications.len() < 2 { return Err(StatusCode::BAD_REQUEST); } - - let interaction_service = state.interaction_service.as_ref() + + let interaction_service = state + .interaction_service + .as_ref() .ok_or(StatusCode::SERVICE_UNAVAILABLE)?; - + match interaction_service .check_eu_medications(&request.medications) .await @@ -46,7 +48,7 @@ pub async fn check_interactions( let has_severe = interactions .iter() .any(|i| matches!(i.severity, InteractionSeverity::Severe)); - + Ok(Json(InteractionResponse { interactions, has_severe, @@ -76,9 +78,11 @@ pub async fn check_new_medication( State(state): State, Json(request): Json, ) -> Result, StatusCode> { - let interaction_service = state.interaction_service.as_ref() + let interaction_service = state + .interaction_service + .as_ref() .ok_or(StatusCode::SERVICE_UNAVAILABLE)?; - + match interaction_service .check_new_medication(&request.new_medication, &request.existing_medications) .await diff --git a/backend/src/handlers/medications.rs b/backend/src/handlers/medications.rs index 478e3ca..68e2f84 100644 --- a/backend/src/handlers/medications.rs +++ b/backend/src/handlers/medications.rs @@ -1,14 +1,17 @@ use axum::{ - extract::{Path, Query, State, Extension, Json}, + extract::{Extension, Json, Path, Query, State}, http::StatusCode, }; use mongodb::bson::oid::ObjectId; use std::time::SystemTime; use crate::{ - models::medication::{Medication, MedicationRepository, CreateMedicationRequest, UpdateMedicationRequest, LogDoseRequest}, - auth::jwt::Claims, // Fixed: import from auth::jwt instead of handlers::auth + auth::jwt::Claims, // Fixed: import from auth::jwt instead of handlers::auth config::AppState, + models::medication::{ + CreateMedicationRequest, LogDoseRequest, Medication, MedicationRepository, + UpdateMedicationRequest, + }, }; #[derive(serde::Deserialize)] @@ -25,7 +28,7 @@ pub async fn create_medication( ) -> Result, StatusCode> { let database = state.db.get_database(); let repo = MedicationRepository::new(database.collection("medications")); - + let now = SystemTime::now(); let medication_id = uuid::Uuid::new_v4().to_string(); @@ -59,12 +62,15 @@ pub async fn create_medication( user_id: claims.sub, profile_id: req.profile_id.clone(), medication_data, - reminders: req.reminder_times.unwrap_or_default().into_iter().map(|time| { - crate::models::medication::MedicationReminder { + reminders: req + .reminder_times + .unwrap_or_default() + .into_iter() + .map(|time| crate::models::medication::MedicationReminder { reminder_id: uuid::Uuid::new_v4().to_string(), scheduled_time: time, - } - }).collect(), + }) + .collect(), created_at: now.into(), updated_at: now.into(), pill_identification: req.pill_identification, // Phase 2.8 @@ -83,13 +89,10 @@ pub async fn list_medications( ) -> Result>, StatusCode> { let database = state.db.get_database(); let repo = MedicationRepository::new(database.collection("medications")); - + let _limit = query.limit.unwrap_or(100); - match repo - .find_by_user(&claims.sub) - .await - { + match repo.find_by_user(&claims.sub).await { Ok(medications) => Ok(Json(medications)), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), } @@ -102,7 +105,7 @@ pub async fn get_medication( ) -> Result, StatusCode> { let database = state.db.get_database(); let repo = MedicationRepository::new(database.collection("medications")); - + match ObjectId::parse_str(&id) { Ok(oid) => match repo.find_by_id(&oid).await { Ok(Some(medication)) => Ok(Json(medication)), @@ -121,7 +124,7 @@ pub async fn update_medication( ) -> Result, StatusCode> { let database = state.db.get_database(); let repo = MedicationRepository::new(database.collection("medications")); - + match ObjectId::parse_str(&id) { Ok(oid) => match repo.update(&oid, req).await { Ok(Some(medication)) => Ok(Json(medication)), @@ -139,7 +142,7 @@ pub async fn delete_medication( ) -> Result { let database = state.db.get_database(); let repo = MedicationRepository::new(database.collection("medications")); - + match ObjectId::parse_str(&id) { Ok(oid) => match repo.delete(&oid).await { Ok(true) => Ok(StatusCode::NO_CONTENT), @@ -157,9 +160,9 @@ pub async fn log_dose( Json(req): Json, ) -> Result { let database = state.db.get_database(); - + let now = SystemTime::now(); - + let dose = crate::models::medication::MedicationDose { id: None, medication_id: id.clone(), @@ -169,8 +172,12 @@ pub async fn log_dose( taken: req.taken.unwrap_or(true), notes: req.notes, }; - - match database.collection("medication_doses").insert_one(dose.clone(), None).await { + + match database + .collection("medication_doses") + .insert_one(dose.clone(), None) + .await + { Ok(_) => Ok(StatusCode::CREATED), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), } @@ -183,7 +190,7 @@ pub async fn get_adherence( ) -> Result, StatusCode> { let database = state.db.get_database(); let repo = MedicationRepository::new(database.collection("medications")); - + match repo.calculate_adherence(&id, 30).await { Ok(stats) => Ok(Json(stats)), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), diff --git a/backend/src/handlers/mod.rs b/backend/src/handlers/mod.rs index a248059..f34e7a7 100644 --- a/backend/src/handlers/mod.rs +++ b/backend/src/handlers/mod.rs @@ -1,20 +1,28 @@ pub mod auth; pub mod health; pub mod health_stats; +pub mod interactions; +pub mod medications; pub mod permissions; +pub mod sessions; pub mod shares; pub mod users; -pub mod sessions; -pub mod medications; -pub mod interactions; // Re-export commonly used handler functions -pub use auth::{register, login, recover_password}; +pub use auth::{login, recover_password, register}; pub use health::{health_check, ready_check}; -pub use shares::{create_share, list_shares, update_share, delete_share}; -pub use permissions::check_permission; -pub use users::{get_profile, update_profile, delete_account, change_password, get_settings, update_settings}; -pub use sessions::{get_sessions, revoke_session, revoke_all_sessions}; -pub use medications::{create_medication, list_medications, get_medication, update_medication, delete_medication, log_dose, get_adherence}; -pub use health_stats::{create_health_stat, list_health_stats, get_health_stat, update_health_stat, delete_health_stat, get_health_trends}; +pub use health_stats::{ + create_health_stat, delete_health_stat, get_health_stat, get_health_trends, list_health_stats, + update_health_stat, +}; pub use interactions::{check_interactions, check_new_medication}; +pub use medications::{ + create_medication, delete_medication, get_adherence, get_medication, list_medications, + log_dose, update_medication, +}; +pub use permissions::check_permission; +pub use sessions::{get_sessions, revoke_all_sessions, revoke_session}; +pub use shares::{create_share, delete_share, list_shares, update_share}; +pub use users::{ + change_password, delete_account, get_profile, get_settings, update_profile, update_settings, +}; diff --git a/backend/src/handlers/permissions.rs b/backend/src/handlers/permissions.rs index 2210410..6a4c751 100644 --- a/backend/src/handlers/permissions.rs +++ b/backend/src/handlers/permissions.rs @@ -2,15 +2,11 @@ use axum::{ extract::{Query, State}, http::StatusCode, response::IntoResponse, - Json, - Extension, + Extension, Json, }; use serde::{Deserialize, Serialize}; -use crate::{ - auth::jwt::Claims, - config::AppState, -}; +use crate::{auth::jwt::Claims, config::AppState}; #[derive(Debug, Deserialize)] pub struct CheckPermissionQuery { @@ -32,27 +28,35 @@ pub async fn check_permission( Query(params): Query, Extension(claims): Extension, ) -> impl IntoResponse { - let has_permission = match state.db.check_user_permission( - &claims.sub, - ¶ms.resource_type, - ¶ms.resource_id, - ¶ms.permission, - ).await { + let has_permission = match state + .db + .check_user_permission( + &claims.sub, + ¶ms.resource_type, + ¶ms.resource_id, + ¶ms.permission, + ) + .await + { Ok(result) => result, Err(e) => { tracing::error!("Failed to check permission: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to check permission" - }))).into_response(); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to check permission" + })), + ) + .into_response(); } }; - + let response = PermissionCheckResponse { has_permission, resource_type: params.resource_type, resource_id: params.resource_id, permission: params.permission, }; - + (StatusCode::OK, Json(response)).into_response() } diff --git a/backend/src/handlers/sessions.rs b/backend/src/handlers/sessions.rs index 74fe6f5..3ddbbbe 100644 --- a/backend/src/handlers/sessions.rs +++ b/backend/src/handlers/sessions.rs @@ -1,6 +1,10 @@ -use axum::{extract::{Path, State, Extension}, http::StatusCode, Json}; use crate::auth::jwt::Claims; use crate::config::AppState; +use axum::{ + extract::{Extension, Path, State}, + http::StatusCode, + Json, +}; use serde::Serialize; #[derive(Debug, Serialize)] diff --git a/backend/src/handlers/shares.rs b/backend/src/handlers/shares.rs index 6acdb7f..9e67c0a 100644 --- a/backend/src/handlers/shares.rs +++ b/backend/src/handlers/shares.rs @@ -2,17 +2,16 @@ use axum::{ extract::{Path, State}, http::StatusCode, response::IntoResponse, - Json, - Extension, + Extension, Json, }; +use mongodb::bson::oid::ObjectId; use serde::{Deserialize, Serialize}; use validator::Validate; -use mongodb::bson::oid::ObjectId; use crate::{ auth::jwt::Claims, config::AppState, - models::{share::Share, permission::Permission}, + models::{permission::Permission, share::Share}, }; #[derive(Debug, Deserialize, Validate)] @@ -39,14 +38,18 @@ pub struct ShareResponse { impl TryFrom for ShareResponse { type Error = anyhow::Error; - + fn try_from(share: Share) -> Result { Ok(Self { id: share.id.map(|id| id.to_string()).unwrap_or_default(), target_user_id: share.target_user_id.to_string(), resource_type: share.resource_type, resource_id: share.resource_id.map(|id| id.to_string()), - permissions: share.permissions.into_iter().map(|p| p.to_string()).collect(), + permissions: share + .permissions + .into_iter() + .map(|p| p.to_string()) + .collect(), expires_at: share.expires_at.map(|dt| dt.timestamp_millis()), created_at: share.created_at.timestamp_millis(), active: share.active, @@ -60,63 +63,86 @@ pub async fn create_share( Json(req): Json, ) -> impl IntoResponse { if let Err(errors) = req.validate() { - return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ - "error": "validation failed", - "details": errors.to_string() - }))).into_response(); + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ + "error": "validation failed", + "details": errors.to_string() + })), + ) + .into_response(); } - + // Find target user by email let target_user = match state.db.find_user_by_email(&req.target_user_email).await { Ok(Some(user)) => user, Ok(None) => { - return (StatusCode::NOT_FOUND, Json(serde_json::json!({ - "error": "target user not found" - }))).into_response(); + return ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ + "error": "target user not found" + })), + ) + .into_response(); } Err(e) => { tracing::error!("Failed to find target user: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "database error" - }))).into_response(); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "database error" + })), + ) + .into_response(); } }; - + let target_user_id = match target_user.id { Some(id) => id, None => { - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "target user has no ID" - }))).into_response(); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "target user has no ID" + })), + ) + .into_response(); } }; - + let owner_id = match ObjectId::parse_str(&claims.sub) { Ok(id) => id, Err(_) => { - return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ - "error": "invalid user ID format" - }))).into_response(); + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ + "error": "invalid user ID format" + })), + ) + .into_response(); } }; - + // Parse resource_id if provided let resource_id = match req.resource_id { - Some(id) => { - match ObjectId::parse_str(&id) { - Ok(oid) => Some(oid), - Err(_) => { - return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ + Some(id) => match ObjectId::parse_str(&id) { + Ok(oid) => Some(oid), + Err(_) => { + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "invalid resource_id format" - }))).into_response(); - } + })), + ) + .into_response(); } }, None => None, }; - + // Parse permissions - support all permission types - let permissions: Vec = req.permissions + let permissions: Vec = req + .permissions .into_iter() .filter_map(|p| match p.to_lowercase().as_str() { "read" => Some(Permission::Read), @@ -127,18 +153,18 @@ pub async fn create_share( _ => None, }) .collect(); - + if permissions.is_empty() { return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "at least one valid permission is required (read, write, delete, share, admin)" }))).into_response(); } - + // Calculate expiration let expires_at = req.expires_days.map(|days| { mongodb::bson::DateTime::now().saturating_add_millis(days as i64 * 24 * 60 * 60 * 1000) }); - + let share = Share::new( owner_id.clone(), target_user_id, @@ -147,24 +173,32 @@ pub async fn create_share( permissions, expires_at, ); - + match state.db.create_share(&share).await { Ok(_) => { let response: ShareResponse = match share.try_into() { Ok(r) => r, Err(_) => { - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to create share response" - }))).into_response(); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to create share response" + })), + ) + .into_response(); } }; (StatusCode::CREATED, Json(response)).into_response() } Err(e) => { tracing::error!("Failed to create share: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to create share" - }))).into_response() + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to create share" + })), + ) + .into_response() } } } @@ -174,7 +208,7 @@ pub async fn list_shares( Extension(claims): Extension, ) -> impl IntoResponse { let user_id = &claims.sub; - + match state.db.list_shares_for_user(user_id).await { Ok(shares) => { let responses: Vec = shares @@ -185,39 +219,50 @@ pub async fn list_shares( } Err(e) => { tracing::error!("Failed to list shares: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to list shares" - }))).into_response() + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to list shares" + })), + ) + .into_response() } } } -pub async fn get_share( - State(state): State, - Path(id): Path, -) -> impl IntoResponse { +pub async fn get_share(State(state): State, Path(id): Path) -> impl IntoResponse { match state.db.get_share(&id).await { Ok(Some(share)) => { let response: ShareResponse = match share.try_into() { Ok(r) => r, Err(_) => { - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to create share response" - }))).into_response(); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to create share response" + })), + ) + .into_response(); } }; (StatusCode::OK, Json(response)).into_response() } - Ok(None) => { - (StatusCode::NOT_FOUND, Json(serde_json::json!({ + Ok(None) => ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": "share not found" - }))).into_response() - } + })), + ) + .into_response(), Err(e) => { tracing::error!("Failed to get share: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to get share" - }))).into_response() + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to get share" + })), + ) + .into_response() } } } @@ -241,34 +286,50 @@ pub async fn update_share( let mut share = match state.db.get_share(&id).await { Ok(Some(s)) => s, Ok(None) => { - return (StatusCode::NOT_FOUND, Json(serde_json::json!({ - "error": "share not found" - }))).into_response(); + return ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ + "error": "share not found" + })), + ) + .into_response(); } Err(e) => { tracing::error!("Failed to get share: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to get share" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to get share" + })), + ) + .into_response(); } }; - + // Verify ownership let owner_id = match ObjectId::parse_str(&claims.sub) { Ok(id) => id, Err(_) => { - return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ - "error": "invalid user ID format" - }))).into_response(); + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ + "error": "invalid user ID format" + })), + ) + .into_response(); } }; - + if share.owner_id != owner_id { - return (StatusCode::FORBIDDEN, Json(serde_json::json!({ - "error": "not authorized to modify this share" - }))).into_response(); + return ( + StatusCode::FORBIDDEN, + Json(serde_json::json!({ + "error": "not authorized to modify this share" + })), + ) + .into_response(); } - + // Update fields if let Some(permissions) = req.permissions { share.permissions = permissions @@ -283,32 +344,42 @@ pub async fn update_share( }) .collect(); } - + if let Some(active) = req.active { share.active = active; } - + if let Some(days) = req.expires_days { - share.expires_at = Some(mongodb::bson::DateTime::now().saturating_add_millis(days as i64 * 24 * 60 * 60 * 1000)); + share.expires_at = Some( + mongodb::bson::DateTime::now().saturating_add_millis(days as i64 * 24 * 60 * 60 * 1000), + ); } - + match state.db.update_share(&share).await { Ok(_) => { let response: ShareResponse = match share.try_into() { Ok(r) => r, Err(_) => { - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to create share response" - }))).into_response(); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to create share response" + })), + ) + .into_response(); } }; (StatusCode::OK, Json(response)).into_response() } Err(e) => { tracing::error!("Failed to update share: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to update share" - }))).into_response() + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to update share" + })), + ) + .into_response() } } } @@ -322,41 +393,61 @@ pub async fn delete_share( let share = match state.db.get_share(&id).await { Ok(Some(s)) => s, Ok(None) => { - return (StatusCode::NOT_FOUND, Json(serde_json::json!({ - "error": "share not found" - }))).into_response() + return ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ + "error": "share not found" + })), + ) + .into_response() } Err(e) => { tracing::error!("Failed to get share: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to get share" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to get share" + })), + ) + .into_response(); } }; - + // Verify ownership let owner_id = match ObjectId::parse_str(&claims.sub) { Ok(id) => id, Err(_) => { - return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ - "error": "invalid user ID format" - }))).into_response(); + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ + "error": "invalid user ID format" + })), + ) + .into_response(); } }; - + if share.owner_id != owner_id { - return (StatusCode::FORBIDDEN, Json(serde_json::json!({ - "error": "not authorized to delete this share" - }))).into_response() + return ( + StatusCode::FORBIDDEN, + Json(serde_json::json!({ + "error": "not authorized to delete this share" + })), + ) + .into_response(); } - + match state.db.delete_share(&id).await { Ok(_) => (StatusCode::NO_CONTENT, ()).into_response(), Err(e) => { tracing::error!("Failed to delete share: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to delete share" - }))).into_response() + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to delete share" + })), + ) + .into_response() } } } diff --git a/backend/src/handlers/users.rs b/backend/src/handlers/users.rs index 9ef4e85..a20801c 100644 --- a/backend/src/handlers/users.rs +++ b/backend/src/handlers/users.rs @@ -1,19 +1,9 @@ -use axum::{ - extract::{State}, - http::StatusCode, - response::IntoResponse, - Json, - Extension, -}; +use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, Json}; +use mongodb::bson::oid::ObjectId; use serde::{Deserialize, Serialize}; use validator::Validate; -use mongodb::bson::oid::ObjectId; -use crate::{ - auth::jwt::Claims, - config::AppState, - models::user::User, -}; +use crate::{auth::jwt::Claims, config::AppState, models::user::User}; #[derive(Debug, Serialize)] pub struct UserProfileResponse { @@ -27,7 +17,7 @@ pub struct UserProfileResponse { impl TryFrom for UserProfileResponse { type Error = anyhow::Error; - + fn try_from(user: User) -> Result { Ok(Self { id: user.id.map(|id| id.to_string()).unwrap_or_default(), @@ -51,22 +41,28 @@ pub async fn get_profile( Extension(claims): Extension, ) -> impl IntoResponse { let user_id = ObjectId::parse_str(&claims.sub).unwrap(); - + match state.db.find_user_by_id(&user_id).await { Ok(Some(user)) => { let response: UserProfileResponse = user.try_into().unwrap(); (StatusCode::OK, Json(response)).into_response() } - Ok(None) => { - (StatusCode::NOT_FOUND, Json(serde_json::json!({ + Ok(None) => ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": "user not found" - }))).into_response() - } + })), + ) + .into_response(), Err(e) => { tracing::error!("Failed to get user profile: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to get profile" - }))).into_response() + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to get profile" + })), + ) + .into_response() } } } @@ -77,33 +73,45 @@ pub async fn update_profile( Json(req): Json, ) -> impl IntoResponse { if let Err(errors) = req.validate() { - return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ - "error": "validation failed", - "details": errors.to_string() - }))).into_response(); + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ + "error": "validation failed", + "details": errors.to_string() + })), + ) + .into_response(); } - + let user_id = ObjectId::parse_str(&claims.sub).unwrap(); - + let mut user = match state.db.find_user_by_id(&user_id).await { Ok(Some(u)) => u, Ok(None) => { - return (StatusCode::NOT_FOUND, Json(serde_json::json!({ - "error": "user not found" - }))).into_response() + return ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ + "error": "user not found" + })), + ) + .into_response() } Err(e) => { tracing::error!("Failed to get user: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "database error" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "database error" + })), + ) + .into_response(); } }; - + if let Some(username) = req.username { user.username = username; } - + match state.db.update_user(&user).await { Ok(_) => { let response: UserProfileResponse = user.try_into().unwrap(); @@ -111,9 +119,13 @@ pub async fn update_profile( } Err(e) => { tracing::error!("Failed to update user: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to update profile" - }))).into_response() + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to update profile" + })), + ) + .into_response() } } } @@ -123,14 +135,18 @@ pub async fn delete_account( Extension(claims): Extension, ) -> impl IntoResponse { let user_id = ObjectId::parse_str(&claims.sub).unwrap(); - + match state.db.delete_user(&user_id).await { Ok(_) => (StatusCode::NO_CONTENT, ()).into_response(), Err(e) => { tracing::error!("Failed to delete user: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to delete account" - }))).into_response() + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to delete account" + })), + ) + .into_response() } } } @@ -149,63 +165,91 @@ pub async fn change_password( Json(req): Json, ) -> impl IntoResponse { if let Err(errors) = req.validate() { - return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ - "error": "validation failed", - "details": errors.to_string() - }))).into_response(); + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ + "error": "validation failed", + "details": errors.to_string() + })), + ) + .into_response(); } - + let user_id = ObjectId::parse_str(&claims.sub).unwrap(); - + let mut user = match state.db.find_user_by_id(&user_id).await { Ok(Some(u)) => u, Ok(None) => { - return (StatusCode::NOT_FOUND, Json(serde_json::json!({ - "error": "user not found" - }))).into_response() + return ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ + "error": "user not found" + })), + ) + .into_response() } Err(e) => { tracing::error!("Failed to get user: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "database error" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "database error" + })), + ) + .into_response(); } }; - + // Verify current password match user.verify_password(&req.current_password) { - Ok(true) => {}, + Ok(true) => {} Ok(false) => { - return (StatusCode::UNAUTHORIZED, Json(serde_json::json!({ - "error": "current password is incorrect" - }))).into_response() + return ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({ + "error": "current password is incorrect" + })), + ) + .into_response() } Err(e) => { tracing::error!("Failed to verify password: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to verify password" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to verify password" + })), + ) + .into_response(); } } - + // Update password match user.update_password(req.new_password) { - Ok(_) => {}, + Ok(_) => {} Err(e) => { tracing::error!("Failed to hash new password: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to update password" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to update password" + })), + ) + .into_response(); } } - + match state.db.update_user(&user).await { Ok(_) => (StatusCode::NO_CONTENT, ()).into_response(), Err(e) => { tracing::error!("Failed to update user: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to update password" - }))).into_response() + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to update password" + })), + ) + .into_response() } } } @@ -230,22 +274,28 @@ pub async fn get_settings( Extension(claims): Extension, ) -> impl IntoResponse { let user_id = ObjectId::parse_str(&claims.sub).unwrap(); - + match state.db.find_user_by_id(&user_id).await { Ok(Some(user)) => { let response: UserSettingsResponse = user.into(); (StatusCode::OK, Json(response)).into_response() } - Ok(None) => { - (StatusCode::NOT_FOUND, Json(serde_json::json!({ + Ok(None) => ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": "user not found" - }))).into_response() - } + })), + ) + .into_response(), Err(e) => { tracing::error!("Failed to get user: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to get settings" - }))).into_response() + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to get settings" + })), + ) + .into_response() } } } @@ -261,29 +311,37 @@ pub async fn update_settings( Json(req): Json, ) -> impl IntoResponse { let user_id = ObjectId::parse_str(&claims.sub).unwrap(); - + let mut user = match state.db.find_user_by_id(&user_id).await { Ok(Some(u)) => u, Ok(None) => { - return (StatusCode::NOT_FOUND, Json(serde_json::json!({ - "error": "user not found" - }))).into_response() + return ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ + "error": "user not found" + })), + ) + .into_response() } Err(e) => { tracing::error!("Failed to get user: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "database error" - }))).into_response() + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "database error" + })), + ) + .into_response(); } }; - + if let Some(recovery_enabled) = req.recovery_enabled { if !recovery_enabled { user.remove_recovery_phrase(); } // Note: Enabling recovery requires a separate endpoint to set the phrase } - + match state.db.update_user(&user).await { Ok(_) => { let response: UserSettingsResponse = user.into(); @@ -291,9 +349,13 @@ pub async fn update_settings( } Err(e) => { tracing::error!("Failed to update user: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ - "error": "failed to update settings" - }))).into_response() + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "failed to update settings" + })), + ) + .into_response() } } } diff --git a/backend/src/main.rs b/backend/src/main.rs index bdc25c8..e161e6d 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,49 +1,49 @@ +mod auth; mod config; mod db; -mod models; -mod auth; mod handlers; mod middleware; +mod models; mod security; mod services; use axum::{ - routing::{get, post, put, delete}, + routing::{delete, get, post, put}, Router, }; -use tower::ServiceBuilder; -use tower_http::{ - cors::CorsLayer, - trace::TraceLayer, -}; use config::Config; use std::sync::Arc; +use tower::ServiceBuilder; +use tower_http::{cors::CorsLayer, trace::TraceLayer}; #[tokio::main] async fn main() -> anyhow::Result<()> { eprintln!("NORMOGEN BACKEND STARTING..."); eprintln!("Loading environment variables..."); - + match dotenv::dotenv() { Ok(path) => eprintln!("Loaded .env from: {:?}", path), Err(e) => eprintln!("No .env file found (this is OK in Docker): {}", e), } - + eprintln!("Initializing logging..."); tracing_subscriber::fmt::init(); tracing::info!("Starting Normogen backend server"); - + // Load configuration let config = Config::from_env()?; eprintln!("Configuration loaded successfully"); - + // Connect to MongoDB tracing::info!("Connecting to MongoDB at {}", config.database.uri); eprintln!("Connecting to MongoDB..."); let db = match db::MongoDb::new(&config.database.uri, &config.database.database).await { Ok(db) => { - tracing::info!("Connected to MongoDB database: {}", config.database.database); + tracing::info!( + "Connected to MongoDB database: {}", + config.database.database + ); eprintln!("MongoDB connection successful"); db } @@ -52,7 +52,7 @@ async fn main() -> anyhow::Result<()> { return Err(e); } }; - + match db.health_check().await { Ok(_) => { tracing::info!("MongoDB health check: OK"); @@ -73,13 +73,13 @@ async fn main() -> anyhow::Result<()> { // Initialize security services (Phase 2.6) let audit_logger = security::AuditLogger::new(&database); let session_manager = security::SessionManager::new(&database); - + // Create account lockout service with reasonable defaults let user_collection = database.collection("users"); let account_lockout = security::AccountLockout::new( user_collection, - 5, // max_attempts - 15, // base_duration_minutes + 5, // max_attempts + 15, // base_duration_minutes 1440, // max_duration_minutes (24 hours) ); @@ -102,17 +102,23 @@ async fn main() -> anyhow::Result<()> { mongo_client: None, interaction_service: Some(interaction_service), }; - + eprintln!("Building router with security middleware..."); - + // Build public routes (no auth required) let public_routes = Router::new() - .route("/health", get(handlers::health_check).head(handlers::health_check)) + .route( + "/health", + get(handlers::health_check).head(handlers::health_check), + ) .route("/ready", get(handlers::ready_check)) .route("/api/auth/register", post(handlers::register)) .route("/api/auth/login", post(handlers::login)) - .route("/api/auth/recover-password", post(handlers::recover_password)); - + .route( + "/api/auth/recover-password", + post(handlers::recover_password), + ); + // Build protected routes (auth required) let protected_routes = Router::new() // User profile management @@ -163,7 +169,7 @@ async fn main() -> anyhow::Result<()> { state.clone(), middleware::jwt_auth_middleware )); - + let app = public_routes .merge(protected_routes) .with_state(state) @@ -186,8 +192,8 @@ async fn main() -> anyhow::Result<()> { let listener = tokio::net::TcpListener::bind(&addr).await?; eprintln!("Server listening on {}", &addr); tracing::info!("Server listening on {}", &addr); - + axum::serve(listener, app).await?; - + Ok(()) } diff --git a/backend/src/middleware/auth.rs b/backend/src/middleware/auth.rs index 46b19ea..2be9ae4 100644 --- a/backend/src/middleware/auth.rs +++ b/backend/src/middleware/auth.rs @@ -1,11 +1,11 @@ +use crate::auth::jwt::Claims; +use crate::config::AppState; use axum::{ extract::{Request, State}, http::StatusCode, middleware::Next, response::Response, }; -use crate::auth::jwt::Claims; -use crate::config::AppState; pub async fn jwt_auth_middleware( State(state): State, @@ -13,29 +13,29 @@ pub async fn jwt_auth_middleware( next: Next, ) -> Result { let headers = req.headers(); - + // Extract Authorization header let auth_header = headers .get("Authorization") .and_then(|h| h.to_str().ok()) .ok_or(StatusCode::UNAUTHORIZED)?; - + // Check Bearer token format if !auth_header.starts_with("Bearer ") { return Err(StatusCode::UNAUTHORIZED); } - + let token = &auth_header[7..]; // Remove "Bearer " prefix - + // Verify token let claims = state .jwt_service .validate_token(token) .map_err(|_| StatusCode::UNAUTHORIZED)?; - + // Add claims to request extensions for handlers to use req.extensions_mut().insert(claims); - + Ok(next.run(req).await) } diff --git a/backend/src/middleware/mod.rs b/backend/src/middleware/mod.rs index 96cb145..f49bb9d 100644 --- a/backend/src/middleware/mod.rs +++ b/backend/src/middleware/mod.rs @@ -1,8 +1,8 @@ pub mod auth; pub mod rate_limit; -pub use rate_limit::general_rate_limit_middleware; pub use auth::jwt_auth_middleware; +pub use rate_limit::general_rate_limit_middleware; // Simple security headers middleware pub async fn security_headers_middleware( @@ -10,13 +10,19 @@ pub async fn security_headers_middleware( next: axum::middleware::Next, ) -> axum::response::Response { let mut response = next.run(req).await; - + let headers = response.headers_mut(); headers.insert("X-Content-Type-Options", "nosniff".parse().unwrap()); headers.insert("X-Frame-Options", "DENY".parse().unwrap()); headers.insert("X-XSS-Protection", "1; mode=block".parse().unwrap()); - headers.insert("Strict-Transport-Security", "max-age=31536000; includeSubDomains".parse().unwrap()); - headers.insert("Content-Security-Policy", "default-src 'self'".parse().unwrap()); - + headers.insert( + "Strict-Transport-Security", + "max-age=31536000; includeSubDomains".parse().unwrap(), + ); + headers.insert( + "Content-Security-Policy", + "default-src 'self'".parse().unwrap(), + ); + response } diff --git a/backend/src/middleware/rate_limit.rs b/backend/src/middleware/rate_limit.rs index a856a4c..b10f95e 100644 --- a/backend/src/middleware/rate_limit.rs +++ b/backend/src/middleware/rate_limit.rs @@ -1,9 +1,4 @@ -use axum::{ - extract::Request, - http::StatusCode, - middleware::Next, - response::Response, -}; +use axum::{extract::Request, http::StatusCode, middleware::Next, response::Response}; /// Middleware for general rate limiting /// NOTE: Currently a stub implementation. TODO: Implement IP-based rate limiting @@ -18,10 +13,7 @@ pub async fn general_rate_limit_middleware( /// Middleware for auth endpoint rate limiting /// NOTE: Currently a stub implementation. TODO: Implement IP-based rate limiting -pub async fn auth_rate_limit_middleware( - req: Request, - next: Next, -) -> Result { +pub async fn auth_rate_limit_middleware(req: Request, next: Next) -> Result { // TODO: Implement proper rate limiting with IP-based tracking // For now, just pass through Ok(next.run(req).await) diff --git a/backend/src/models/audit_log.rs b/backend/src/models/audit_log.rs index cacf058..0704717 100644 --- a/backend/src/models/audit_log.rs +++ b/backend/src/models/audit_log.rs @@ -1,10 +1,10 @@ -use mongodb::{ - Collection, - bson::{doc, oid::ObjectId}, -}; -use futures::stream::TryStreamExt; -use serde::{Deserialize, Serialize}; use anyhow::Result; +use futures::stream::TryStreamExt; +use mongodb::{ + bson::{doc, oid::ObjectId}, + Collection, +}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum AuditEventType { @@ -87,7 +87,8 @@ impl AuditLogRepository { } pub async fn find_by_user(&self, user_id: &ObjectId) -> Result> { - let cursor = self.collection + let cursor = self + .collection .find( doc! { "user_id": user_id @@ -102,15 +103,13 @@ impl AuditLogRepository { pub async fn find_recent(&self, limit: u64) -> Result> { use mongodb::options::FindOptions; - + let opts = FindOptions::builder() .sort(doc! { "timestamp": -1 }) .limit(limit as i64) .build(); - let cursor = self.collection - .find(doc! {}, opts) - .await?; + let cursor = self.collection.find(doc! {}, opts).await?; let logs: Vec = cursor.try_collect().await?; Ok(logs) diff --git a/backend/src/models/family.rs b/backend/src/models/family.rs index cd2d4f1..9aa9a90 100644 --- a/backend/src/models/family.rs +++ b/backend/src/models/family.rs @@ -1,5 +1,8 @@ +use mongodb::{ + bson::{doc, oid::ObjectId, DateTime}, + Collection, +}; use serde::{Deserialize, Serialize}; -use mongodb::{bson::{doc, oid::ObjectId, DateTime}, Collection}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Family { @@ -29,13 +32,16 @@ impl FamilyRepository { pub fn new(collection: Collection) -> Self { Self { collection } } - + pub async fn create(&self, family: &Family) -> mongodb::error::Result<()> { self.collection.insert_one(family, None).await?; Ok(()) } - - pub async fn find_by_family_id(&self, family_id: &str) -> mongodb::error::Result> { + + pub async fn find_by_family_id( + &self, + family_id: &str, + ) -> mongodb::error::Result> { self.collection .find_one(doc! { "familyId": family_id }, None) .await diff --git a/backend/src/models/health_data.rs b/backend/src/models/health_data.rs index 0242da2..f55efe4 100644 --- a/backend/src/models/health_data.rs +++ b/backend/src/models/health_data.rs @@ -1,5 +1,5 @@ -use serde::{Deserialize, Serialize}; use mongodb::bson::{oid::ObjectId, DateTime}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HealthData { diff --git a/backend/src/models/health_stats.rs b/backend/src/models/health_stats.rs index 4bef19d..3f12520 100644 --- a/backend/src/models/health_stats.rs +++ b/backend/src/models/health_stats.rs @@ -1,7 +1,10 @@ -use mongodb::Collection; -use serde::{Deserialize, Serialize}; -use mongodb::{bson::{oid::ObjectId, doc}, error::Error as MongoError}; use futures::stream::TryStreamExt; +use mongodb::Collection; +use mongodb::{ + bson::{doc, oid::ObjectId}, + error::Error as MongoError, +}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HealthStatistic { @@ -46,7 +49,11 @@ impl HealthStatisticsRepository { self.collection.find_one(filter, None).await } - pub async fn update(&self, id: &ObjectId, stat: &HealthStatistic) -> Result, MongoError> { + pub async fn update( + &self, + id: &ObjectId, + stat: &HealthStatistic, + ) -> Result, MongoError> { let filter = doc! { "_id": id }; self.collection.replace_one(filter, stat, None).await?; Ok(Some(stat.clone())) diff --git a/backend/src/models/interactions.rs b/backend/src/models/interactions.rs index 0d80592..f4b526e 100644 --- a/backend/src/models/interactions.rs +++ b/backend/src/models/interactions.rs @@ -1,9 +1,9 @@ //! Interaction Models -//! +//! //! Database models for drug interactions -use serde::{Deserialize, Serialize}; use mongodb::bson::{oid::ObjectId, DateTime}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DrugInteraction { diff --git a/backend/src/models/lab_result.rs b/backend/src/models/lab_result.rs index cbab5e7..50f179f 100644 --- a/backend/src/models/lab_result.rs +++ b/backend/src/models/lab_result.rs @@ -1,6 +1,6 @@ -use serde::{Deserialize, Serialize}; -use mongodb::{bson::oid::ObjectId, Collection}; use futures::stream::TryStreamExt; +use mongodb::{bson::oid::ObjectId, Collection}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LabResult { @@ -41,12 +41,18 @@ impl LabResultRepository { Self { collection } } - pub async fn create(&self, lab_result: LabResult) -> Result> { + pub async fn create( + &self, + lab_result: LabResult, + ) -> Result> { self.collection.insert_one(lab_result.clone(), None).await?; Ok(lab_result) } - pub async fn list_by_user(&self, user_id: &str) -> Result, Box> { + pub async fn list_by_user( + &self, + user_id: &str, + ) -> Result, Box> { let filter = mongodb::bson::doc! { "userId": user_id }; diff --git a/backend/src/models/medication.rs b/backend/src/models/medication.rs index 1afedfb..ff70bd4 100644 --- a/backend/src/models/medication.rs +++ b/backend/src/models/medication.rs @@ -1,8 +1,8 @@ -use serde::{Deserialize, Serialize}; -use mongodb::bson::{oid::ObjectId, DateTime, doc}; -use mongodb::Collection; -use futures::stream::StreamExt; use super::health_data::EncryptedField; +use futures::stream::StreamExt; +use mongodb::bson::{doc, oid::ObjectId, DateTime}; +use mongodb::Collection; +use serde::{Deserialize, Serialize}; // ============================================================================ // PILL IDENTIFICATION (Phase 2.8) @@ -14,11 +14,11 @@ pub struct PillIdentification { /// Size of the pill (optional) #[serde(skip_serializing_if = "Option::is_none")] pub size: Option, - + /// Shape of the pill (optional) #[serde(skip_serializing_if = "Option::is_none")] pub shape: Option, - + /// Color of the pill (optional) #[serde(skip_serializing_if = "Option::is_none")] pub color: Option, @@ -27,11 +27,11 @@ pub struct PillIdentification { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum PillSize { - Tiny, // < 5mm - Small, // 5-10mm - Medium, // 10-15mm - Large, // 15-20mm - ExtraLarge,// > 20mm + Tiny, // < 5mm + Small, // 5-10mm + Medium, // 10-15mm + Large, // 15-20mm + ExtraLarge, // > 20mm #[serde(rename = "custom")] Custom(String), } @@ -114,7 +114,7 @@ pub struct Medication { pub created_at: DateTime, #[serde(rename = "updatedAt")] pub updated_at: DateTime, - + /// Physical pill identification (Phase 2.8 - optional) #[serde(skip_serializing_if = "Option::is_none")] pub pill_identification: Option, @@ -167,7 +167,7 @@ pub struct CreateMedicationRequest { pub tags: Option>, pub reminder_times: Option>, pub profile_id: String, - + /// Pill identification (Phase 2.8 - optional) #[serde(rename = "pill_identification")] pub pill_identification: Option, @@ -189,7 +189,7 @@ pub struct UpdateMedicationRequest { pub notes: Option, pub tags: Option>, pub reminder_times: Option>, - + /// Pill identification (Phase 2.8 - optional) #[serde(rename = "pill_identification")] pub pill_identification: Option, @@ -216,13 +216,19 @@ impl MedicationRepository { pub fn new(collection: Collection) -> Self { Self { collection } } - - pub async fn create(&self, medication: Medication) -> Result> { + + pub async fn create( + &self, + medication: Medication, + ) -> Result> { let _result = self.collection.insert_one(medication.clone(), None).await?; Ok(medication) } - - pub async fn find_by_user(&self, user_id: &str) -> Result, Box> { + + pub async fn find_by_user( + &self, + user_id: &str, + ) -> Result, Box> { let filter = doc! { "userId": user_id }; let mut cursor = self.collection.find(filter, None).await?; let mut medications = Vec::new(); @@ -231,9 +237,13 @@ impl MedicationRepository { } Ok(medications) } - - pub async fn find_by_user_and_profile(&self, user_id: &str, profile_id: &str) -> Result, Box> { - let filter = doc! { + + pub async fn find_by_user_and_profile( + &self, + user_id: &str, + profile_id: &str, + ) -> Result, Box> { + let filter = doc! { "userId": user_id, "profileId": profile_id }; @@ -244,16 +254,23 @@ impl MedicationRepository { } Ok(medications) } - - pub async fn find_by_id(&self, id: &ObjectId) -> Result, Box> { + + pub async fn find_by_id( + &self, + id: &ObjectId, + ) -> Result, Box> { let filter = doc! { "_id": id }; let medication = self.collection.find_one(filter, None).await?; Ok(medication) } - - pub async fn update(&self, id: &ObjectId, updates: UpdateMedicationRequest) -> Result, Box> { + + pub async fn update( + &self, + id: &ObjectId, + updates: UpdateMedicationRequest, + ) -> Result, Box> { let mut update_doc = doc! {}; - + if let Some(name) = updates.name { update_doc.insert("medicationData.name", name); } @@ -301,21 +318,28 @@ impl MedicationRepository { update_doc.insert("pillIdentification", pill_doc); } } - + update_doc.insert("updatedAt", mongodb::bson::DateTime::now()); - + let filter = doc! { "_id": id }; - let medication = self.collection.find_one_and_update(filter, doc! { "$set": update_doc }, None).await?; + let medication = self + .collection + .find_one_and_update(filter, doc! { "$set": update_doc }, None) + .await?; Ok(medication) } - + pub async fn delete(&self, id: &ObjectId) -> Result> { let filter = doc! { "_id": id }; let result = self.collection.delete_one(filter, None).await?; Ok(result.deleted_count > 0) } - - pub async fn calculate_adherence(&self, medication_id: &str, days: i64) -> Result> { + + pub async fn calculate_adherence( + &self, + medication_id: &str, + days: i64, + ) -> Result> { // For now, return a placeholder adherence calculation // In a full implementation, this would query the medication_doses collection Ok(AdherenceStats { diff --git a/backend/src/models/mod.rs b/backend/src/models/mod.rs index 5ec2e6c..c5dba69 100644 --- a/backend/src/models/mod.rs +++ b/backend/src/models/mod.rs @@ -2,6 +2,7 @@ pub mod audit_log; pub mod family; pub mod health_data; pub mod health_stats; +pub mod interactions; pub mod lab_result; pub mod medication; pub mod permission; @@ -10,4 +11,3 @@ pub mod refresh_token; pub mod session; pub mod share; pub mod user; -pub mod interactions; diff --git a/backend/src/models/permission.rs b/backend/src/models/permission.rs index 5f5d5b6..db0513c 100644 --- a/backend/src/models/permission.rs +++ b/backend/src/models/permission.rs @@ -27,15 +27,15 @@ impl Permission { pub fn can_read(&self) -> bool { matches!(self, Self::Read | Self::Admin) } - + pub fn can_write(&self) -> bool { matches!(self, Self::Write | Self::Admin) } - + pub fn can_delete(&self) -> bool { matches!(self, Self::Delete | Self::Admin) } - + pub fn can_share(&self) -> bool { matches!(self, Self::Share | Self::Admin) } diff --git a/backend/src/models/profile.rs b/backend/src/models/profile.rs index 1def644..9f828ce 100644 --- a/backend/src/models/profile.rs +++ b/backend/src/models/profile.rs @@ -1,5 +1,8 @@ +use mongodb::{ + bson::{doc, oid::ObjectId, DateTime}, + Collection, +}; use serde::{Deserialize, Serialize}; -use mongodb::{bson::{doc, oid::ObjectId, DateTime}, Collection}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Profile { @@ -35,13 +38,16 @@ impl ProfileRepository { pub fn new(collection: Collection) -> Self { Self { collection } } - + pub async fn create(&self, profile: &Profile) -> mongodb::error::Result<()> { self.collection.insert_one(profile, None).await?; Ok(()) } - - pub async fn find_by_profile_id(&self, profile_id: &str) -> mongodb::error::Result> { + + pub async fn find_by_profile_id( + &self, + profile_id: &str, + ) -> mongodb::error::Result> { self.collection .find_one(doc! { "profileId": profile_id }, None) .await diff --git a/backend/src/models/refresh_token.rs b/backend/src/models/refresh_token.rs index e50dfaf..789e7e1 100644 --- a/backend/src/models/refresh_token.rs +++ b/backend/src/models/refresh_token.rs @@ -1,5 +1,5 @@ -use serde::{Deserialize, Serialize}; use mongodb::bson::{oid::ObjectId, DateTime}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RefreshToken { diff --git a/backend/src/models/session.rs b/backend/src/models/session.rs index 0f0407b..e26272a 100644 --- a/backend/src/models/session.rs +++ b/backend/src/models/session.rs @@ -1,16 +1,16 @@ -use mongodb::{ - Collection, - bson::{doc, oid::ObjectId}, -}; -use futures::stream::TryStreamExt; -use serde::{Deserialize, Serialize}; use anyhow::Result; +use futures::stream::TryStreamExt; +use mongodb::{ + bson::{doc, oid::ObjectId}, + Collection, +}; +use serde::{Deserialize, Serialize}; use std::time::SystemTime; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeviceInfo { - pub device_type: String, // "mobile", "desktop", "tablet" - pub os: String, // "iOS", "Android", "Windows", "macOS", "Linux" + pub device_type: String, // "mobile", "desktop", "tablet" + pub os: String, // "iOS", "Android", "Windows", "macOS", "Linux" pub browser: Option, pub ip_address: String, } @@ -21,7 +21,7 @@ pub struct Session { pub id: Option, pub user_id: ObjectId, pub device_info: DeviceInfo, - pub token_hash: String, // Hash of the JWT token + pub token_hash: String, // Hash of the JWT token pub created_at: mongodb::bson::DateTime, pub last_used_at: mongodb::bson::DateTime, pub expires_at: mongodb::bson::DateTime, @@ -48,7 +48,7 @@ impl SessionRepository { ) -> Result { let now = SystemTime::now(); let now_bson = mongodb::bson::DateTime::from(now); - + let expires_at = SystemTime::now() .checked_add(std::time::Duration::from_secs(duration_hours as u64 * 3600)) .ok_or_else(|| anyhow::anyhow!("Invalid duration"))?; @@ -65,11 +65,17 @@ impl SessionRepository { is_revoked: false, }; - self.collection.insert_one(session, None).await?.inserted_id.as_object_id().ok_or_else(|| anyhow::anyhow!("Failed to get inserted id")) + self.collection + .insert_one(session, None) + .await? + .inserted_id + .as_object_id() + .ok_or_else(|| anyhow::anyhow!("Failed to get inserted id")) } pub async fn find_by_user(&self, user_id: &ObjectId) -> Result> { - let cursor = self.collection + let cursor = self + .collection .find( doc! { "user_id": user_id, @@ -98,7 +104,7 @@ impl SessionRepository { pub async fn revoke_all_for_user(&self, user_id: &ObjectId) -> Result<()> { self.collection .update_many( - doc! { + doc! { "user_id": user_id, "is_revoked": false }, @@ -110,7 +116,8 @@ impl SessionRepository { } pub async fn cleanup_expired(&self) -> Result { - let result = self.collection + let result = self + .collection .delete_many( doc! { "expires_at": { "$lt": mongodb::bson::DateTime::now() } diff --git a/backend/src/models/share.rs b/backend/src/models/share.rs index c2762c5..1f9d4e3 100644 --- a/backend/src/models/share.rs +++ b/backend/src/models/share.rs @@ -1,7 +1,7 @@ +use mongodb::bson::DateTime; use mongodb::bson::{doc, oid::ObjectId}; use mongodb::Collection; use serde::{Deserialize, Serialize}; -use mongodb::bson::DateTime; use super::permission::Permission; @@ -42,7 +42,7 @@ impl Share { active: true, } } - + pub fn is_expired(&self) -> bool { if let Some(expires) = self.expires_at { DateTime::now() > expires @@ -50,11 +50,11 @@ impl Share { false } } - + pub fn has_permission(&self, permission: &Permission) -> bool { self.permissions.contains(permission) || self.permissions.contains(&Permission::Admin) } - + pub fn revoke(&mut self) { self.active = false; } @@ -69,19 +69,19 @@ impl ShareRepository { pub fn new(collection: Collection) -> Self { Self { collection } } - + pub async fn create(&self, share: &Share) -> mongodb::error::Result> { let result = self.collection.insert_one(share, None).await?; Ok(Some(result.inserted_id.as_object_id().unwrap())) } - + pub async fn find_by_id(&self, id: &ObjectId) -> mongodb::error::Result> { self.collection.find_one(doc! { "_id": id }, None).await } - + pub async fn find_by_owner(&self, owner_id: &ObjectId) -> mongodb::error::Result> { use futures::stream::TryStreamExt; - + self.collection .find(doc! { "owner_id": owner_id }, None) .await? @@ -89,27 +89,37 @@ impl ShareRepository { .await .map_err(|e| mongodb::error::Error::from(e)) } - - pub async fn find_by_target(&self, target_user_id: &ObjectId) -> mongodb::error::Result> { + + pub async fn find_by_target( + &self, + target_user_id: &ObjectId, + ) -> mongodb::error::Result> { use futures::stream::TryStreamExt; - + self.collection - .find(doc! { "target_user_id": target_user_id, "active": true }, None) + .find( + doc! { "target_user_id": target_user_id, "active": true }, + None, + ) .await? .try_collect() .await .map_err(|e| mongodb::error::Error::from(e)) } - + pub async fn update(&self, share: &Share) -> mongodb::error::Result<()> { if let Some(id) = &share.id { - self.collection.replace_one(doc! { "_id": id }, share, None).await?; + self.collection + .replace_one(doc! { "_id": id }, share, None) + .await?; } Ok(()) } - + pub async fn delete(&self, share_id: &ObjectId) -> mongodb::error::Result<()> { - self.collection.delete_one(doc! { "_id": share_id }, None).await?; + self.collection + .delete_one(doc! { "_id": share_id }, None) + .await?; Ok(()) } } diff --git a/backend/src/models/user.rs b/backend/src/models/user.rs index d5db98d..53b9cf8 100644 --- a/backend/src/models/user.rs +++ b/backend/src/models/user.rs @@ -2,18 +2,18 @@ use mongodb::bson::{doc, oid::ObjectId}; use mongodb::Collection; use serde::{Deserialize, Serialize}; -use mongodb::bson::DateTime; use crate::auth::password::verify_password; +use mongodb::bson::DateTime; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct User { #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] pub id: Option, - + pub email: String, - + pub username: String, - + pub password_hash: String, /// Password recovery phrase hash (zero-knowledge) @@ -54,10 +54,10 @@ impl User { ) -> Result { // Import PasswordService use crate::auth::password::PasswordService; - + // 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)?) @@ -94,7 +94,7 @@ impl User { 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) } @@ -102,7 +102,7 @@ impl User { /// Update the password hash (increments token_version to invalidate all tokens) pub fn update_password(&mut self, new_password: String) -> Result<(), anyhow::Error> { use crate::auth::password::PasswordService; - + self.password_hash = PasswordService::hash_password(&new_password)?; self.token_version += 1; Ok(()) @@ -111,7 +111,7 @@ impl User { /// Set or update the recovery phrase pub fn set_recovery_phrase(&mut self, phrase: String) -> Result<(), anyhow::Error> { use crate::auth::password::PasswordService; - + self.recovery_phrase_hash = Some(PasswordService::hash_password(&phrase)?); self.recovery_enabled = true; Ok(()) @@ -156,13 +156,14 @@ impl UserRepository { /// 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 + 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> { + pub async fn find_by_verification_token( + &self, + token: &str, + ) -> mongodb::error::Result> { self.collection .find_one(doc! { "verification_token": token }, None) .await @@ -177,12 +178,16 @@ impl UserRepository { } /// Update the token version - silently fails if ObjectId is invalid - pub async fn update_token_version(&self, user_id: &str, version: i32) -> mongodb::error::Result<()> { + pub async fn update_token_version( + &self, + user_id: &str, + version: i32, + ) -> mongodb::error::Result<()> { let oid = match ObjectId::parse_str(user_id) { Ok(id) => id, Err(_) => return Ok(()), // Silently fail if invalid ObjectId }; - + self.collection .update_one( doc! { "_id": oid }, @@ -204,7 +209,7 @@ impl UserRepository { /// Update last active timestamp pub async fn update_last_active(&self, user_id: &ObjectId) -> mongodb::error::Result<()> { use mongodb::bson::DateTime; - + let now = DateTime::now(); self.collection .update_one( diff --git a/backend/src/security/account_lockout.rs b/backend/src/security/account_lockout.rs index ac5b8ee..3822a63 100644 --- a/backend/src/security/account_lockout.rs +++ b/backend/src/security/account_lockout.rs @@ -1,8 +1,8 @@ +use anyhow::Result; use mongodb::bson::{doc, DateTime}; use mongodb::Collection; use std::sync::Arc; use tokio::sync::RwLock; -use anyhow::Result; #[derive(Clone)] pub struct AccountLockout { @@ -26,16 +26,11 @@ impl AccountLockout { max_duration_minutes, } } - + pub async fn check_lockout(&self, email: &str) -> Result { let collection = self.user_collection.read().await; - let user = collection - .find_one( - doc! { "email": email }, - None, - ) - .await?; - + let user = collection.find_one(doc! { "email": email }, None).await?; + if let Some(user_doc) = user { if let Some(locked_until_val) = user_doc.get("locked_until") { if let Some(dt) = locked_until_val.as_datetime() { @@ -46,32 +41,28 @@ impl AccountLockout { } } } - + Ok(false) // Account is not locked } - + pub async fn record_failed_attempt(&self, email: &str) -> Result { let collection = self.user_collection.write().await; - + // Get current failed attempts - let user = collection - .find_one( - doc! { "email": email }, - None, - ) - .await?; - + let user = collection.find_one(doc! { "email": email }, None).await?; + let current_attempts = if let Some(user_doc) = user { - user_doc.get("failed_login_attempts") + user_doc + .get("failed_login_attempts") .and_then(|v| v.as_i64()) .unwrap_or(0) as u32 } else { 0 }; - + let new_attempts = current_attempts + 1; let should_lock = new_attempts >= self.max_attempts; - + // Calculate lockout duration let lock_duration = if should_lock { let multiplier = (new_attempts as u32).saturating_sub(self.max_attempts) + 1; @@ -80,7 +71,7 @@ impl AccountLockout { } else { 0 }; - + let locked_until = if lock_duration > 0 { let now = DateTime::now(); let duration_millis = lock_duration as u64 * 60 * 1000; @@ -88,7 +79,7 @@ impl AccountLockout { } else { DateTime::now() }; - + // Update user collection .update_one( @@ -103,13 +94,13 @@ impl AccountLockout { None, ) .await?; - + Ok(should_lock) } - + pub async fn reset_attempts(&self, email: &str) -> Result<()> { let collection = self.user_collection.write().await; - + collection .update_one( doc! { "email": email }, @@ -122,7 +113,7 @@ impl AccountLockout { None, ) .await?; - + Ok(()) } } diff --git a/backend/src/security/audit_logger.rs b/backend/src/security/audit_logger.rs index 7d70ace..23bdde3 100644 --- a/backend/src/security/audit_logger.rs +++ b/backend/src/security/audit_logger.rs @@ -1,6 +1,6 @@ +use crate::models::audit_log::{AuditEventType, AuditLog, AuditLogRepository}; use anyhow::Result; use mongodb::bson::oid::ObjectId; -use crate::models::audit_log::{AuditLog, AuditLogRepository, AuditEventType}; #[derive(Clone)] pub struct AuditLogger { @@ -24,7 +24,14 @@ impl AuditLogger { resource_id: Option, ) -> Result { self.repository - .log(event_type, user_id, email, ip_address, resource_type, resource_id) + .log( + event_type, + user_id, + email, + ip_address, + resource_type, + resource_id, + ) .await } diff --git a/backend/src/security/mod.rs b/backend/src/security/mod.rs index 37736ff..e682707 100644 --- a/backend/src/security/mod.rs +++ b/backend/src/security/mod.rs @@ -1,7 +1,7 @@ +pub mod account_lockout; pub mod audit_logger; pub mod session_manager; -pub mod account_lockout; +pub use account_lockout::AccountLockout; pub use audit_logger::AuditLogger; pub use session_manager::SessionManager; -pub use account_lockout::AccountLockout; diff --git a/backend/src/security/session_manager.rs b/backend/src/security/session_manager.rs index cae6c9d..6945d68 100644 --- a/backend/src/security/session_manager.rs +++ b/backend/src/security/session_manager.rs @@ -1,5 +1,5 @@ +use crate::models::session::{DeviceInfo, Session, SessionRepository}; use anyhow::Result; -use crate::models::session::{Session, SessionRepository, DeviceInfo}; use mongodb::bson::oid::ObjectId; #[derive(Clone)] @@ -21,7 +21,9 @@ impl SessionManager { token_hash: String, duration_hours: i64, ) -> Result { - self.repository.create(user_id, device_info, token_hash, duration_hours).await + self.repository + .create(user_id, device_info, token_hash, duration_hours) + .await } pub async fn get_user_sessions(&self, user_id: &ObjectId) -> Result> { diff --git a/backend/src/services/ingredient_mapper.rs b/backend/src/services/ingredient_mapper.rs index df95913..0e22c5f 100644 --- a/backend/src/services/ingredient_mapper.rs +++ b/backend/src/services/ingredient_mapper.rs @@ -1,7 +1,7 @@ //! Ingredient Mapper Service -//! +//! //! Maps EU drug names to US drug names for interaction checking -//! +//! //! Example: //! - Paracetamol (EU) → Acetaminophen (US) @@ -15,16 +15,16 @@ pub struct IngredientMapper { impl IngredientMapper { pub fn new() -> Self { let mut mappings = HashMap::new(); - + // EU to US drug name mappings mappings.insert("paracetamol".to_string(), "acetaminophen".to_string()); mappings.insert("paracetamolum".to_string(), "acetaminophen".to_string()); mappings.insert("acetylsalicylic acid".to_string(), "aspirin".to_string()); - + // Antibiotics mappings.insert("amoxicilline".to_string(), "amoxicillin".to_string()); mappings.insert("amoxicillinum".to_string(), "amoxicillin".to_string()); - + // These are the same in both mappings.insert("ibuprofen".to_string(), "ibuprofen".to_string()); mappings.insert("metformin".to_string(), "metformin".to_string()); @@ -32,26 +32,25 @@ impl IngredientMapper { mappings.insert("atorvastatin".to_string(), "atorvastatin".to_string()); mappings.insert("simvastatin".to_string(), "simvastatin".to_string()); mappings.insert("omeprazole".to_string(), "omeprazole".to_string()); - + Self { mappings } } - + /// Map EU drug name to US drug name pub fn map_to_us(&self, eu_name: &str) -> String { let normalized = eu_name.to_lowercase().trim().to_string(); - - self.mappings.get(&normalized) + + self.mappings + .get(&normalized) .unwrap_or(&eu_name.to_string()) .clone() } - + /// Map multiple EU drug names to US names pub fn map_many_to_us(&self, eu_names: &[String]) -> Vec { - eu_names.iter() - .map(|name| self.map_to_us(name)) - .collect() + eu_names.iter().map(|name| self.map_to_us(name)).collect() } - + /// Add a custom mapping pub fn add_mapping(&mut self, eu_name: String, us_name: String) { self.mappings.insert(eu_name.to_lowercase(), us_name); @@ -67,19 +66,19 @@ impl Default for IngredientMapper { #[cfg(test)] mod tests { use super::*; - + #[test] fn test_paracetamol_mapping() { let mapper = IngredientMapper::new(); assert_eq!(mapper.map_to_us("paracetamol"), "acetaminophen"); } - + #[test] fn test_same_name() { let mapper = IngredientMapper::new(); assert_eq!(mapper.map_to_us("ibuprofen"), "ibuprofen"); } - + #[test] fn test_case_insensitive() { let mapper = IngredientMapper::new(); diff --git a/backend/src/services/interaction_service.rs b/backend/src/services/interaction_service.rs index 9a00235..cdc7fe7 100644 --- a/backend/src/services/interaction_service.rs +++ b/backend/src/services/interaction_service.rs @@ -1,12 +1,11 @@ //! Interaction Service -//! +//! //! Combines ingredient mapping and OpenFDA interaction checking //! Provides a unified API for checking drug interactions use crate::services::{ - IngredientMapper, - OpenFDAService, - openfda_service::{DrugInteraction, InteractionSeverity} + openfda_service::{DrugInteraction, InteractionSeverity}, + IngredientMapper, OpenFDAService, }; use mongodb::bson::oid::ObjectId; use serde::{Deserialize, Serialize}; @@ -23,56 +22,65 @@ impl InteractionService { fda: OpenFDAService::new(), } } - + /// Check interactions between EU medications /// Maps EU names to US names, then checks interactions pub async fn check_eu_medications( &self, - eu_medications: &[String] + eu_medications: &[String], ) -> Result, Box> { // Step 1: Map EU names to US names let us_medications: Vec = self.mapper.map_many_to_us(eu_medications); - + // Step 2: Check interactions using US names let interactions = self.fda.check_interactions(&us_medications).await?; - + // Step 3: Map back to EU names in response let interactions_mapped: Vec = interactions .into_iter() .map(|mut interaction| { // Map US names back to EU names if they were mapped for (i, eu_name) in eu_medications.iter().enumerate() { - if us_medications.get(i).map(|us| us == &interaction.drug1).unwrap_or(false) { + if us_medications + .get(i) + .map(|us| us == &interaction.drug1) + .unwrap_or(false) + { interaction.drug1 = eu_name.clone(); } - if us_medications.get(i).map(|us| us == &interaction.drug2).unwrap_or(false) { + if us_medications + .get(i) + .map(|us| us == &interaction.drug2) + .unwrap_or(false) + { interaction.drug2 = eu_name.clone(); } } interaction }) .collect(); - + Ok(interactions_mapped) } - + /// Check a single medication against user's current medications pub async fn check_new_medication( &self, new_medication: &str, - existing_medications: &[String] + existing_medications: &[String], ) -> Result> { let all_meds = { let mut meds = vec![new_medication.to_string()]; meds.extend_from_slice(existing_medications); meds }; - + let interactions = self.check_eu_medications(&all_meds).await?; - - let has_severe = interactions.iter() + + let has_severe = interactions + .iter() .any(|i| matches!(i.severity, InteractionSeverity::Severe)); - + Ok(DrugInteractionCheckResult { interactions, has_severe, @@ -97,32 +105,32 @@ pub struct DrugInteractionCheckResult { #[cfg(test)] mod tests { use super::*; - + #[tokio::test] async fn test_paracetamol_warfarin() { let service = InteractionService::new(); - + // Test EU name mapping + interaction check let result = service .check_eu_medications(&["paracetamol".to_string(), "warfarin".to_string()]) .await .unwrap(); - + // Paracetamol maps to acetaminophen, should check against warfarin // (Note: actual interaction depends on our known interactions database) println!("Interactions found: {:?}", result); } - + #[tokio::test] async fn test_new_medication_check() { let service = InteractionService::new(); - + let existing = vec!["warfarin".to_string()]; let result = service .check_new_medication("aspirin", &existing) .await .unwrap(); - + // Warfarin + Aspirin should have severe interaction assert_eq!(result.interactions.len(), 1); assert!(result.has_severe); diff --git a/backend/src/services/mod.rs b/backend/src/services/mod.rs index 8952461..ccf698e 100644 --- a/backend/src/services/mod.rs +++ b/backend/src/services/mod.rs @@ -1,14 +1,14 @@ //! Phase 2.8 Services Module -//! +//! //! This module contains external service integrations: //! - Ingredient Mapper (EU to US drug names) //! - OpenFDA Service (drug interactions) //! - Interaction Checker (combined service) pub mod ingredient_mapper; -pub mod openfda_service; pub mod interaction_service; +pub mod openfda_service; pub use ingredient_mapper::IngredientMapper; -pub use openfda_service::OpenFDAService; pub use interaction_service::InteractionService; +pub use openfda_service::OpenFDAService; diff --git a/backend/tests/auth_tests.rs b/backend/tests/auth_tests.rs index 0426ec9..db28b33 100644 --- a/backend/tests/auth_tests.rs +++ b/backend/tests/auth_tests.rs @@ -6,22 +6,24 @@ const BASE_URL: &str = "http://127.0.0.1:8000"; #[tokio::test] async fn test_health_check() { let client = Client::new(); - let response = client.get(&format!("{}/health", BASE_URL)) + let response = client + .get(&format!("{}/health", BASE_URL)) .send() .await .expect("Failed to send request"); - + assert_eq!(response.status(), 200); } #[tokio::test] async fn test_ready_check() { let client = Client::new(); - let response = client.get(&format!("{}/ready", BASE_URL)) + let response = client + .get(&format!("{}/ready", BASE_URL)) .send() .await .expect("Failed to send request"); - + assert_eq!(response.status(), 200); } @@ -29,7 +31,7 @@ async fn test_ready_check() { async fn test_register_user() { let client = Client::new(); let email = format!("test_{}@example.com", uuid::Uuid::new_v4()); - + let payload = json!({ "email": email, "password_hash": "hashed_password_placeholder", @@ -37,15 +39,16 @@ async fn test_register_user() { "recovery_phrase_iv": "iv_placeholder", "recovery_phrase_auth_tag": "auth_tag_placeholder" }); - - let response = client.post(&format!("{}/api/auth/register", BASE_URL)) + + let response = client + .post(&format!("{}/api/auth/register", BASE_URL)) .json(&payload) .send() .await .expect("Failed to send request"); - + assert_eq!(response.status(), 200); - + let json: Value = response.json().await.expect("Failed to parse JSON"); assert_eq!(json["email"], email); assert!(json["user_id"].is_string()); @@ -55,7 +58,7 @@ async fn test_register_user() { async fn test_login() { let client = Client::new(); let email = format!("test_{}@example.com", uuid::Uuid::new_v4()); - + // First register a user let register_payload = json!({ "email": email, @@ -64,27 +67,29 @@ async fn test_login() { "recovery_phrase_iv": "iv_placeholder", "recovery_phrase_auth_tag": "auth_tag_placeholder" }); - - let _reg_response = client.post(&format!("{}/api/auth/register", BASE_URL)) + + let _reg_response = client + .post(&format!("{}/api/auth/register", BASE_URL)) .json(®ister_payload) .send() .await .expect("Failed to send request"); - + // Now login let login_payload = json!({ "email": email, "password_hash": "hashed_password_placeholder" }); - - let response = client.post(&format!("{}/api/auth/login", BASE_URL)) + + let response = client + .post(&format!("{}/api/auth/login", BASE_URL)) .json(&login_payload) .send() .await .expect("Failed to send request"); - + assert_eq!(response.status(), 200); - + let json: Value = response.json().await.expect("Failed to parse JSON"); assert!(json["access_token"].is_string()); assert!(json["refresh_token"].is_string()); @@ -94,12 +99,13 @@ async fn test_login() { #[tokio::test] async fn test_get_profile_without_auth() { let client = Client::new(); - - let response = client.get(&format!("{}/api/users/me", BASE_URL)) + + let response = client + .get(&format!("{}/api/users/me", BASE_URL)) .send() .await .expect("Failed to send request"); - + // Should return 401 Unauthorized without auth token assert_eq!(response.status(), 401); } @@ -108,7 +114,7 @@ async fn test_get_profile_without_auth() { async fn test_get_profile_with_auth() { let client = Client::new(); let email = format!("test_{}@example.com", uuid::Uuid::new_v4()); - + // Register and login let register_payload = json!({ "email": email, @@ -117,36 +123,41 @@ async fn test_get_profile_with_auth() { "recovery_phrase_iv": "iv_placeholder", "recovery_phrase_auth_tag": "auth_tag_placeholder" }); - - client.post(&format!("{}/api/auth/register", BASE_URL)) + + client + .post(&format!("{}/api/auth/register", BASE_URL)) .json(®ister_payload) .send() .await .expect("Failed to send request"); - + let login_payload = json!({ "email": email, "password_hash": "hashed_password_placeholder" }); - - let login_response = client.post(&format!("{}/api/auth/login", BASE_URL)) + + let login_response = client + .post(&format!("{}/api/auth/login", BASE_URL)) .json(&login_payload) .send() .await .expect("Failed to send request"); - + let login_json: Value = login_response.json().await.expect("Failed to parse JSON"); - let access_token = login_json["access_token"].as_str().expect("No access token"); - + let access_token = login_json["access_token"] + .as_str() + .expect("No access token"); + // Get profile with auth token - let response = client.get(&format!("{}/api/users/me", BASE_URL)) + let response = client + .get(&format!("{}/api/users/me", BASE_URL)) .header("Authorization", format!("Bearer {}", access_token)) .send() .await .expect("Failed to send request"); - + assert_eq!(response.status(), 200); - + let json: Value = response.json().await.expect("Failed to parse JSON"); assert_eq!(json["email"], email); } diff --git a/backend/tests/medication_tests.rs b/backend/tests/medication_tests.rs index 0de9ddd..89f889c 100644 --- a/backend/tests/medication_tests.rs +++ b/backend/tests/medication_tests.rs @@ -8,9 +8,9 @@ mod medication_tests { use reqwest::Client; use serde_json::json; - + const BASE_URL: &str = "http://localhost:3000"; - + #[tokio::test] async fn test_create_medication_requires_auth() { let client = Client::new(); @@ -25,11 +25,11 @@ mod medication_tests { .send() .await .expect("Failed to send request"); - + // Should return 401 since no auth token provided assert_eq!(response.status(), 401); } - + #[tokio::test] async fn test_list_medications_requires_auth() { let client = Client::new(); @@ -38,20 +38,23 @@ mod medication_tests { .send() .await .expect("Failed to send request"); - + // Should return 401 since no auth token provided assert_eq!(response.status(), 401); } - + #[tokio::test] async fn test_get_medication_requires_auth() { let client = Client::new(); let response = client - .get(&format!("{}/api/medications/507f1f77bcf86cd799439011", BASE_URL)) + .get(&format!( + "{}/api/medications/507f1f77bcf86cd799439011", + BASE_URL + )) .send() .await .expect("Failed to send request"); - + // Should return 401 since no auth token provided assert_eq!(response.status(), 401); }