diff --git a/backend/clippy.toml b/backend/clippy.toml index 10ce8e1..11b23ad 100644 --- a/backend/clippy.toml +++ b/backend/clippy.toml @@ -1,16 +1,27 @@ -# Clippy configuration for Normogen backend -# This configuration fine-tunes Clippy lints for our project +# Clippy configuration -# Cognitive complexity threshold (default is already quite high) -cognitive-complexity-threshold = 30 +# Allow certain warnings for development +ambiguous-glob-reexports = "allow" +cast-lossless = "allow" +doc-markdown = "warn" +empty-structs-with-brackets = "warn" +explicit-auto-deref = "warn" +if-then-some-else-none = "warn" +match-wildcard-for-single-variants = "warn" +missing-errors-doc = "warn" +missing-panics-doc = "warn" +missing-safety-doc = "warn" +semicolon-if-nothing-returned = "warn" +unreadable-literal = "warn" +unused-self = "warn" +used-underscore-binding = "warn" -# Documentation threshold - accept common technical terms -doc-valid-idents = [ - "MongoDB", - "JWT", - "API", - "JSON", - "OAuth", - "HTTP", - "URL", -] +# Deny certain lints +missing-docs-in-private-items = "warn" +unwrap-used = "warn" +expect-used = "warn" +indexing-slicing = "warn" +panic = "deny" +unimplemented = "warn" +todo = "warn" +unreachable = "warn" diff --git a/backend/src/auth/jwt.rs b/backend/src/auth/jwt.rs index e34cac3..60289b8 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,16 +99,24 @@ 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) } @@ -116,11 +124,10 @@ 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(); @@ -129,7 +136,7 @@ impl JwtService { *user_tokens = user_tokens.split_off(user_tokens.len() - 5); } } - + Ok(()) } @@ -144,18 +151,13 @@ 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 04e3bbd..9ffc867 100644 --- a/backend/src/auth/password.rs +++ b/backend/src/auth/password.rs @@ -1,7 +1,10 @@ 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; @@ -9,8 +12,7 @@ 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()) } @@ -19,8 +21,7 @@ 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 ec57c5d..323c526 100644 --- a/backend/src/config/mod.rs +++ b/backend/src/config/mod.rs @@ -1,5 +1,5 @@ -use anyhow::Result; use std::sync::Arc; +use anyhow::Result; #[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,10 +74,8 @@ 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()), @@ -89,8 +87,7 @@ 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/init.rs b/backend/src/db/init.rs index 45afd12..15a8bd3 100644 --- a/backend/src/db/init.rs +++ b/backend/src/db/init.rs @@ -1,4 +1,9 @@ -use mongodb::{bson::doc, Client, Collection, IndexModel}; +use mongodb::{ + Client, + Collection, + bson::doc, + IndexModel, +}; use anyhow::Result; @@ -20,17 +25,13 @@ impl DatabaseInitializer { // Create users collection and index { let collection: Collection = db.collection("users"); - + // Create email index using the builder pattern let index = IndexModel::builder() .keys(doc! { "email": 1 }) - .options( - mongodb::options::IndexOptions::builder() - .unique(true) - .build(), - ) + .options(mongodb::options::IndexOptions::builder().unique(true).build()) .build(); - + match collection.create_index(index, None).await { Ok(_) => println!("✓ Created index on users.email"), Err(e) => println!("Warning: Failed to create index on users.email: {}", e), @@ -40,37 +41,37 @@ impl DatabaseInitializer { // Create families collection and indexes { let collection: Collection = db.collection("families"); - - let index1 = IndexModel::builder().keys(doc! { "userId": 1 }).build(); - - let index2 = IndexModel::builder().keys(doc! { "familyId": 1 }).build(); - + + let index1 = IndexModel::builder() + .keys(doc! { "userId": 1 }) + .build(); + + let index2 = IndexModel::builder() + .keys(doc! { "familyId": 1 }) + .build(); + match collection.create_index(index1, None).await { Ok(_) => println!("✓ Created index on families.userId"), Err(e) => println!("Warning: Failed to create index on families.userId: {}", e), } - + match collection.create_index(index2, None).await { Ok(_) => println!("✓ Created index on families.familyId"), - Err(e) => println!( - "Warning: Failed to create index on families.familyId: {}", - e - ), + Err(e) => println!("Warning: Failed to create index on families.familyId: {}", e), } } // Create profiles collection and index { let collection: Collection = db.collection("profiles"); - - let index = IndexModel::builder().keys(doc! { "familyId": 1 }).build(); - + + let index = IndexModel::builder() + .keys(doc! { "familyId": 1 }) + .build(); + match collection.create_index(index, None).await { Ok(_) => println!("✓ Created index on profiles.familyId"), - Err(e) => println!( - "Warning: Failed to create index on profiles.familyId: {}", - e - ), + Err(e) => println!("Warning: Failed to create index on profiles.familyId: {}", e), } } @@ -101,9 +102,11 @@ impl DatabaseInitializer { // Create shares collection and index { let collection: Collection = db.collection("shares"); - - let index = IndexModel::builder().keys(doc! { "familyId": 1 }).build(); - + + let index = IndexModel::builder() + .keys(doc! { "familyId": 1 }) + .build(); + match collection.create_index(index, None).await { Ok(_) => println!("✓ Created index on shares.familyId"), Err(e) => println!("Warning: Failed to create index on shares.familyId: {}", e), @@ -113,22 +116,15 @@ impl DatabaseInitializer { // Create refresh_tokens collection and index { let collection: Collection = db.collection("refresh_tokens"); - + let index = IndexModel::builder() .keys(doc! { "token": 1 }) - .options( - mongodb::options::IndexOptions::builder() - .unique(true) - .build(), - ) + .options(mongodb::options::IndexOptions::builder().unique(true).build()) .build(); - + match collection.create_index(index, None).await { Ok(_) => println!("✓ Created index on refresh_tokens.token"), - Err(e) => println!( - "Warning: Failed to create index on refresh_tokens.token: {}", - e - ), + Err(e) => println!("Warning: Failed to create index on refresh_tokens.token: {}", e), } } diff --git a/backend/src/db/mod.rs b/backend/src/db/mod.rs index b6cbde0..51a0e3d 100644 --- a/backend/src/db/mod.rs +++ b/backend/src/db/mod.rs @@ -1,18 +1,18 @@ -use anyhow::Result; use mongodb::{Client, Database}; use std::env; +use anyhow::Result; -pub mod appointment; +pub mod user; pub mod family; +pub mod profile; pub mod health_data; pub mod lab_result; pub mod medication; -pub mod permission; -pub mod profile; +pub mod appointment; pub mod share; -pub mod user; +pub mod permission; -pub mod init; // Database initialization module +pub mod init; // Database initialization module mod mongodb_impl; @@ -21,9 +21,9 @@ pub use mongodb_impl::MongoDb; pub async fn create_database() -> Result { let mongo_uri = env::var("MONGODB_URI").expect("MONGODB_URI must be set"); let db_name = env::var("DATABASE_NAME").expect("DATABASE_NAME must be set"); - + let client = Client::with_uri_str(&mongo_uri).await?; let database = client.database(&db_name); - + Ok(database) } diff --git a/backend/src/db/mongodb_impl.rs b/backend/src/db/mongodb_impl.rs index 6040074..0b2fbab 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::{ - medication::{Medication, MedicationDose, MedicationRepository, UpdateMedicationRequest}, - permission::Permission, - share::{Share, ShareRepository}, user::{User, UserRepository}, + share::{Share, ShareRepository}, + permission::Permission, + medication::{Medication, MedicationRepository, MedicationDose, UpdateMedicationRequest}, }; #[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,35 +31,27 @@ 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"), @@ -69,13 +61,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"); @@ -84,17 +76,14 @@ 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"), @@ -104,13 +93,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"), @@ -119,7 +108,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 { @@ -135,82 +124,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, @@ -220,12 +209,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() @@ -239,16 +228,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, @@ -258,98 +247,74 @@ 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 4c2e625..95c6c0e 100644 --- a/backend/src/handlers/auth.rs +++ b/backend/src/handlers/auth.rs @@ -1,9 +1,17 @@ -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::audit_log::AuditEventType, models::user::User, + auth::jwt::Claims, + config::AppState, + models::user::User, + models::audit_log::AuditEventType, }; #[derive(Debug, Deserialize, Validate)] @@ -31,40 +39,28 @@ 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(), @@ -75,81 +71,63 @@ 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() } @@ -166,36 +144,28 @@ 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, @@ -204,53 +174,37 @@ 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) => { @@ -258,86 +212,70 @@ 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() } @@ -356,108 +294,78 @@ 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 6b7920e..2964c14 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 4d1e592..f45a996 100644 --- a/backend/src/handlers/health_stats.rs +++ b/backend/src/handlers/health_stats.rs @@ -1,19 +1,14 @@ -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 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; #[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, @@ -29,7 +24,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( @@ -38,7 +33,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), @@ -46,9 +41,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(), @@ -66,11 +61,7 @@ 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() } } } @@ -84,11 +75,7 @@ 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() } } } @@ -114,11 +101,7 @@ 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() } } } @@ -138,13 +121,7 @@ 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 { @@ -154,7 +131,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; } @@ -168,11 +145,7 @@ 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(), } } @@ -190,13 +163,7 @@ 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 { @@ -205,11 +172,7 @@ 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(), } } @@ -226,25 +189,21 @@ 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(), @@ -253,16 +212,12 @@ 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 ce7c2e3..7dc0897 100644 --- a/backend/src/handlers/interactions.rs +++ b/backend/src/handlers/interactions.rs @@ -34,12 +34,10 @@ 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 @@ -48,7 +46,7 @@ pub async fn check_interactions( let has_severe = interactions .iter() .any(|i| matches!(i.severity, InteractionSeverity::Severe)); - + Ok(Json(InteractionResponse { interactions, has_severe, @@ -78,11 +76,9 @@ 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 68e2f84..478e3ca 100644 --- a/backend/src/handlers/medications.rs +++ b/backend/src/handlers/medications.rs @@ -1,17 +1,14 @@ use axum::{ - extract::{Extension, Json, Path, Query, State}, + extract::{Path, Query, State, Extension, Json}, http::StatusCode, }; use mongodb::bson::oid::ObjectId; use std::time::SystemTime; use crate::{ - auth::jwt::Claims, // Fixed: import from auth::jwt instead of handlers::auth + models::medication::{Medication, MedicationRepository, CreateMedicationRequest, UpdateMedicationRequest, LogDoseRequest}, + auth::jwt::Claims, // Fixed: import from auth::jwt instead of handlers::auth config::AppState, - models::medication::{ - CreateMedicationRequest, LogDoseRequest, Medication, MedicationRepository, - UpdateMedicationRequest, - }, }; #[derive(serde::Deserialize)] @@ -28,7 +25,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(); @@ -62,15 +59,12 @@ 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 @@ -89,10 +83,13 @@ 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), } @@ -105,7 +102,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)), @@ -124,7 +121,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)), @@ -142,7 +139,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), @@ -160,9 +157,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(), @@ -172,12 +169,8 @@ 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), } @@ -190,7 +183,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 f34e7a7..a248059 100644 --- a/backend/src/handlers/mod.rs +++ b/backend/src/handlers/mod.rs @@ -1,28 +1,20 @@ 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::{login, recover_password, register}; +pub use auth::{register, login, recover_password}; pub use health::{health_check, ready_check}; -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 shares::{create_share, list_shares, update_share, delete_share}; 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, -}; +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 interactions::{check_interactions, check_new_medication}; diff --git a/backend/src/handlers/permissions.rs b/backend/src/handlers/permissions.rs index 6a4c751..2210410 100644 --- a/backend/src/handlers/permissions.rs +++ b/backend/src/handlers/permissions.rs @@ -2,11 +2,15 @@ use axum::{ extract::{Query, State}, http::StatusCode, response::IntoResponse, - Extension, Json, + Json, + Extension, }; use serde::{Deserialize, Serialize}; -use crate::{auth::jwt::Claims, config::AppState}; +use crate::{ + auth::jwt::Claims, + config::AppState, +}; #[derive(Debug, Deserialize)] pub struct CheckPermissionQuery { @@ -28,35 +32,27 @@ 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 3ddbbbe..74fe6f5 100644 --- a/backend/src/handlers/sessions.rs +++ b/backend/src/handlers/sessions.rs @@ -1,10 +1,6 @@ +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 9e67c0a..6acdb7f 100644 --- a/backend/src/handlers/shares.rs +++ b/backend/src/handlers/shares.rs @@ -2,16 +2,17 @@ use axum::{ extract::{Path, State}, http::StatusCode, response::IntoResponse, - Extension, Json, + Json, + Extension, }; -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::{permission::Permission, share::Share}, + models::{share::Share, permission::Permission}, }; #[derive(Debug, Deserialize, Validate)] @@ -38,18 +39,14 @@ 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, @@ -63,86 +60,63 @@ 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), @@ -153,18 +127,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, @@ -173,32 +147,24 @@ 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() } } } @@ -208,7 +174,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 @@ -219,50 +185,39 @@ 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() } } } @@ -286,50 +241,34 @@ 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 @@ -344,42 +283,32 @@ 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() } } } @@ -393,61 +322,41 @@ 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 a20801c..9ef4e85 100644 --- a/backend/src/handlers/users.rs +++ b/backend/src/handlers/users.rs @@ -1,9 +1,19 @@ -use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, Json}; -use mongodb::bson::oid::ObjectId; +use axum::{ + extract::{State}, + http::StatusCode, + response::IntoResponse, + Json, + Extension, +}; 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 { @@ -17,7 +27,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(), @@ -41,28 +51,22 @@ 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() } } } @@ -73,45 +77,33 @@ 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(); @@ -119,13 +111,9 @@ 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() } } } @@ -135,18 +123,14 @@ 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() } } } @@ -165,91 +149,63 @@ 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() } } } @@ -274,28 +230,22 @@ 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() } } } @@ -311,37 +261,29 @@ 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(); @@ -349,13 +291,9 @@ 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 e161e6d..bdc25c8 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::{delete, get, post, put}, + routing::{get, post, put, delete}, 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,23 +102,17 @@ 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 @@ -169,7 +163,7 @@ async fn main() -> anyhow::Result<()> { state.clone(), middleware::jwt_auth_middleware )); - + let app = public_routes .merge(protected_routes) .with_state(state) @@ -192,8 +186,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 2be9ae4..46b19ea 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 f49bb9d..96cb145 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 auth::jwt_auth_middleware; pub use rate_limit::general_rate_limit_middleware; +pub use auth::jwt_auth_middleware; // Simple security headers middleware pub async fn security_headers_middleware( @@ -10,19 +10,13 @@ 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 b10f95e..a856a4c 100644 --- a/backend/src/middleware/rate_limit.rs +++ b/backend/src/middleware/rate_limit.rs @@ -1,4 +1,9 @@ -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 @@ -13,7 +18,10 @@ 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 0704717..cacf058 100644 --- a/backend/src/models/audit_log.rs +++ b/backend/src/models/audit_log.rs @@ -1,10 +1,10 @@ -use anyhow::Result; -use futures::stream::TryStreamExt; use mongodb::{ - bson::{doc, oid::ObjectId}, Collection, + bson::{doc, oid::ObjectId}, }; +use futures::stream::TryStreamExt; use serde::{Deserialize, Serialize}; +use anyhow::Result; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum AuditEventType { @@ -87,8 +87,7 @@ 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 @@ -103,13 +102,15 @@ 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 9aa9a90..cd2d4f1 100644 --- a/backend/src/models/family.rs +++ b/backend/src/models/family.rs @@ -1,8 +1,5 @@ -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 { @@ -32,16 +29,13 @@ 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 f55efe4..0242da2 100644 --- a/backend/src/models/health_data.rs +++ b/backend/src/models/health_data.rs @@ -1,5 +1,5 @@ -use mongodb::bson::{oid::ObjectId, DateTime}; use serde::{Deserialize, Serialize}; +use mongodb::bson::{oid::ObjectId, DateTime}; #[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 3f12520..4bef19d 100644 --- a/backend/src/models/health_stats.rs +++ b/backend/src/models/health_stats.rs @@ -1,10 +1,7 @@ -use futures::stream::TryStreamExt; use mongodb::Collection; -use mongodb::{ - bson::{doc, oid::ObjectId}, - error::Error as MongoError, -}; use serde::{Deserialize, Serialize}; +use mongodb::{bson::{oid::ObjectId, doc}, error::Error as MongoError}; +use futures::stream::TryStreamExt; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HealthStatistic { @@ -49,11 +46,7 @@ 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 f4b526e..0d80592 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 mongodb::bson::{oid::ObjectId, DateTime}; use serde::{Deserialize, Serialize}; +use mongodb::bson::{oid::ObjectId, DateTime}; #[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 50f179f..cbab5e7 100644 --- a/backend/src/models/lab_result.rs +++ b/backend/src/models/lab_result.rs @@ -1,6 +1,6 @@ -use futures::stream::TryStreamExt; -use mongodb::{bson::oid::ObjectId, Collection}; use serde::{Deserialize, Serialize}; +use mongodb::{bson::oid::ObjectId, Collection}; +use futures::stream::TryStreamExt; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LabResult { @@ -41,18 +41,12 @@ 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 ff70bd4..1afedfb 100644 --- a/backend/src/models/medication.rs +++ b/backend/src/models/medication.rs @@ -1,8 +1,8 @@ -use super::health_data::EncryptedField; -use futures::stream::StreamExt; -use mongodb::bson::{doc, oid::ObjectId, DateTime}; -use mongodb::Collection; use serde::{Deserialize, Serialize}; +use mongodb::bson::{oid::ObjectId, DateTime, doc}; +use mongodb::Collection; +use futures::stream::StreamExt; +use super::health_data::EncryptedField; // ============================================================================ // 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,19 +216,13 @@ 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(); @@ -237,13 +231,9 @@ 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 }; @@ -254,23 +244,16 @@ 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); } @@ -318,28 +301,21 @@ 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 c5dba69..5ec2e6c 100644 --- a/backend/src/models/mod.rs +++ b/backend/src/models/mod.rs @@ -2,7 +2,6 @@ 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; @@ -11,3 +10,4 @@ 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 db0513c..5f5d5b6 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 9f828ce..1def644 100644 --- a/backend/src/models/profile.rs +++ b/backend/src/models/profile.rs @@ -1,8 +1,5 @@ -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 { @@ -38,16 +35,13 @@ 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 789e7e1..e50dfaf 100644 --- a/backend/src/models/refresh_token.rs +++ b/backend/src/models/refresh_token.rs @@ -1,5 +1,5 @@ -use mongodb::bson::{oid::ObjectId, DateTime}; use serde::{Deserialize, Serialize}; +use mongodb::bson::{oid::ObjectId, DateTime}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RefreshToken { diff --git a/backend/src/models/session.rs b/backend/src/models/session.rs index e26272a..0f0407b 100644 --- a/backend/src/models/session.rs +++ b/backend/src/models/session.rs @@ -1,16 +1,16 @@ -use anyhow::Result; -use futures::stream::TryStreamExt; use mongodb::{ - bson::{doc, oid::ObjectId}, Collection, + bson::{doc, oid::ObjectId}, }; +use futures::stream::TryStreamExt; use serde::{Deserialize, Serialize}; +use anyhow::Result; 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,17 +65,11 @@ 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, @@ -104,7 +98,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 }, @@ -116,8 +110,7 @@ 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 1f9d4e3..c2762c5 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,37 +89,27 @@ 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 53b9cf8..d5db98d 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 crate::auth::password::verify_password; use mongodb::bson::DateTime; +use crate::auth::password::verify_password; #[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,14 +156,13 @@ 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 @@ -178,16 +177,12 @@ 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 }, @@ -209,7 +204,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 3822a63..ac5b8ee 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,11 +26,16 @@ 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() { @@ -41,28 +46,32 @@ 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; @@ -71,7 +80,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; @@ -79,7 +88,7 @@ impl AccountLockout { } else { DateTime::now() }; - + // Update user collection .update_one( @@ -94,13 +103,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 }, @@ -113,7 +122,7 @@ impl AccountLockout { None, ) .await?; - + Ok(()) } } diff --git a/backend/src/security/audit_logger.rs b/backend/src/security/audit_logger.rs index 23bdde3..7d70ace 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,14 +24,7 @@ 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 e682707..37736ff 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 6945d68..cae6c9d 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,9 +21,7 @@ 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 0e22c5f..df95913 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,25 +32,26 @@ 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); @@ -66,19 +67,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 cdc7fe7..9a00235 100644 --- a/backend/src/services/interaction_service.rs +++ b/backend/src/services/interaction_service.rs @@ -1,11 +1,12 @@ //! Interaction Service -//! +//! //! Combines ingredient mapping and OpenFDA interaction checking //! Provides a unified API for checking drug interactions use crate::services::{ - openfda_service::{DrugInteraction, InteractionSeverity}, - IngredientMapper, OpenFDAService, + IngredientMapper, + OpenFDAService, + openfda_service::{DrugInteraction, InteractionSeverity} }; use mongodb::bson::oid::ObjectId; use serde::{Deserialize, Serialize}; @@ -22,65 +23,56 @@ 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, @@ -105,32 +97,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 ccf698e..8952461 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 interaction_service; pub mod openfda_service; +pub mod interaction_service; pub use ingredient_mapper::IngredientMapper; -pub use interaction_service::InteractionService; pub use openfda_service::OpenFDAService; +pub use interaction_service::InteractionService; diff --git a/backend/src/services/openfda_service.rs b/backend/src/services/openfda_service.rs index 0b5574d..b9af959 100644 --- a/backend/src/services/openfda_service.rs +++ b/backend/src/services/openfda_service.rs @@ -2,7 +2,7 @@ use reqwest::Client; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum InteractionSeverity { Mild, @@ -31,70 +31,57 @@ impl OpenFDAService { base_url: "https://api.fda.gov/drug/event.json".to_string(), } } - - /// Check for interactions between multiple drugs + + /// Check interactions between multiple medications pub async fn check_interactions( &self, - drugs: &[String], + medications: &[String], ) -> Result, Box> { let mut interactions = Vec::new(); - - // Check each pair of drugs - for i in 0..drugs.len() { - for j in (i + 1)..drugs.len() { - if let Some(interaction) = self.check_pair(&drugs[i], &drugs[j]).await { + + // Check all pairs + for i in 0..medications.len() { + for j in (i + 1)..medications.len() { + if let Some(interaction) = self + .check_pair_interaction(&medications[i], &medications[j]) + .await + { interactions.push(interaction); } } } - + Ok(interactions) } - - /// Check for interaction between a specific pair of drugs - async fn check_pair(&self, drug1: &str, drug2: &str) -> Option { + + /// Check interaction between two specific drugs + async fn check_pair_interaction( + &self, + drug1: &str, + drug2: &str, + ) -> Option { // For MVP, use a hardcoded database of known interactions // In production, you would: // 1. Query OpenFDA drug event endpoint // 2. Use a professional interaction database // 3. Integrate with user-provided data - - let pair = format!("{}+{}", drug1.to_lowercase(), drug2.to_lowercase()); - + + let pair = format!( + "{}+{}", + drug1.to_lowercase(), + drug2.to_lowercase() + ); + // Known severe interactions (for demonstration) - let known_interactions = vec![ - ( - "warfarin+aspirin", - InteractionSeverity::Severe, - "Increased risk of bleeding", - ), - ( - "warfarin+ibuprofen", - InteractionSeverity::Severe, - "Increased risk of bleeding", - ), - ( - "acetaminophen+alcohol", - InteractionSeverity::Severe, - "Increased risk of liver damage", - ), - ( - "ssri+maoi", - InteractionSeverity::Severe, - "Serotonin syndrome risk", - ), - ( - "digoxin+verapamil", - InteractionSeverity::Moderate, - "Increased digoxin levels", - ), - ( - "acei+arb", - InteractionSeverity::Moderate, - "Increased risk of hyperkalemia", - ), + let known_interactions = [ + ("warfarin+aspirin", InteractionSeverity::Severe, "Increased risk of bleeding"), + ("warfarin+ibuprofen", InteractionSeverity::Severe, "Increased risk of bleeding"), + ("acetaminophen+alcohol", InteractionSeverity::Severe, "Increased risk of liver damage"), + ("ssri+maoi", InteractionSeverity::Severe, "Serotonin syndrome risk"), + ("digoxin+verapamil", InteractionSeverity::Moderate, "Increased digoxin levels"), + ("acei+arb", InteractionSeverity::Moderate, "Increased risk of hyperkalemia"), ]; - + for (known_pair, severity, desc) in known_interactions { if pair.contains(known_pair) || known_pair.contains(&pair) { return Some(DrugInteraction { @@ -105,10 +92,10 @@ impl OpenFDAService { }); } } - + None } - + /// Query OpenFDA for drug event reports async fn query_drug_events( &self, @@ -116,9 +103,10 @@ impl OpenFDAService { ) -> Result> { let query = format!( "{}?search=patient.drug.medicinalproduct:{}&limit=10", - self.base_url, drug_name + self.base_url, + drug_name ); - + let response = self .client .get(&query) @@ -126,7 +114,7 @@ impl OpenFDAService { .await? .json::() .await?; - + Ok(response) } } @@ -140,7 +128,7 @@ impl Default for OpenFDAService { #[cfg(test)] mod tests { use super::*; - + #[tokio::test] async fn test_check_warfarin_aspirin() { let service = OpenFDAService::new(); @@ -148,7 +136,7 @@ mod tests { .check_interactions(&["warfarin".to_string(), "aspirin".to_string()]) .await .unwrap(); - + assert!(!interactions.is_empty()); assert_eq!(interactions[0].severity, InteractionSeverity::Severe); } diff --git a/backend/tests/auth_tests.rs b/backend/tests/auth_tests.rs index db28b33..0426ec9 100644 --- a/backend/tests/auth_tests.rs +++ b/backend/tests/auth_tests.rs @@ -6,24 +6,22 @@ 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); } @@ -31,7 +29,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", @@ -39,16 +37,15 @@ 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()); @@ -58,7 +55,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, @@ -67,29 +64,27 @@ 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()); @@ -99,13 +94,12 @@ 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); } @@ -114,7 +108,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, @@ -123,41 +117,36 @@ 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 89f889c..0de9ddd 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,23 +38,20 @@ 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); }