Compare commits

..

No commits in common. "ee0feb77ef2ea213f1e147bca32bd5efa7091d14" and "22e244f6c8ec8fbba6d0efc11eb2f2ca8314f51b" have entirely different histories.

45 changed files with 923 additions and 1375 deletions

View file

@ -1,16 +1,27 @@
# Clippy configuration for Normogen backend # Clippy configuration
# This configuration fine-tunes Clippy lints for our project
# Cognitive complexity threshold (default is already quite high) # Allow certain warnings for development
cognitive-complexity-threshold = 30 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 # Deny certain lints
doc-valid-idents = [ missing-docs-in-private-items = "warn"
"MongoDB", unwrap-used = "warn"
"JWT", expect-used = "warn"
"API", indexing-slicing = "warn"
"JSON", panic = "deny"
"OAuth", unimplemented = "warn"
"HTTP", todo = "warn"
"URL", unreachable = "warn"
]

View file

@ -99,7 +99,11 @@ impl JwtService {
/// Validate access token /// Validate access token
pub fn validate_token(&self, token: &str) -> Result<Claims> { pub fn validate_token(&self, token: &str) -> Result<Claims> {
let token_data = decode::<Claims>(token, &self.decoding_key, &Validation::default()) let token_data = decode::<Claims>(
token,
&self.decoding_key,
&Validation::default()
)
.map_err(|e| anyhow::anyhow!("Invalid token: {}", e))?; .map_err(|e| anyhow::anyhow!("Invalid token: {}", e))?;
Ok(token_data.claims) Ok(token_data.claims)
@ -107,7 +111,11 @@ impl JwtService {
/// Validate refresh token /// Validate refresh token
pub fn validate_refresh_token(&self, token: &str) -> Result<RefreshClaims> { pub fn validate_refresh_token(&self, token: &str) -> Result<RefreshClaims> {
let token_data = decode::<RefreshClaims>(token, &self.decoding_key, &Validation::default()) let token_data = decode::<RefreshClaims>(
token,
&self.decoding_key,
&Validation::default()
)
.map_err(|e| anyhow::anyhow!("Invalid refresh token: {}", e))?; .map_err(|e| anyhow::anyhow!("Invalid refresh token: {}", e))?;
Ok(token_data.claims) Ok(token_data.claims)
@ -116,8 +124,7 @@ impl JwtService {
/// Store refresh token for a user /// Store refresh token for a user
pub async fn store_refresh_token(&self, user_id: &str, token: &str) -> Result<()> { pub async fn store_refresh_token(&self, user_id: &str, token: &str) -> Result<()> {
let mut tokens = self.refresh_tokens.write().await; let mut tokens = self.refresh_tokens.write().await;
tokens tokens.entry(user_id.to_string())
.entry(user_id.to_string())
.or_insert_with(Vec::new) .or_insert_with(Vec::new)
.push(token.to_string()); .push(token.to_string());
@ -144,12 +151,7 @@ impl JwtService {
} }
/// Rotate refresh token (remove old, add new) /// Rotate refresh token (remove old, add new)
pub async fn rotate_refresh_token( pub async fn rotate_refresh_token(&self, user_id: &str, old_token: &str, new_token: &str) -> Result<()> {
&self,
user_id: &str,
old_token: &str,
new_token: &str,
) -> Result<()> {
// Remove old token // Remove old token
self.revoke_refresh_token(old_token).await?; self.revoke_refresh_token(old_token).await?;

View file

@ -1,7 +1,10 @@
use anyhow::Result; use anyhow::Result;
use pbkdf2::{ use pbkdf2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, password_hash::{
Pbkdf2, rand_core::OsRng,
PasswordHash, PasswordHasher, PasswordVerifier, SaltString
},
Pbkdf2
}; };
pub struct PasswordService; pub struct PasswordService;
@ -9,8 +12,7 @@ pub struct PasswordService;
impl PasswordService { impl PasswordService {
pub fn hash_password(password: &str) -> Result<String> { pub fn hash_password(password: &str) -> Result<String> {
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
let password_hash = Pbkdf2 let password_hash = Pbkdf2.hash_password(password.as_bytes(), &salt)
.hash_password(password.as_bytes(), &salt)
.map_err(|e| anyhow::anyhow!("Failed to hash password: {}", e))?; .map_err(|e| anyhow::anyhow!("Failed to hash password: {}", e))?;
Ok(password_hash.to_string()) Ok(password_hash.to_string())
} }
@ -19,8 +21,7 @@ impl PasswordService {
let parsed_hash = PasswordHash::new(hash) let parsed_hash = PasswordHash::new(hash)
.map_err(|e| anyhow::anyhow!("Failed to parse password hash: {}", e))?; .map_err(|e| anyhow::anyhow!("Failed to parse password hash: {}", e))?;
Pbkdf2 Pbkdf2.verify_password(password.as_bytes(), &parsed_hash)
.verify_password(password.as_bytes(), &parsed_hash)
.map(|_| true) .map(|_| true)
.map_err(|e| anyhow::anyhow!("Password verification failed: {}", e)) .map_err(|e| anyhow::anyhow!("Password verification failed: {}", e))
} }

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result;
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
@ -74,10 +74,8 @@ impl Config {
.parse()?, .parse()?,
}, },
database: DatabaseConfig { database: DatabaseConfig {
uri: std::env::var("MONGODB_URI") uri: std::env::var("MONGODB_URI").unwrap_or_else(|_| "mongodb://localhost:27017".to_string()),
.unwrap_or_else(|_| "mongodb://localhost:27017".to_string()), database: std::env::var("MONGODB_DATABASE").unwrap_or_else(|_| "normogen".to_string()),
database: std::env::var("MONGODB_DATABASE")
.unwrap_or_else(|_| "normogen".to_string()),
}, },
jwt: JwtConfig { jwt: JwtConfig {
secret: std::env::var("JWT_SECRET").unwrap_or_else(|_| "secret".to_string()), secret: std::env::var("JWT_SECRET").unwrap_or_else(|_| "secret".to_string()),
@ -89,8 +87,7 @@ impl Config {
.parse()?, .parse()?,
}, },
encryption: EncryptionConfig { encryption: EncryptionConfig {
key: std::env::var("ENCRYPTION_KEY") key: std::env::var("ENCRYPTION_KEY").unwrap_or_else(|_| "default_key_32_bytes_long!".to_string()),
.unwrap_or_else(|_| "default_key_32_bytes_long!".to_string()),
}, },
cors: CorsConfig { cors: CorsConfig {
allowed_origins: std::env::var("CORS_ALLOWED_ORIGINS") allowed_origins: std::env::var("CORS_ALLOWED_ORIGINS")

View file

@ -1,4 +1,9 @@
use mongodb::{bson::doc, Client, Collection, IndexModel}; use mongodb::{
Client,
Collection,
bson::doc,
IndexModel,
};
use anyhow::Result; use anyhow::Result;
@ -24,11 +29,7 @@ impl DatabaseInitializer {
// Create email index using the builder pattern // Create email index using the builder pattern
let index = IndexModel::builder() let index = IndexModel::builder()
.keys(doc! { "email": 1 }) .keys(doc! { "email": 1 })
.options( .options(mongodb::options::IndexOptions::builder().unique(true).build())
mongodb::options::IndexOptions::builder()
.unique(true)
.build(),
)
.build(); .build();
match collection.create_index(index, None).await { match collection.create_index(index, None).await {
@ -41,9 +42,13 @@ impl DatabaseInitializer {
{ {
let collection: Collection<mongodb::bson::Document> = db.collection("families"); let collection: Collection<mongodb::bson::Document> = db.collection("families");
let index1 = IndexModel::builder().keys(doc! { "userId": 1 }).build(); let index1 = IndexModel::builder()
.keys(doc! { "userId": 1 })
.build();
let index2 = IndexModel::builder().keys(doc! { "familyId": 1 }).build(); let index2 = IndexModel::builder()
.keys(doc! { "familyId": 1 })
.build();
match collection.create_index(index1, None).await { match collection.create_index(index1, None).await {
Ok(_) => println!("✓ Created index on families.userId"), Ok(_) => println!("✓ Created index on families.userId"),
@ -52,10 +57,7 @@ impl DatabaseInitializer {
match collection.create_index(index2, None).await { match collection.create_index(index2, None).await {
Ok(_) => println!("✓ Created index on families.familyId"), Ok(_) => println!("✓ Created index on families.familyId"),
Err(e) => println!( Err(e) => println!("Warning: Failed to create index on families.familyId: {}", e),
"Warning: Failed to create index on families.familyId: {}",
e
),
} }
} }
@ -63,14 +65,13 @@ impl DatabaseInitializer {
{ {
let collection: Collection<mongodb::bson::Document> = db.collection("profiles"); let collection: Collection<mongodb::bson::Document> = 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 { match collection.create_index(index, None).await {
Ok(_) => println!("✓ Created index on profiles.familyId"), Ok(_) => println!("✓ Created index on profiles.familyId"),
Err(e) => println!( Err(e) => println!("Warning: Failed to create index on profiles.familyId: {}", e),
"Warning: Failed to create index on profiles.familyId: {}",
e
),
} }
} }
@ -102,7 +103,9 @@ impl DatabaseInitializer {
{ {
let collection: Collection<mongodb::bson::Document> = db.collection("shares"); let collection: Collection<mongodb::bson::Document> = 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 { match collection.create_index(index, None).await {
Ok(_) => println!("✓ Created index on shares.familyId"), Ok(_) => println!("✓ Created index on shares.familyId"),
@ -116,19 +119,12 @@ impl DatabaseInitializer {
let index = IndexModel::builder() let index = IndexModel::builder()
.keys(doc! { "token": 1 }) .keys(doc! { "token": 1 })
.options( .options(mongodb::options::IndexOptions::builder().unique(true).build())
mongodb::options::IndexOptions::builder()
.unique(true)
.build(),
)
.build(); .build();
match collection.create_index(index, None).await { match collection.create_index(index, None).await {
Ok(_) => println!("✓ Created index on refresh_tokens.token"), Ok(_) => println!("✓ Created index on refresh_tokens.token"),
Err(e) => println!( Err(e) => println!("Warning: Failed to create index on refresh_tokens.token: {}", e),
"Warning: Failed to create index on refresh_tokens.token: {}",
e
),
} }
} }

View file

@ -1,16 +1,16 @@
use anyhow::Result;
use mongodb::{Client, Database}; use mongodb::{Client, Database};
use std::env; use std::env;
use anyhow::Result;
pub mod appointment; pub mod user;
pub mod family; pub mod family;
pub mod profile;
pub mod health_data; pub mod health_data;
pub mod lab_result; pub mod lab_result;
pub mod medication; pub mod medication;
pub mod permission; pub mod appointment;
pub mod profile;
pub mod share; pub mod share;
pub mod user; pub mod permission;
pub mod init; // Database initialization module pub mod init; // Database initialization module

View file

@ -1,13 +1,13 @@
use mongodb::{Client, Database, Collection, bson::doc, options::ClientOptions};
use anyhow::Result; use anyhow::Result;
use mongodb::bson::oid::ObjectId; use mongodb::bson::oid::ObjectId;
use mongodb::{bson::doc, options::ClientOptions, Client, Collection, Database};
use std::time::Duration; use std::time::Duration;
use crate::models::{ use crate::models::{
medication::{Medication, MedicationDose, MedicationRepository, UpdateMedicationRequest},
permission::Permission,
share::{Share, ShareRepository},
user::{User, UserRepository}, user::{User, UserRepository},
share::{Share, ShareRepository},
permission::Permission,
medication::{Medication, MedicationRepository, MedicationDose, UpdateMedicationRequest},
}; };
#[derive(Clone)] #[derive(Clone)]
@ -33,25 +33,17 @@ impl MongoDb {
eprintln!("[MongoDB] ERROR: Failed to parse URI: {}", e); eprintln!("[MongoDB] ERROR: Failed to parse URI: {}", e);
let error_msg = e.to_string().to_lowercase(); let error_msg = e.to_string().to_lowercase();
if error_msg.contains("dns") if error_msg.contains("dns") || error_msg.contains("resolution") || error_msg.contains("lookup") {
|| error_msg.contains("resolution")
|| error_msg.contains("lookup")
{
eprintln!("[MongoDB] DNS RESOLUTION ERROR DETECTED!"); eprintln!("[MongoDB] DNS RESOLUTION ERROR DETECTED!");
eprintln!("[MongoDB] Cannot resolve hostname in: {}", uri); eprintln!("[MongoDB] Cannot resolve hostname in: {}", uri);
eprintln!("[MongoDB] Error: {}", e); eprintln!("[MongoDB] Error: {}", e);
} }
eprintln!( eprintln!("[MongoDB] Will continue in degraded mode (database operations will fail)");
"[MongoDB] Will continue in degraded mode (database operations will fail)"
);
// Create a minimal configuration that will allow the server to start // Create a minimal configuration that will allow the server to start
// but database operations will fail gracefully // but database operations will fail gracefully
let mut opts = ClientOptions::parse("mongodb://localhost:27017") let mut opts = ClientOptions::parse("mongodb://localhost:27017").await
.await .map_err(|e| anyhow::anyhow!("Failed to create fallback client options: {}", e))?;
.map_err(|e| {
anyhow::anyhow!("Failed to create fallback client options: {}", e)
})?;
opts.server_selection_timeout = Some(Duration::from_secs(1)); opts.server_selection_timeout = Some(Duration::from_secs(1));
opts.connect_timeout = Some(Duration::from_secs(1)); opts.connect_timeout = Some(Duration::from_secs(1));
@ -86,11 +78,8 @@ impl MongoDb {
eprintln!("[MongoDB] Will continue in degraded mode"); eprintln!("[MongoDB] Will continue in degraded mode");
// Create a fallback client // Create a fallback client
let fallback_opts = ClientOptions::parse("mongodb://localhost:27017") let fallback_opts = ClientOptions::parse("mongodb://localhost:27017").await
.await .map_err(|e| anyhow::anyhow!("Failed to create fallback client options: {}", e))?;
.map_err(|e| {
anyhow::anyhow!("Failed to create fallback client options: {}", e)
})?;
let fallback_client = Client::with_options(fallback_opts) let fallback_client = Client::with_options(fallback_opts)
.map_err(|e| anyhow::anyhow!("Failed to create MongoDB client: {}", e))?; .map_err(|e| anyhow::anyhow!("Failed to create MongoDB client: {}", e))?;
let database = fallback_client.database(db_name); let database = fallback_client.database(db_name);
@ -260,10 +249,7 @@ impl MongoDb {
let resource_types = ["profiles", "health_data", "lab_results", "medications"]; let resource_types = ["profiles", "health_data", "lab_results", "medications"];
for resource_type in resource_types { for resource_type in resource_types {
if self if self.check_user_permission(user_id, resource_type, resource_id, permission).await? {
.check_user_permission(user_id, resource_type, resource_id, permission)
.await?
{
return Ok(true); return Ok(true);
} }
} }
@ -275,8 +261,7 @@ impl MongoDb {
pub async fn create_medication(&self, medication: &Medication) -> Result<Option<ObjectId>> { pub async fn create_medication(&self, medication: &Medication) -> Result<Option<ObjectId>> {
let repo = MedicationRepository::new(self.medications.clone()); let repo = MedicationRepository::new(self.medications.clone());
let created = repo let created = repo.create(medication.clone())
.create(medication.clone())
.await .await
.map_err(|e| anyhow::anyhow!("Failed to create medication: {}", e))?; .map_err(|e| anyhow::anyhow!("Failed to create medication: {}", e))?;
Ok(created.id) Ok(created.id)
@ -285,40 +270,28 @@ impl MongoDb {
pub async fn get_medication(&self, id: &str) -> Result<Option<Medication>> { pub async fn get_medication(&self, id: &str) -> Result<Option<Medication>> {
let object_id = ObjectId::parse_str(id)?; let object_id = ObjectId::parse_str(id)?;
let repo = MedicationRepository::new(self.medications.clone()); let repo = MedicationRepository::new(self.medications.clone());
Ok(repo Ok(repo.find_by_id(&object_id)
.find_by_id(&object_id)
.await .await
.map_err(|e| anyhow::anyhow!("Failed to get medication: {}", e))?) .map_err(|e| anyhow::anyhow!("Failed to get medication: {}", e))?)
} }
pub async fn list_medications( pub async fn list_medications(&self, user_id: &str, profile_id: Option<&str>) -> Result<Vec<Medication>> {
&self,
user_id: &str,
profile_id: Option<&str>,
) -> Result<Vec<Medication>> {
let repo = MedicationRepository::new(self.medications.clone()); let repo = MedicationRepository::new(self.medications.clone());
if let Some(profile_id) = profile_id { if let Some(profile_id) = profile_id {
Ok(repo Ok(repo.find_by_user_and_profile(user_id, profile_id)
.find_by_user_and_profile(user_id, profile_id)
.await .await
.map_err(|e| anyhow::anyhow!("Failed to list medications by profile: {}", e))?) .map_err(|e| anyhow::anyhow!("Failed to list medications by profile: {}", e))?)
} else { } else {
Ok(repo Ok(repo.find_by_user(user_id)
.find_by_user(user_id)
.await .await
.map_err(|e| anyhow::anyhow!("Failed to list medications: {}", e))?) .map_err(|e| anyhow::anyhow!("Failed to list medications: {}", e))?)
} }
} }
pub async fn update_medication( pub async fn update_medication(&self, id: &str, updates: UpdateMedicationRequest) -> Result<Option<Medication>> {
&self,
id: &str,
updates: UpdateMedicationRequest,
) -> Result<Option<Medication>> {
let object_id = ObjectId::parse_str(id)?; let object_id = ObjectId::parse_str(id)?;
let repo = MedicationRepository::new(self.medications.clone()); let repo = MedicationRepository::new(self.medications.clone());
Ok(repo Ok(repo.update(&object_id, updates)
.update(&object_id, updates)
.await .await
.map_err(|e| anyhow::anyhow!("Failed to update medication: {}", e))?) .map_err(|e| anyhow::anyhow!("Failed to update medication: {}", e))?)
} }
@ -326,30 +299,22 @@ impl MongoDb {
pub async fn delete_medication(&self, id: &str) -> Result<bool> { pub async fn delete_medication(&self, id: &str) -> Result<bool> {
let object_id = ObjectId::parse_str(id)?; let object_id = ObjectId::parse_str(id)?;
let repo = MedicationRepository::new(self.medications.clone()); let repo = MedicationRepository::new(self.medications.clone());
Ok(repo Ok(repo.delete(&object_id)
.delete(&object_id)
.await .await
.map_err(|e| anyhow::anyhow!("Failed to delete medication: {}", e))?) .map_err(|e| anyhow::anyhow!("Failed to delete medication: {}", e))?)
} }
pub async fn log_medication_dose(&self, dose: &MedicationDose) -> Result<Option<ObjectId>> { pub async fn log_medication_dose(&self, dose: &MedicationDose) -> Result<Option<ObjectId>> {
// Insert the dose into the medication_doses collection // Insert the dose into the medication_doses collection
let result = self let result = self.medication_doses.insert_one(dose.clone(), None)
.medication_doses
.insert_one(dose.clone(), None)
.await .await
.map_err(|e| anyhow::anyhow!("Failed to log dose: {}", e))?; .map_err(|e| anyhow::anyhow!("Failed to log dose: {}", e))?;
Ok(result.inserted_id.as_object_id()) Ok(result.inserted_id.as_object_id())
} }
pub async fn get_medication_adherence( pub async fn get_medication_adherence(&self, medication_id: &str, days: i64) -> Result<crate::models::medication::AdherenceStats> {
&self,
medication_id: &str,
days: i64,
) -> Result<crate::models::medication::AdherenceStats> {
let repo = MedicationRepository::new(self.medications.clone()); let repo = MedicationRepository::new(self.medications.clone());
Ok(repo Ok(repo.calculate_adherence(medication_id, days)
.calculate_adherence(medication_id, days)
.await .await
.map_err(|e| anyhow::anyhow!("Failed to calculate adherence: {}", e))?) .map_err(|e| anyhow::anyhow!("Failed to calculate adherence: {}", e))?)
} }

View file

@ -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 serde::{Deserialize, Serialize};
use validator::Validate; use validator::Validate;
use crate::{ 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)] #[derive(Debug, Deserialize, Validate)]
@ -31,37 +39,25 @@ pub async fn register(
Json(req): Json<RegisterRequest>, Json(req): Json<RegisterRequest>,
) -> impl IntoResponse { ) -> impl IntoResponse {
if let Err(errors) = req.validate() { if let Err(errors) = req.validate() {
return ( return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "validation failed", "error": "validation failed",
"details": errors.to_string() "details": errors.to_string()
})), }))).into_response();
)
.into_response();
} }
// Check if user already exists // Check if user already exists
match state.db.find_user_by_email(&req.email).await { match state.db.find_user_by_email(&req.email).await {
Ok(Some(_)) => { Ok(Some(_)) => {
return ( return (StatusCode::CONFLICT, Json(serde_json::json!({
StatusCode::CONFLICT,
Json(serde_json::json!({
"error": "user already exists" "error": "user already exists"
})), }))).into_response()
)
.into_response()
} }
Ok(None) => {} Ok(None) => {},
Err(e) => { Err(e) => {
tracing::error!("Failed to check user existence: {}", e); tracing::error!("Failed to check user existence: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "database error" "error": "database error"
})), }))).into_response()
)
.into_response();
} }
} }
@ -75,13 +71,9 @@ pub async fn register(
Ok(u) => u, Ok(u) => u,
Err(e) => { Err(e) => {
tracing::error!("Failed to create user: {}", e); tracing::error!("Failed to create user: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to create user" "error": "failed to create user"
})), }))).into_response()
)
.into_response();
} }
}; };
@ -93,37 +85,27 @@ pub async fn register(
Ok(Some(id)) => { Ok(Some(id)) => {
// Log registration (Phase 2.6) // Log registration (Phase 2.6)
if let Some(ref audit) = state.audit_logger { if let Some(ref audit) = state.audit_logger {
let _ = audit let _ = audit.log_event(
.log_event(
AuditEventType::LoginSuccess, // Using LoginSuccess as registration event AuditEventType::LoginSuccess, // Using LoginSuccess as registration event
Some(id), Some(id),
Some(req.email.clone()), Some(req.email.clone()),
"0.0.0.0".to_string(), "0.0.0.0".to_string(),
None, None,
None, None,
) ).await;
.await;
} }
id id
} },
Ok(None) => { Ok(None) => {
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to create user" "error": "failed to create user"
})), }))).into_response()
)
.into_response()
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to save user: {}", e); tracing::error!("Failed to save user: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "database error" "error": "database error"
})), }))).into_response()
)
.into_response();
} }
}; };
@ -133,13 +115,9 @@ pub async fn register(
Ok(t) => t, Ok(t) => t,
Err(e) => { Err(e) => {
tracing::error!("Failed to generate token: {}", e); tracing::error!("Failed to generate token: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to generate token" "error": "failed to generate token"
})), }))).into_response()
)
.into_response();
} }
}; };
@ -166,30 +144,22 @@ pub async fn login(
Json(req): Json<LoginRequest>, Json(req): Json<LoginRequest>,
) -> impl IntoResponse { ) -> impl IntoResponse {
if let Err(errors) = req.validate() { if let Err(errors) = req.validate() {
return ( return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "validation failed", "error": "validation failed",
"details": errors.to_string() "details": errors.to_string()
})), }))).into_response();
)
.into_response();
} }
// Check account lockout status (Phase 2.6) // Check account lockout status (Phase 2.6)
if let Some(ref lockout) = state.account_lockout { if let Some(ref lockout) = state.account_lockout {
match lockout.check_lockout(&req.email).await { match lockout.check_lockout(&req.email).await {
Ok(true) => { Ok(true) => {
return ( return (StatusCode::TOO_MANY_REQUESTS, Json(serde_json::json!({
StatusCode::TOO_MANY_REQUESTS,
Json(serde_json::json!({
"error": "account is temporarily locked due to too many failed attempts", "error": "account is temporarily locked due to too many failed attempts",
"retry_after": "please try again later" "retry_after": "please try again later"
})), }))).into_response()
)
.into_response()
} }
Ok(false) => {} Ok(false) => {},
Err(e) => { Err(e) => {
tracing::error!("Failed to check lockout status: {}", e); tracing::error!("Failed to check lockout status: {}", e);
} }
@ -207,49 +177,33 @@ pub async fn login(
// Log failed login (Phase 2.6) // Log failed login (Phase 2.6)
if let Some(ref audit) = state.audit_logger { if let Some(ref audit) = state.audit_logger {
let _ = audit let _ = audit.log_event(
.log_event(
AuditEventType::LoginFailed, AuditEventType::LoginFailed,
None, None,
Some(req.email.clone()), Some(req.email.clone()),
"0.0.0.0".to_string(), // TODO: Extract real IP "0.0.0.0".to_string(), // TODO: Extract real IP
None, None,
None, None,
) ).await;
.await;
} }
return ( return (StatusCode::UNAUTHORIZED, Json(serde_json::json!({
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({
"error": "invalid credentials" "error": "invalid credentials"
})), }))).into_response()
)
.into_response();
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to find user: {}", e); tracing::error!("Failed to find user: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "database error" "error": "database error"
})), }))).into_response()
)
.into_response();
} }
}; };
let user_id = user let user_id = user.id.ok_or_else(|| {
.id (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
.ok_or_else(|| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "invalid user state" "error": "invalid user state"
})), })))
) }).unwrap();
})
.unwrap();
// Verify password // Verify password
match user.verify_password(&req.password) { match user.verify_password(&req.password) {
@ -258,7 +212,7 @@ pub async fn login(
if let Some(ref lockout) = state.account_lockout { if let Some(ref lockout) = state.account_lockout {
let _ = lockout.reset_attempts(&req.email).await; let _ = lockout.reset_attempts(&req.email).await;
} }
} },
Ok(false) => { Ok(false) => {
// Record failed attempt (Phase 2.6) // Record failed attempt (Phase 2.6)
if let Some(ref lockout) = state.account_lockout { if let Some(ref lockout) = state.account_lockout {
@ -267,35 +221,25 @@ pub async fn login(
// Log failed login (Phase 2.6) // Log failed login (Phase 2.6)
if let Some(ref audit) = state.audit_logger { if let Some(ref audit) = state.audit_logger {
let _ = audit let _ = audit.log_event(
.log_event(
AuditEventType::LoginFailed, AuditEventType::LoginFailed,
Some(user_id), Some(user_id),
Some(req.email.clone()), Some(req.email.clone()),
"0.0.0.0".to_string(), // TODO: Extract real IP "0.0.0.0".to_string(), // TODO: Extract real IP
None, None,
None, None,
) ).await;
.await;
} }
return ( return (StatusCode::UNAUTHORIZED, Json(serde_json::json!({
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({
"error": "invalid credentials" "error": "invalid credentials"
})), }))).into_response()
)
.into_response();
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to verify password: {}", e); tracing::error!("Failed to verify password: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "authentication error" "error": "authentication error"
})), }))).into_response()
)
.into_response();
} }
} }
@ -307,28 +251,22 @@ pub async fn login(
Ok(t) => t, Ok(t) => t,
Err(e) => { Err(e) => {
tracing::error!("Failed to generate token: {}", e); tracing::error!("Failed to generate token: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to generate token" "error": "failed to generate token"
})), }))).into_response()
)
.into_response();
} }
}; };
// Log successful login (Phase 2.6) // Log successful login (Phase 2.6)
if let Some(ref audit) = state.audit_logger { if let Some(ref audit) = state.audit_logger {
let _ = audit let _ = audit.log_event(
.log_event(
AuditEventType::LoginSuccess, AuditEventType::LoginSuccess,
Some(user_id), Some(user_id),
Some(req.email.clone()), Some(req.email.clone()),
"0.0.0.0".to_string(), // TODO: Extract real IP "0.0.0.0".to_string(), // TODO: Extract real IP
None, None,
None, None,
) ).await;
.await;
} }
let response = AuthResponse { let response = AuthResponse {
@ -356,76 +294,52 @@ pub async fn recover_password(
Json(req): Json<RecoverPasswordRequest>, Json(req): Json<RecoverPasswordRequest>,
) -> impl IntoResponse { ) -> impl IntoResponse {
if let Err(errors) = req.validate() { if let Err(errors) = req.validate() {
return ( return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "validation failed", "error": "validation failed",
"details": errors.to_string() "details": errors.to_string()
})), }))).into_response();
)
.into_response();
} }
// Find user by email // Find user by email
let mut user = match state.db.find_user_by_email(&req.email).await { let mut user = match state.db.find_user_by_email(&req.email).await {
Ok(Some(u)) => u, Ok(Some(u)) => u,
Ok(None) => { Ok(None) => {
return ( return (StatusCode::UNAUTHORIZED, Json(serde_json::json!({
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({
"error": "invalid credentials" "error": "invalid credentials"
})), }))).into_response()
)
.into_response()
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to find user: {}", e); tracing::error!("Failed to find user: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "database error" "error": "database error"
})), }))).into_response()
)
.into_response();
} }
}; };
// Verify recovery phrase // Verify recovery phrase
match user.verify_recovery_phrase(&req.recovery_phrase) { match user.verify_recovery_phrase(&req.recovery_phrase) {
Ok(true) => {} Ok(true) => {},
Ok(false) => { Ok(false) => {
return ( return (StatusCode::UNAUTHORIZED, Json(serde_json::json!({
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({
"error": "invalid credentials" "error": "invalid credentials"
})), }))).into_response()
)
.into_response()
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to verify recovery phrase: {}", e); tracing::error!("Failed to verify recovery phrase: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "authentication error" "error": "authentication error"
})), }))).into_response()
)
.into_response();
} }
} }
// Update password // Update password
match user.update_password(req.new_password) { match user.update_password(req.new_password) {
Ok(_) => {} Ok(_) => {},
Err(e) => { Err(e) => {
tracing::error!("Failed to update password: {}", e); tracing::error!("Failed to update password: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to update password" "error": "failed to update password"
})), }))).into_response()
)
.into_response();
} }
} }
@ -435,29 +349,23 @@ pub async fn recover_password(
// Log password recovery (Phase 2.6) // Log password recovery (Phase 2.6)
if let Some(ref audit) = state.audit_logger { if let Some(ref audit) = state.audit_logger {
let user_id_for_log = user.id; let user_id_for_log = user.id;
let _ = audit let _ = audit.log_event(
.log_event(
AuditEventType::PasswordRecovery, AuditEventType::PasswordRecovery,
user_id_for_log, user_id_for_log,
Some(req.email.clone()), Some(req.email.clone()),
"0.0.0.0".to_string(), "0.0.0.0".to_string(),
None, None,
None, None,
) ).await;
.await;
} }
(StatusCode::NO_CONTENT, ()).into_response() (StatusCode::NO_CONTENT, ()).into_response()
} },
Err(e) => { Err(e) => {
tracing::error!("Failed to save user: {}", e); tracing::error!("Failed to save user: {}", e);
( (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "database error" "error": "database error"
})), }))).into_response()
)
.into_response()
} }
} }
} }

View file

@ -1,6 +1,6 @@
use crate::config::AppState;
use axum::{extract::State, response::Json}; use axum::{extract::State, response::Json};
use serde_json::{json, Value}; use serde_json::{json, Value};
use crate::config::AppState;
pub async fn health_check(State(state): State<AppState>) -> Json<Value> { pub async fn health_check(State(state): State<AppState>) -> Json<Value> {
let status = if let Ok(_) = state.db.health_check().await { let status = if let Ok(_) = state.db.health_check().await {

View file

@ -1,14 +1,9 @@
use crate::auth::jwt::Claims; use axum::{Extension, Json, extract::{Path, State, Query}, http::StatusCode, response::IntoResponse};
use crate::config::AppState;
use crate::models::health_stats::HealthStatistic;
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
Extension, Json,
};
use mongodb::bson::oid::ObjectId; use mongodb::bson::oid::ObjectId;
use serde::Deserialize; use serde::Deserialize;
use crate::models::health_stats::HealthStatistic;
use crate::auth::jwt::Claims;
use crate::config::AppState;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct CreateHealthStatRequest { pub struct CreateHealthStatRequest {
@ -46,7 +41,7 @@ pub async fn create_health_stat(
// For complex objects like blood pressure, use a default // For complex objects like blood pressure, use a default
0.0 0.0
} }
_ => 0.0, _ => 0.0
}; };
let stat = HealthStatistic { let stat = HealthStatistic {
@ -66,11 +61,7 @@ pub async fn create_health_stat(
Ok(created) => (StatusCode::CREATED, Json(created)).into_response(), Ok(created) => (StatusCode::CREATED, Json(created)).into_response(),
Err(e) => { Err(e) => {
eprintln!("Error creating health stat: {:?}", 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(), Ok(stats) => (StatusCode::OK, Json(stats)).into_response(),
Err(e) => { Err(e) => {
eprintln!("Error fetching health stats: {:?}", 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(), Ok(None) => (StatusCode::NOT_FOUND, "Health stat not found").into_response(),
Err(e) => { Err(e) => {
eprintln!("Error fetching health stat: {:?}", 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 { let mut stat = match repo.find_by_id(&object_id).await {
Ok(Some(s)) => s, Ok(Some(s)) => s,
Ok(None) => return (StatusCode::NOT_FOUND, "Health stat not found").into_response(), Ok(None) => return (StatusCode::NOT_FOUND, "Health stat not found").into_response(),
Err(_) => { Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch health stat").into_response(),
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to fetch health stat",
)
.into_response()
}
}; };
if stat.user_id != claims.sub { if stat.user_id != claims.sub {
@ -154,7 +131,7 @@ pub async fn update_health_stat(
if let Some(value) = req.value { if let Some(value) = req.value {
let value_num = match value { let value_num = match value {
serde_json::Value::Number(n) => n.as_f64().unwrap_or(0.0), serde_json::Value::Number(n) => n.as_f64().unwrap_or(0.0),
_ => 0.0, _ => 0.0
}; };
stat.value = value_num; stat.value = value_num;
} }
@ -168,11 +145,7 @@ pub async fn update_health_stat(
match repo.update(&object_id, &stat).await { match repo.update(&object_id, &stat).await {
Ok(Some(updated)) => (StatusCode::OK, Json(updated)).into_response(), Ok(Some(updated)) => (StatusCode::OK, Json(updated)).into_response(),
Ok(None) => (StatusCode::NOT_FOUND, "Failed to update").into_response(), Ok(None) => (StatusCode::NOT_FOUND, "Failed to update").into_response(),
Err(_) => ( Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Failed to update health stat").into_response(),
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 { let stat = match repo.find_by_id(&object_id).await {
Ok(Some(s)) => s, Ok(Some(s)) => s,
Ok(None) => return (StatusCode::NOT_FOUND, "Health stat not found").into_response(), Ok(None) => return (StatusCode::NOT_FOUND, "Health stat not found").into_response(),
Err(_) => { Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch health stat").into_response(),
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to fetch health stat",
)
.into_response()
}
}; };
if stat.user_id != claims.sub { if stat.user_id != claims.sub {
@ -205,11 +172,7 @@ pub async fn delete_health_stat(
match repo.delete(&object_id).await { match repo.delete(&object_id).await {
Ok(true) => StatusCode::NO_CONTENT.into_response(), 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(),
} }
} }
@ -229,15 +192,11 @@ pub async fn get_health_trends(
// Calculate basic trend statistics // Calculate basic trend statistics
if filtered.is_empty() { if filtered.is_empty() {
return ( return (StatusCode::OK, Json(serde_json::json!({
StatusCode::OK,
Json(serde_json::json!({
"stat_type": query.stat_type, "stat_type": query.stat_type,
"count": 0, "count": 0,
"data": [] "data": []
})), }))).into_response();
)
.into_response();
} }
let values: Vec<f64> = filtered.iter().map(|s| s.value).collect(); let values: Vec<f64> = filtered.iter().map(|s| s.value).collect();
@ -258,11 +217,7 @@ pub async fn get_health_trends(
} }
Err(e) => { Err(e) => {
eprintln!("Error fetching health trends: {:?}", 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()
} }
} }
} }

View file

@ -35,9 +35,7 @@ pub async fn check_interactions(
return Err(StatusCode::BAD_REQUEST); return Err(StatusCode::BAD_REQUEST);
} }
let interaction_service = state let interaction_service = state.interaction_service.as_ref()
.interaction_service
.as_ref()
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?; .ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
match interaction_service match interaction_service
@ -78,9 +76,7 @@ pub async fn check_new_medication(
State(state): State<AppState>, State(state): State<AppState>,
Json(request): Json<CheckNewMedicationRequest>, Json(request): Json<CheckNewMedicationRequest>,
) -> Result<Json<InteractionResponse>, StatusCode> { ) -> Result<Json<InteractionResponse>, StatusCode> {
let interaction_service = state let interaction_service = state.interaction_service.as_ref()
.interaction_service
.as_ref()
.ok_or(StatusCode::SERVICE_UNAVAILABLE)?; .ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
match interaction_service match interaction_service

View file

@ -1,17 +1,14 @@
use axum::{ use axum::{
extract::{Extension, Json, Path, Query, State}, extract::{Path, Query, State, Extension, Json},
http::StatusCode, http::StatusCode,
}; };
use mongodb::bson::oid::ObjectId; use mongodb::bson::oid::ObjectId;
use std::time::SystemTime; use std::time::SystemTime;
use crate::{ use crate::{
models::medication::{Medication, MedicationRepository, CreateMedicationRequest, UpdateMedicationRequest, LogDoseRequest},
auth::jwt::Claims, // Fixed: import from auth::jwt instead of handlers::auth auth::jwt::Claims, // Fixed: import from auth::jwt instead of handlers::auth
config::AppState, config::AppState,
models::medication::{
CreateMedicationRequest, LogDoseRequest, Medication, MedicationRepository,
UpdateMedicationRequest,
},
}; };
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
@ -62,15 +59,12 @@ pub async fn create_medication(
user_id: claims.sub, user_id: claims.sub,
profile_id: req.profile_id.clone(), profile_id: req.profile_id.clone(),
medication_data, medication_data,
reminders: req reminders: req.reminder_times.unwrap_or_default().into_iter().map(|time| {
.reminder_times crate::models::medication::MedicationReminder {
.unwrap_or_default()
.into_iter()
.map(|time| crate::models::medication::MedicationReminder {
reminder_id: uuid::Uuid::new_v4().to_string(), reminder_id: uuid::Uuid::new_v4().to_string(),
scheduled_time: time, scheduled_time: time,
}) }
.collect(), }).collect(),
created_at: now.into(), created_at: now.into(),
updated_at: now.into(), updated_at: now.into(),
pill_identification: req.pill_identification, // Phase 2.8 pill_identification: req.pill_identification, // Phase 2.8
@ -92,7 +86,10 @@ pub async fn list_medications(
let _limit = query.limit.unwrap_or(100); 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)), Ok(medications) => Ok(Json(medications)),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
} }
@ -173,11 +170,7 @@ pub async fn log_dose(
notes: req.notes, notes: req.notes,
}; };
match database match database.collection("medication_doses").insert_one(dose.clone(), None).await {
.collection("medication_doses")
.insert_one(dose.clone(), None)
.await
{
Ok(_) => Ok(StatusCode::CREATED), Ok(_) => Ok(StatusCode::CREATED),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
} }

View file

@ -1,28 +1,20 @@
pub mod auth; pub mod auth;
pub mod health; pub mod health;
pub mod health_stats; pub mod health_stats;
pub mod interactions;
pub mod medications;
pub mod permissions; pub mod permissions;
pub mod sessions;
pub mod shares; pub mod shares;
pub mod users; pub mod users;
pub mod sessions;
pub mod medications;
pub mod interactions;
// Re-export commonly used handler functions // 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::{health_check, ready_check};
pub use health_stats::{ pub use shares::{create_share, list_shares, update_share, delete_share};
create_health_stat, delete_health_stat, get_health_stat, get_health_trends, list_health_stats,
update_health_stat,
};
pub use interactions::{check_interactions, check_new_medication};
pub use medications::{
create_medication, delete_medication, get_adherence, get_medication, list_medications,
log_dose, update_medication,
};
pub use permissions::check_permission; pub use permissions::check_permission;
pub use sessions::{get_sessions, revoke_all_sessions, revoke_session}; pub use users::{get_profile, update_profile, delete_account, change_password, get_settings, update_settings};
pub use shares::{create_share, delete_share, list_shares, update_share}; pub use sessions::{get_sessions, revoke_session, revoke_all_sessions};
pub use users::{ pub use medications::{create_medication, list_medications, get_medication, update_medication, delete_medication, log_dose, get_adherence};
change_password, delete_account, get_profile, get_settings, update_profile, update_settings, 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};

View file

@ -2,11 +2,15 @@ use axum::{
extract::{Query, State}, extract::{Query, State},
http::StatusCode, http::StatusCode,
response::IntoResponse, response::IntoResponse,
Extension, Json, Json,
Extension,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{auth::jwt::Claims, config::AppState}; use crate::{
auth::jwt::Claims,
config::AppState,
};
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct CheckPermissionQuery { pub struct CheckPermissionQuery {
@ -28,26 +32,18 @@ pub async fn check_permission(
Query(params): Query<CheckPermissionQuery>, Query(params): Query<CheckPermissionQuery>,
Extension(claims): Extension<Claims>, Extension(claims): Extension<Claims>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let has_permission = match state let has_permission = match state.db.check_user_permission(
.db
.check_user_permission(
&claims.sub, &claims.sub,
&params.resource_type, &params.resource_type,
&params.resource_id, &params.resource_id,
&params.permission, &params.permission,
) ).await {
.await
{
Ok(result) => result, Ok(result) => result,
Err(e) => { Err(e) => {
tracing::error!("Failed to check permission: {}", e); tracing::error!("Failed to check permission: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to check permission" "error": "failed to check permission"
})), }))).into_response();
)
.into_response();
} }
}; };

View file

@ -1,10 +1,6 @@
use axum::{extract::{Path, State, Extension}, http::StatusCode, Json};
use crate::auth::jwt::Claims; use crate::auth::jwt::Claims;
use crate::config::AppState; use crate::config::AppState;
use axum::{
extract::{Extension, Path, State},
http::StatusCode,
Json,
};
use serde::Serialize; use serde::Serialize;
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]

View file

@ -2,16 +2,17 @@ use axum::{
extract::{Path, State}, extract::{Path, State},
http::StatusCode, http::StatusCode,
response::IntoResponse, response::IntoResponse,
Extension, Json, Json,
Extension,
}; };
use mongodb::bson::oid::ObjectId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use validator::Validate; use validator::Validate;
use mongodb::bson::oid::ObjectId;
use crate::{ use crate::{
auth::jwt::Claims, auth::jwt::Claims,
config::AppState, config::AppState,
models::{permission::Permission, share::Share}, models::{share::Share, permission::Permission},
}; };
#[derive(Debug, Deserialize, Validate)] #[derive(Debug, Deserialize, Validate)]
@ -45,11 +46,7 @@ impl TryFrom<Share> for ShareResponse {
target_user_id: share.target_user_id.to_string(), target_user_id: share.target_user_id.to_string(),
resource_type: share.resource_type, resource_type: share.resource_type,
resource_id: share.resource_id.map(|id| id.to_string()), resource_id: share.resource_id.map(|id| id.to_string()),
permissions: share permissions: share.permissions.into_iter().map(|p| p.to_string()).collect(),
.permissions
.into_iter()
.map(|p| p.to_string())
.collect(),
expires_at: share.expires_at.map(|dt| dt.timestamp_millis()), expires_at: share.expires_at.map(|dt| dt.timestamp_millis()),
created_at: share.created_at.timestamp_millis(), created_at: share.created_at.timestamp_millis(),
active: share.active, active: share.active,
@ -63,86 +60,63 @@ pub async fn create_share(
Json(req): Json<CreateShareRequest>, Json(req): Json<CreateShareRequest>,
) -> impl IntoResponse { ) -> impl IntoResponse {
if let Err(errors) = req.validate() { if let Err(errors) = req.validate() {
return ( return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "validation failed", "error": "validation failed",
"details": errors.to_string() "details": errors.to_string()
})), }))).into_response();
)
.into_response();
} }
// Find target user by email // Find target user by email
let target_user = match state.db.find_user_by_email(&req.target_user_email).await { let target_user = match state.db.find_user_by_email(&req.target_user_email).await {
Ok(Some(user)) => user, Ok(Some(user)) => user,
Ok(None) => { Ok(None) => {
return ( return (StatusCode::NOT_FOUND, Json(serde_json::json!({
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "target user not found" "error": "target user not found"
})), }))).into_response();
)
.into_response();
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to find target user: {}", e); tracing::error!("Failed to find target user: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "database error" "error": "database error"
})), }))).into_response();
)
.into_response();
} }
}; };
let target_user_id = match target_user.id { let target_user_id = match target_user.id {
Some(id) => id, Some(id) => id,
None => { None => {
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "target user has no ID" "error": "target user has no ID"
})), }))).into_response();
)
.into_response();
} }
}; };
let owner_id = match ObjectId::parse_str(&claims.sub) { let owner_id = match ObjectId::parse_str(&claims.sub) {
Ok(id) => id, Ok(id) => id,
Err(_) => { Err(_) => {
return ( return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "invalid user ID format" "error": "invalid user ID format"
})), }))).into_response();
)
.into_response();
} }
}; };
// Parse resource_id if provided // Parse resource_id if provided
let resource_id = match req.resource_id { let resource_id = match req.resource_id {
Some(id) => match ObjectId::parse_str(&id) { Some(id) => {
match ObjectId::parse_str(&id) {
Ok(oid) => Some(oid), Ok(oid) => Some(oid),
Err(_) => { Err(_) => {
return ( return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "invalid resource_id format" "error": "invalid resource_id format"
})), }))).into_response();
) }
.into_response();
} }
}, },
None => None, None => None,
}; };
// Parse permissions - support all permission types // Parse permissions - support all permission types
let permissions: Vec<Permission> = req let permissions: Vec<Permission> = req.permissions
.permissions
.into_iter() .into_iter()
.filter_map(|p| match p.to_lowercase().as_str() { .filter_map(|p| match p.to_lowercase().as_str() {
"read" => Some(Permission::Read), "read" => Some(Permission::Read),
@ -179,26 +153,18 @@ pub async fn create_share(
let response: ShareResponse = match share.try_into() { let response: ShareResponse = match share.try_into() {
Ok(r) => r, Ok(r) => r,
Err(_) => { Err(_) => {
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to create share response" "error": "failed to create share response"
})), }))).into_response();
)
.into_response();
} }
}; };
(StatusCode::CREATED, Json(response)).into_response() (StatusCode::CREATED, Json(response)).into_response()
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to create share: {}", e); tracing::error!("Failed to create share: {}", e);
( (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to create share" "error": "failed to create share"
})), }))).into_response()
)
.into_response()
} }
} }
} }
@ -219,50 +185,39 @@ pub async fn list_shares(
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to list shares: {}", e); tracing::error!("Failed to list shares: {}", e);
( (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to list shares" "error": "failed to list shares"
})), }))).into_response()
)
.into_response()
} }
} }
} }
pub async fn get_share(State(state): State<AppState>, Path(id): Path<String>) -> impl IntoResponse { pub async fn get_share(
State(state): State<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
match state.db.get_share(&id).await { match state.db.get_share(&id).await {
Ok(Some(share)) => { Ok(Some(share)) => {
let response: ShareResponse = match share.try_into() { let response: ShareResponse = match share.try_into() {
Ok(r) => r, Ok(r) => r,
Err(_) => { Err(_) => {
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to create share response" "error": "failed to create share response"
})), }))).into_response();
)
.into_response();
} }
}; };
(StatusCode::OK, Json(response)).into_response() (StatusCode::OK, Json(response)).into_response()
} }
Ok(None) => ( Ok(None) => {
StatusCode::NOT_FOUND, (StatusCode::NOT_FOUND, Json(serde_json::json!({
Json(serde_json::json!({
"error": "share not found" "error": "share not found"
})), }))).into_response()
) }
.into_response(),
Err(e) => { Err(e) => {
tracing::error!("Failed to get share: {}", e); tracing::error!("Failed to get share: {}", e);
( (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to get share" "error": "failed to get share"
})), }))).into_response()
)
.into_response()
} }
} }
} }
@ -286,23 +241,15 @@ pub async fn update_share(
let mut share = match state.db.get_share(&id).await { let mut share = match state.db.get_share(&id).await {
Ok(Some(s)) => s, Ok(Some(s)) => s,
Ok(None) => { Ok(None) => {
return ( return (StatusCode::NOT_FOUND, Json(serde_json::json!({
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "share not found" "error": "share not found"
})), }))).into_response();
)
.into_response();
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to get share: {}", e); tracing::error!("Failed to get share: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to get share" "error": "failed to get share"
})), }))).into_response()
)
.into_response();
} }
}; };
@ -310,24 +257,16 @@ pub async fn update_share(
let owner_id = match ObjectId::parse_str(&claims.sub) { let owner_id = match ObjectId::parse_str(&claims.sub) {
Ok(id) => id, Ok(id) => id,
Err(_) => { Err(_) => {
return ( return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "invalid user ID format" "error": "invalid user ID format"
})), }))).into_response();
)
.into_response();
} }
}; };
if share.owner_id != owner_id { if share.owner_id != owner_id {
return ( return (StatusCode::FORBIDDEN, Json(serde_json::json!({
StatusCode::FORBIDDEN,
Json(serde_json::json!({
"error": "not authorized to modify this share" "error": "not authorized to modify this share"
})), }))).into_response();
)
.into_response();
} }
// Update fields // Update fields
@ -350,9 +289,7 @@ pub async fn update_share(
} }
if let Some(days) = req.expires_days { if let Some(days) = req.expires_days {
share.expires_at = Some( share.expires_at = Some(mongodb::bson::DateTime::now().saturating_add_millis(days as i64 * 24 * 60 * 60 * 1000));
mongodb::bson::DateTime::now().saturating_add_millis(days as i64 * 24 * 60 * 60 * 1000),
);
} }
match state.db.update_share(&share).await { match state.db.update_share(&share).await {
@ -360,26 +297,18 @@ pub async fn update_share(
let response: ShareResponse = match share.try_into() { let response: ShareResponse = match share.try_into() {
Ok(r) => r, Ok(r) => r,
Err(_) => { Err(_) => {
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to create share response" "error": "failed to create share response"
})), }))).into_response();
)
.into_response();
} }
}; };
(StatusCode::OK, Json(response)).into_response() (StatusCode::OK, Json(response)).into_response()
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to update share: {}", e); tracing::error!("Failed to update share: {}", e);
( (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to update share" "error": "failed to update share"
})), }))).into_response()
)
.into_response()
} }
} }
} }
@ -393,23 +322,15 @@ pub async fn delete_share(
let share = match state.db.get_share(&id).await { let share = match state.db.get_share(&id).await {
Ok(Some(s)) => s, Ok(Some(s)) => s,
Ok(None) => { Ok(None) => {
return ( return (StatusCode::NOT_FOUND, Json(serde_json::json!({
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "share not found" "error": "share not found"
})), }))).into_response()
)
.into_response()
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to get share: {}", e); tracing::error!("Failed to get share: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to get share" "error": "failed to get share"
})), }))).into_response()
)
.into_response();
} }
}; };
@ -417,37 +338,25 @@ pub async fn delete_share(
let owner_id = match ObjectId::parse_str(&claims.sub) { let owner_id = match ObjectId::parse_str(&claims.sub) {
Ok(id) => id, Ok(id) => id,
Err(_) => { Err(_) => {
return ( return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "invalid user ID format" "error": "invalid user ID format"
})), }))).into_response();
)
.into_response();
} }
}; };
if share.owner_id != owner_id { if share.owner_id != owner_id {
return ( return (StatusCode::FORBIDDEN, Json(serde_json::json!({
StatusCode::FORBIDDEN,
Json(serde_json::json!({
"error": "not authorized to delete this share" "error": "not authorized to delete this share"
})), }))).into_response()
)
.into_response();
} }
match state.db.delete_share(&id).await { match state.db.delete_share(&id).await {
Ok(_) => (StatusCode::NO_CONTENT, ()).into_response(), Ok(_) => (StatusCode::NO_CONTENT, ()).into_response(),
Err(e) => { Err(e) => {
tracing::error!("Failed to delete share: {}", e); tracing::error!("Failed to delete share: {}", e);
( (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to delete share" "error": "failed to delete share"
})), }))).into_response()
)
.into_response()
} }
} }
} }

View file

@ -1,9 +1,19 @@
use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, Json}; use axum::{
use mongodb::bson::oid::ObjectId; extract::{State},
http::StatusCode,
response::IntoResponse,
Json,
Extension,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use validator::Validate; 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)] #[derive(Debug, Serialize)]
pub struct UserProfileResponse { pub struct UserProfileResponse {
@ -47,22 +57,16 @@ pub async fn get_profile(
let response: UserProfileResponse = user.try_into().unwrap(); let response: UserProfileResponse = user.try_into().unwrap();
(StatusCode::OK, Json(response)).into_response() (StatusCode::OK, Json(response)).into_response()
} }
Ok(None) => ( Ok(None) => {
StatusCode::NOT_FOUND, (StatusCode::NOT_FOUND, Json(serde_json::json!({
Json(serde_json::json!({
"error": "user not found" "error": "user not found"
})), }))).into_response()
) }
.into_response(),
Err(e) => { Err(e) => {
tracing::error!("Failed to get user profile: {}", e); tracing::error!("Failed to get user profile: {}", e);
( (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to get profile" "error": "failed to get profile"
})), }))).into_response()
)
.into_response()
} }
} }
} }
@ -73,14 +77,10 @@ pub async fn update_profile(
Json(req): Json<UpdateProfileRequest>, Json(req): Json<UpdateProfileRequest>,
) -> impl IntoResponse { ) -> impl IntoResponse {
if let Err(errors) = req.validate() { if let Err(errors) = req.validate() {
return ( return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "validation failed", "error": "validation failed",
"details": errors.to_string() "details": errors.to_string()
})), }))).into_response();
)
.into_response();
} }
let user_id = ObjectId::parse_str(&claims.sub).unwrap(); let user_id = ObjectId::parse_str(&claims.sub).unwrap();
@ -88,23 +88,15 @@ pub async fn update_profile(
let mut user = match state.db.find_user_by_id(&user_id).await { let mut user = match state.db.find_user_by_id(&user_id).await {
Ok(Some(u)) => u, Ok(Some(u)) => u,
Ok(None) => { Ok(None) => {
return ( return (StatusCode::NOT_FOUND, Json(serde_json::json!({
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "user not found" "error": "user not found"
})), }))).into_response()
)
.into_response()
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to get user: {}", e); tracing::error!("Failed to get user: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "database error" "error": "database error"
})), }))).into_response()
)
.into_response();
} }
}; };
@ -119,13 +111,9 @@ pub async fn update_profile(
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to update user: {}", e); tracing::error!("Failed to update user: {}", e);
( (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to update profile" "error": "failed to update profile"
})), }))).into_response()
)
.into_response()
} }
} }
} }
@ -140,13 +128,9 @@ pub async fn delete_account(
Ok(_) => (StatusCode::NO_CONTENT, ()).into_response(), Ok(_) => (StatusCode::NO_CONTENT, ()).into_response(),
Err(e) => { Err(e) => {
tracing::error!("Failed to delete user: {}", e); tracing::error!("Failed to delete user: {}", e);
( (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to delete account" "error": "failed to delete account"
})), }))).into_response()
)
.into_response()
} }
} }
} }
@ -165,14 +149,10 @@ pub async fn change_password(
Json(req): Json<ChangePasswordRequest>, Json(req): Json<ChangePasswordRequest>,
) -> impl IntoResponse { ) -> impl IntoResponse {
if let Err(errors) = req.validate() { if let Err(errors) = req.validate() {
return ( return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "validation failed", "error": "validation failed",
"details": errors.to_string() "details": errors.to_string()
})), }))).into_response();
)
.into_response();
} }
let user_id = ObjectId::parse_str(&claims.sub).unwrap(); let user_id = ObjectId::parse_str(&claims.sub).unwrap();
@ -180,62 +160,42 @@ pub async fn change_password(
let mut user = match state.db.find_user_by_id(&user_id).await { let mut user = match state.db.find_user_by_id(&user_id).await {
Ok(Some(u)) => u, Ok(Some(u)) => u,
Ok(None) => { Ok(None) => {
return ( return (StatusCode::NOT_FOUND, Json(serde_json::json!({
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "user not found" "error": "user not found"
})), }))).into_response()
)
.into_response()
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to get user: {}", e); tracing::error!("Failed to get user: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "database error" "error": "database error"
})), }))).into_response()
)
.into_response();
} }
}; };
// Verify current password // Verify current password
match user.verify_password(&req.current_password) { match user.verify_password(&req.current_password) {
Ok(true) => {} Ok(true) => {},
Ok(false) => { Ok(false) => {
return ( return (StatusCode::UNAUTHORIZED, Json(serde_json::json!({
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({
"error": "current password is incorrect" "error": "current password is incorrect"
})), }))).into_response()
)
.into_response()
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to verify password: {}", e); tracing::error!("Failed to verify password: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to verify password" "error": "failed to verify password"
})), }))).into_response()
)
.into_response();
} }
} }
// Update password // Update password
match user.update_password(req.new_password) { match user.update_password(req.new_password) {
Ok(_) => {} Ok(_) => {},
Err(e) => { Err(e) => {
tracing::error!("Failed to hash new password: {}", e); tracing::error!("Failed to hash new password: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to update password" "error": "failed to update password"
})), }))).into_response()
)
.into_response();
} }
} }
@ -243,13 +203,9 @@ pub async fn change_password(
Ok(_) => (StatusCode::NO_CONTENT, ()).into_response(), Ok(_) => (StatusCode::NO_CONTENT, ()).into_response(),
Err(e) => { Err(e) => {
tracing::error!("Failed to update user: {}", e); tracing::error!("Failed to update user: {}", e);
( (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to update password" "error": "failed to update password"
})), }))).into_response()
)
.into_response()
} }
} }
} }
@ -280,22 +236,16 @@ pub async fn get_settings(
let response: UserSettingsResponse = user.into(); let response: UserSettingsResponse = user.into();
(StatusCode::OK, Json(response)).into_response() (StatusCode::OK, Json(response)).into_response()
} }
Ok(None) => ( Ok(None) => {
StatusCode::NOT_FOUND, (StatusCode::NOT_FOUND, Json(serde_json::json!({
Json(serde_json::json!({
"error": "user not found" "error": "user not found"
})), }))).into_response()
) }
.into_response(),
Err(e) => { Err(e) => {
tracing::error!("Failed to get user: {}", e); tracing::error!("Failed to get user: {}", e);
( (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to get settings" "error": "failed to get settings"
})), }))).into_response()
)
.into_response()
} }
} }
} }
@ -315,23 +265,15 @@ pub async fn update_settings(
let mut user = match state.db.find_user_by_id(&user_id).await { let mut user = match state.db.find_user_by_id(&user_id).await {
Ok(Some(u)) => u, Ok(Some(u)) => u,
Ok(None) => { Ok(None) => {
return ( return (StatusCode::NOT_FOUND, Json(serde_json::json!({
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": "user not found" "error": "user not found"
})), }))).into_response()
)
.into_response()
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to get user: {}", e); tracing::error!("Failed to get user: {}", e);
return ( return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "database error" "error": "database error"
})), }))).into_response()
)
.into_response();
} }
}; };
@ -349,13 +291,9 @@ pub async fn update_settings(
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to update user: {}", e); tracing::error!("Failed to update user: {}", e);
( (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "failed to update settings" "error": "failed to update settings"
})), }))).into_response()
)
.into_response()
} }
} }
} }

View file

@ -1,20 +1,23 @@
mod auth;
mod config; mod config;
mod db; mod db;
mod models;
mod auth;
mod handlers; mod handlers;
mod middleware; mod middleware;
mod models;
mod security; mod security;
mod services; mod services;
use axum::{ use axum::{
routing::{delete, get, post, put}, routing::{get, post, put, delete},
Router, Router,
}; };
use tower::ServiceBuilder;
use tower_http::{
cors::CorsLayer,
trace::TraceLayer,
};
use config::Config; use config::Config;
use std::sync::Arc; use std::sync::Arc;
use tower::ServiceBuilder;
use tower_http::{cors::CorsLayer, trace::TraceLayer};
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
@ -40,10 +43,7 @@ async fn main() -> anyhow::Result<()> {
eprintln!("Connecting to MongoDB..."); eprintln!("Connecting to MongoDB...");
let db = match db::MongoDb::new(&config.database.uri, &config.database.database).await { let db = match db::MongoDb::new(&config.database.uri, &config.database.database).await {
Ok(db) => { Ok(db) => {
tracing::info!( tracing::info!("Connected to MongoDB database: {}", config.database.database);
"Connected to MongoDB database: {}",
config.database.database
);
eprintln!("MongoDB connection successful"); eprintln!("MongoDB connection successful");
db db
} }
@ -107,17 +107,11 @@ async fn main() -> anyhow::Result<()> {
// Build public routes (no auth required) // Build public routes (no auth required)
let public_routes = Router::new() let public_routes = Router::new()
.route( .route("/health", get(handlers::health_check).head(handlers::health_check))
"/health",
get(handlers::health_check).head(handlers::health_check),
)
.route("/ready", get(handlers::ready_check)) .route("/ready", get(handlers::ready_check))
.route("/api/auth/register", post(handlers::register)) .route("/api/auth/register", post(handlers::register))
.route("/api/auth/login", post(handlers::login)) .route("/api/auth/login", post(handlers::login))
.route( .route("/api/auth/recover-password", post(handlers::recover_password));
"/api/auth/recover-password",
post(handlers::recover_password),
);
// Build protected routes (auth required) // Build protected routes (auth required)
let protected_routes = Router::new() let protected_routes = Router::new()

View file

@ -1,11 +1,11 @@
use crate::auth::jwt::Claims;
use crate::config::AppState;
use axum::{ use axum::{
extract::{Request, State}, extract::{Request, State},
http::StatusCode, http::StatusCode,
middleware::Next, middleware::Next,
response::Response, response::Response,
}; };
use crate::auth::jwt::Claims;
use crate::config::AppState;
pub async fn jwt_auth_middleware( pub async fn jwt_auth_middleware(
State(state): State<AppState>, State(state): State<AppState>,

View file

@ -1,8 +1,8 @@
pub mod auth; pub mod auth;
pub mod rate_limit; pub mod rate_limit;
pub use auth::jwt_auth_middleware;
pub use rate_limit::general_rate_limit_middleware; pub use rate_limit::general_rate_limit_middleware;
pub use auth::jwt_auth_middleware;
// Simple security headers middleware // Simple security headers middleware
pub async fn security_headers_middleware( pub async fn security_headers_middleware(
@ -15,14 +15,8 @@ pub async fn security_headers_middleware(
headers.insert("X-Content-Type-Options", "nosniff".parse().unwrap()); headers.insert("X-Content-Type-Options", "nosniff".parse().unwrap());
headers.insert("X-Frame-Options", "DENY".parse().unwrap()); headers.insert("X-Frame-Options", "DENY".parse().unwrap());
headers.insert("X-XSS-Protection", "1; mode=block".parse().unwrap()); headers.insert("X-XSS-Protection", "1; mode=block".parse().unwrap());
headers.insert( headers.insert("Strict-Transport-Security", "max-age=31536000; includeSubDomains".parse().unwrap());
"Strict-Transport-Security", headers.insert("Content-Security-Policy", "default-src 'self'".parse().unwrap());
"max-age=31536000; includeSubDomains".parse().unwrap(),
);
headers.insert(
"Content-Security-Policy",
"default-src 'self'".parse().unwrap(),
);
response response
} }

View file

@ -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 /// Middleware for general rate limiting
/// NOTE: Currently a stub implementation. TODO: Implement IP-based 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 /// Middleware for auth endpoint rate limiting
/// NOTE: Currently a stub implementation. TODO: Implement IP-based 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<Response, StatusCode> { pub async fn auth_rate_limit_middleware(
req: Request,
next: Next,
) -> Result<Response, StatusCode> {
// TODO: Implement proper rate limiting with IP-based tracking // TODO: Implement proper rate limiting with IP-based tracking
// For now, just pass through // For now, just pass through
Ok(next.run(req).await) Ok(next.run(req).await)

View file

@ -1,10 +1,10 @@
use anyhow::Result;
use futures::stream::TryStreamExt;
use mongodb::{ use mongodb::{
bson::{doc, oid::ObjectId},
Collection, Collection,
bson::{doc, oid::ObjectId},
}; };
use futures::stream::TryStreamExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use anyhow::Result;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AuditEventType { pub enum AuditEventType {
@ -87,8 +87,7 @@ impl AuditLogRepository {
} }
pub async fn find_by_user(&self, user_id: &ObjectId) -> Result<Vec<AuditLog>> { pub async fn find_by_user(&self, user_id: &ObjectId) -> Result<Vec<AuditLog>> {
let cursor = self let cursor = self.collection
.collection
.find( .find(
doc! { doc! {
"user_id": user_id "user_id": user_id
@ -109,7 +108,9 @@ impl AuditLogRepository {
.limit(limit as i64) .limit(limit as i64)
.build(); .build();
let cursor = self.collection.find(doc! {}, opts).await?; let cursor = self.collection
.find(doc! {}, opts)
.await?;
let logs: Vec<AuditLog> = cursor.try_collect().await?; let logs: Vec<AuditLog> = cursor.try_collect().await?;
Ok(logs) Ok(logs)

View file

@ -1,8 +1,5 @@
use mongodb::{
bson::{doc, oid::ObjectId, DateTime},
Collection,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use mongodb::{bson::{doc, oid::ObjectId, DateTime}, Collection};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Family { pub struct Family {
@ -38,10 +35,7 @@ impl FamilyRepository {
Ok(()) Ok(())
} }
pub async fn find_by_family_id( pub async fn find_by_family_id(&self, family_id: &str) -> mongodb::error::Result<Option<Family>> {
&self,
family_id: &str,
) -> mongodb::error::Result<Option<Family>> {
self.collection self.collection
.find_one(doc! { "familyId": family_id }, None) .find_one(doc! { "familyId": family_id }, None)
.await .await

View file

@ -1,5 +1,5 @@
use mongodb::bson::{oid::ObjectId, DateTime};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use mongodb::bson::{oid::ObjectId, DateTime};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthData { pub struct HealthData {

View file

@ -1,10 +1,7 @@
use futures::stream::TryStreamExt;
use mongodb::Collection; use mongodb::Collection;
use mongodb::{
bson::{doc, oid::ObjectId},
error::Error as MongoError,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use mongodb::{bson::{oid::ObjectId, doc}, error::Error as MongoError};
use futures::stream::TryStreamExt;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthStatistic { pub struct HealthStatistic {
@ -49,11 +46,7 @@ impl HealthStatisticsRepository {
self.collection.find_one(filter, None).await self.collection.find_one(filter, None).await
} }
pub async fn update( pub async fn update(&self, id: &ObjectId, stat: &HealthStatistic) -> Result<Option<HealthStatistic>, MongoError> {
&self,
id: &ObjectId,
stat: &HealthStatistic,
) -> Result<Option<HealthStatistic>, MongoError> {
let filter = doc! { "_id": id }; let filter = doc! { "_id": id };
self.collection.replace_one(filter, stat, None).await?; self.collection.replace_one(filter, stat, None).await?;
Ok(Some(stat.clone())) Ok(Some(stat.clone()))

View file

@ -2,8 +2,8 @@
//! //!
//! Database models for drug interactions //! Database models for drug interactions
use mongodb::bson::{oid::ObjectId, DateTime};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use mongodb::bson::{oid::ObjectId, DateTime};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DrugInteraction { pub struct DrugInteraction {

View file

@ -1,6 +1,6 @@
use futures::stream::TryStreamExt;
use mongodb::{bson::oid::ObjectId, Collection};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use mongodb::{bson::oid::ObjectId, Collection};
use futures::stream::TryStreamExt;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LabResult { pub struct LabResult {
@ -41,18 +41,12 @@ impl LabResultRepository {
Self { collection } Self { collection }
} }
pub async fn create( pub async fn create(&self, lab_result: LabResult) -> Result<LabResult, Box<dyn std::error::Error>> {
&self,
lab_result: LabResult,
) -> Result<LabResult, Box<dyn std::error::Error>> {
self.collection.insert_one(lab_result.clone(), None).await?; self.collection.insert_one(lab_result.clone(), None).await?;
Ok(lab_result) Ok(lab_result)
} }
pub async fn list_by_user( pub async fn list_by_user(&self, user_id: &str) -> Result<Vec<LabResult>, Box<dyn std::error::Error>> {
&self,
user_id: &str,
) -> Result<Vec<LabResult>, Box<dyn std::error::Error>> {
let filter = mongodb::bson::doc! { let filter = mongodb::bson::doc! {
"userId": user_id "userId": user_id
}; };

View file

@ -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 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) // PILL IDENTIFICATION (Phase 2.8)
@ -217,18 +217,12 @@ impl MedicationRepository {
Self { collection } Self { collection }
} }
pub async fn create( pub async fn create(&self, medication: Medication) -> Result<Medication, Box<dyn std::error::Error>> {
&self,
medication: Medication,
) -> Result<Medication, Box<dyn std::error::Error>> {
let _result = self.collection.insert_one(medication.clone(), None).await?; let _result = self.collection.insert_one(medication.clone(), None).await?;
Ok(medication) Ok(medication)
} }
pub async fn find_by_user( pub async fn find_by_user(&self, user_id: &str) -> Result<Vec<Medication>, Box<dyn std::error::Error>> {
&self,
user_id: &str,
) -> Result<Vec<Medication>, Box<dyn std::error::Error>> {
let filter = doc! { "userId": user_id }; let filter = doc! { "userId": user_id };
let mut cursor = self.collection.find(filter, None).await?; let mut cursor = self.collection.find(filter, None).await?;
let mut medications = Vec::new(); let mut medications = Vec::new();
@ -238,11 +232,7 @@ impl MedicationRepository {
Ok(medications) Ok(medications)
} }
pub async fn find_by_user_and_profile( pub async fn find_by_user_and_profile(&self, user_id: &str, profile_id: &str) -> Result<Vec<Medication>, Box<dyn std::error::Error>> {
&self,
user_id: &str,
profile_id: &str,
) -> Result<Vec<Medication>, Box<dyn std::error::Error>> {
let filter = doc! { let filter = doc! {
"userId": user_id, "userId": user_id,
"profileId": profile_id "profileId": profile_id
@ -255,20 +245,13 @@ impl MedicationRepository {
Ok(medications) Ok(medications)
} }
pub async fn find_by_id( pub async fn find_by_id(&self, id: &ObjectId) -> Result<Option<Medication>, Box<dyn std::error::Error>> {
&self,
id: &ObjectId,
) -> Result<Option<Medication>, Box<dyn std::error::Error>> {
let filter = doc! { "_id": id }; let filter = doc! { "_id": id };
let medication = self.collection.find_one(filter, None).await?; let medication = self.collection.find_one(filter, None).await?;
Ok(medication) Ok(medication)
} }
pub async fn update( pub async fn update(&self, id: &ObjectId, updates: UpdateMedicationRequest) -> Result<Option<Medication>, Box<dyn std::error::Error>> {
&self,
id: &ObjectId,
updates: UpdateMedicationRequest,
) -> Result<Option<Medication>, Box<dyn std::error::Error>> {
let mut update_doc = doc! {}; let mut update_doc = doc! {};
if let Some(name) = updates.name { if let Some(name) = updates.name {
@ -322,10 +305,7 @@ impl MedicationRepository {
update_doc.insert("updatedAt", mongodb::bson::DateTime::now()); update_doc.insert("updatedAt", mongodb::bson::DateTime::now());
let filter = doc! { "_id": id }; let filter = doc! { "_id": id };
let medication = self let medication = self.collection.find_one_and_update(filter, doc! { "$set": update_doc }, None).await?;
.collection
.find_one_and_update(filter, doc! { "$set": update_doc }, None)
.await?;
Ok(medication) Ok(medication)
} }
@ -335,11 +315,7 @@ impl MedicationRepository {
Ok(result.deleted_count > 0) Ok(result.deleted_count > 0)
} }
pub async fn calculate_adherence( pub async fn calculate_adherence(&self, medication_id: &str, days: i64) -> Result<AdherenceStats, Box<dyn std::error::Error>> {
&self,
medication_id: &str,
days: i64,
) -> Result<AdherenceStats, Box<dyn std::error::Error>> {
// For now, return a placeholder adherence calculation // For now, return a placeholder adherence calculation
// In a full implementation, this would query the medication_doses collection // In a full implementation, this would query the medication_doses collection
Ok(AdherenceStats { Ok(AdherenceStats {

View file

@ -2,7 +2,6 @@ pub mod audit_log;
pub mod family; pub mod family;
pub mod health_data; pub mod health_data;
pub mod health_stats; pub mod health_stats;
pub mod interactions;
pub mod lab_result; pub mod lab_result;
pub mod medication; pub mod medication;
pub mod permission; pub mod permission;
@ -11,3 +10,4 @@ pub mod refresh_token;
pub mod session; pub mod session;
pub mod share; pub mod share;
pub mod user; pub mod user;
pub mod interactions;

View file

@ -1,8 +1,5 @@
use mongodb::{
bson::{doc, oid::ObjectId, DateTime},
Collection,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use mongodb::{bson::{doc, oid::ObjectId, DateTime}, Collection};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Profile { pub struct Profile {
@ -44,10 +41,7 @@ impl ProfileRepository {
Ok(()) Ok(())
} }
pub async fn find_by_profile_id( pub async fn find_by_profile_id(&self, profile_id: &str) -> mongodb::error::Result<Option<Profile>> {
&self,
profile_id: &str,
) -> mongodb::error::Result<Option<Profile>> {
self.collection self.collection
.find_one(doc! { "profileId": profile_id }, None) .find_one(doc! { "profileId": profile_id }, None)
.await .await

View file

@ -1,5 +1,5 @@
use mongodb::bson::{oid::ObjectId, DateTime};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use mongodb::bson::{oid::ObjectId, DateTime};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RefreshToken { pub struct RefreshToken {

View file

@ -1,10 +1,10 @@
use anyhow::Result;
use futures::stream::TryStreamExt;
use mongodb::{ use mongodb::{
bson::{doc, oid::ObjectId},
Collection, Collection,
bson::{doc, oid::ObjectId},
}; };
use futures::stream::TryStreamExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use anyhow::Result;
use std::time::SystemTime; use std::time::SystemTime;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -65,17 +65,11 @@ impl SessionRepository {
is_revoked: false, is_revoked: false,
}; };
self.collection self.collection.insert_one(session, None).await?.inserted_id.as_object_id().ok_or_else(|| anyhow::anyhow!("Failed to get inserted id"))
.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<Vec<Session>> { pub async fn find_by_user(&self, user_id: &ObjectId) -> Result<Vec<Session>> {
let cursor = self let cursor = self.collection
.collection
.find( .find(
doc! { doc! {
"user_id": user_id, "user_id": user_id,
@ -116,8 +110,7 @@ impl SessionRepository {
} }
pub async fn cleanup_expired(&self) -> Result<u64> { pub async fn cleanup_expired(&self) -> Result<u64> {
let result = self let result = self.collection
.collection
.delete_many( .delete_many(
doc! { doc! {
"expires_at": { "$lt": mongodb::bson::DateTime::now() } "expires_at": { "$lt": mongodb::bson::DateTime::now() }

View file

@ -1,7 +1,7 @@
use mongodb::bson::DateTime;
use mongodb::bson::{doc, oid::ObjectId}; use mongodb::bson::{doc, oid::ObjectId};
use mongodb::Collection; use mongodb::Collection;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use mongodb::bson::DateTime;
use super::permission::Permission; use super::permission::Permission;
@ -90,17 +90,11 @@ impl ShareRepository {
.map_err(|e| mongodb::error::Error::from(e)) .map_err(|e| mongodb::error::Error::from(e))
} }
pub async fn find_by_target( pub async fn find_by_target(&self, target_user_id: &ObjectId) -> mongodb::error::Result<Vec<Share>> {
&self,
target_user_id: &ObjectId,
) -> mongodb::error::Result<Vec<Share>> {
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
self.collection self.collection
.find( .find(doc! { "target_user_id": target_user_id, "active": true }, None)
doc! { "target_user_id": target_user_id, "active": true },
None,
)
.await? .await?
.try_collect() .try_collect()
.await .await
@ -109,17 +103,13 @@ impl ShareRepository {
pub async fn update(&self, share: &Share) -> mongodb::error::Result<()> { pub async fn update(&self, share: &Share) -> mongodb::error::Result<()> {
if let Some(id) = &share.id { if let Some(id) = &share.id {
self.collection self.collection.replace_one(doc! { "_id": id }, share, None).await?;
.replace_one(doc! { "_id": id }, share, None)
.await?;
} }
Ok(()) Ok(())
} }
pub async fn delete(&self, share_id: &ObjectId) -> mongodb::error::Result<()> { pub async fn delete(&self, share_id: &ObjectId) -> mongodb::error::Result<()> {
self.collection self.collection.delete_one(doc! { "_id": share_id }, None).await?;
.delete_one(doc! { "_id": share_id }, None)
.await?;
Ok(()) Ok(())
} }
} }

View file

@ -2,8 +2,8 @@ use mongodb::bson::{doc, oid::ObjectId};
use mongodb::Collection; use mongodb::Collection;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::auth::password::verify_password;
use mongodb::bson::DateTime; use mongodb::bson::DateTime;
use crate::auth::password::verify_password;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User { pub struct User {
@ -156,14 +156,13 @@ impl UserRepository {
/// Find a user by ID /// Find a user by ID
pub async fn find_by_id(&self, id: &ObjectId) -> mongodb::error::Result<Option<User>> { pub async fn find_by_id(&self, id: &ObjectId) -> mongodb::error::Result<Option<User>> {
self.collection.find_one(doc! { "_id": id }, None).await self.collection
.find_one(doc! { "_id": id }, None)
.await
} }
/// Find a user by verification token /// Find a user by verification token
pub async fn find_by_verification_token( pub async fn find_by_verification_token(&self, token: &str) -> mongodb::error::Result<Option<User>> {
&self,
token: &str,
) -> mongodb::error::Result<Option<User>> {
self.collection self.collection
.find_one(doc! { "verification_token": token }, None) .find_one(doc! { "verification_token": token }, None)
.await .await
@ -178,11 +177,7 @@ impl UserRepository {
} }
/// Update the token version - silently fails if ObjectId is invalid /// Update the token version - silently fails if ObjectId is invalid
pub async fn update_token_version( pub async fn update_token_version(&self, user_id: &str, version: i32) -> mongodb::error::Result<()> {
&self,
user_id: &str,
version: i32,
) -> mongodb::error::Result<()> {
let oid = match ObjectId::parse_str(user_id) { let oid = match ObjectId::parse_str(user_id) {
Ok(id) => id, Ok(id) => id,
Err(_) => return Ok(()), // Silently fail if invalid ObjectId Err(_) => return Ok(()), // Silently fail if invalid ObjectId

View file

@ -1,8 +1,8 @@
use anyhow::Result;
use mongodb::bson::{doc, DateTime}; use mongodb::bson::{doc, DateTime};
use mongodb::Collection; use mongodb::Collection;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use anyhow::Result;
#[derive(Clone)] #[derive(Clone)]
pub struct AccountLockout { pub struct AccountLockout {
@ -29,7 +29,12 @@ impl AccountLockout {
pub async fn check_lockout(&self, email: &str) -> Result<bool> { pub async fn check_lockout(&self, email: &str) -> Result<bool> {
let collection = self.user_collection.read().await; 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(user_doc) = user {
if let Some(locked_until_val) = user_doc.get("locked_until") { if let Some(locked_until_val) = user_doc.get("locked_until") {
@ -49,11 +54,15 @@ impl AccountLockout {
let collection = self.user_collection.write().await; let collection = self.user_collection.write().await;
// Get current failed attempts // 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 { let current_attempts = if let Some(user_doc) = user {
user_doc user_doc.get("failed_login_attempts")
.get("failed_login_attempts")
.and_then(|v| v.as_i64()) .and_then(|v| v.as_i64())
.unwrap_or(0) as u32 .unwrap_or(0) as u32
} else { } else {

View file

@ -1,6 +1,6 @@
use crate::models::audit_log::{AuditEventType, AuditLog, AuditLogRepository};
use anyhow::Result; use anyhow::Result;
use mongodb::bson::oid::ObjectId; use mongodb::bson::oid::ObjectId;
use crate::models::audit_log::{AuditLog, AuditLogRepository, AuditEventType};
#[derive(Clone)] #[derive(Clone)]
pub struct AuditLogger { pub struct AuditLogger {
@ -24,14 +24,7 @@ impl AuditLogger {
resource_id: Option<String>, resource_id: Option<String>,
) -> Result<ObjectId> { ) -> Result<ObjectId> {
self.repository self.repository
.log( .log(event_type, user_id, email, ip_address, resource_type, resource_id)
event_type,
user_id,
email,
ip_address,
resource_type,
resource_id,
)
.await .await
} }

View file

@ -1,7 +1,7 @@
pub mod account_lockout;
pub mod audit_logger; pub mod audit_logger;
pub mod session_manager; pub mod session_manager;
pub mod account_lockout;
pub use account_lockout::AccountLockout;
pub use audit_logger::AuditLogger; pub use audit_logger::AuditLogger;
pub use session_manager::SessionManager; pub use session_manager::SessionManager;
pub use account_lockout::AccountLockout;

View file

@ -1,5 +1,5 @@
use crate::models::session::{DeviceInfo, Session, SessionRepository};
use anyhow::Result; use anyhow::Result;
use crate::models::session::{Session, SessionRepository, DeviceInfo};
use mongodb::bson::oid::ObjectId; use mongodb::bson::oid::ObjectId;
#[derive(Clone)] #[derive(Clone)]
@ -21,9 +21,7 @@ impl SessionManager {
token_hash: String, token_hash: String,
duration_hours: i64, duration_hours: i64,
) -> Result<ObjectId> { ) -> Result<ObjectId> {
self.repository self.repository.create(user_id, device_info, token_hash, duration_hours).await
.create(user_id, device_info, token_hash, duration_hours)
.await
} }
pub async fn get_user_sessions(&self, user_id: &ObjectId) -> Result<Vec<Session>> { pub async fn get_user_sessions(&self, user_id: &ObjectId) -> Result<Vec<Session>> {

View file

@ -40,15 +40,16 @@ impl IngredientMapper {
pub fn map_to_us(&self, eu_name: &str) -> String { pub fn map_to_us(&self, eu_name: &str) -> String {
let normalized = eu_name.to_lowercase().trim().to_string(); let normalized = eu_name.to_lowercase().trim().to_string();
self.mappings self.mappings.get(&normalized)
.get(&normalized)
.unwrap_or(&eu_name.to_string()) .unwrap_or(&eu_name.to_string())
.clone() .clone()
} }
/// Map multiple EU drug names to US names /// Map multiple EU drug names to US names
pub fn map_many_to_us(&self, eu_names: &[String]) -> Vec<String> { pub fn map_many_to_us(&self, eu_names: &[String]) -> Vec<String> {
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 /// Add a custom mapping

View file

@ -4,8 +4,9 @@
//! Provides a unified API for checking drug interactions //! Provides a unified API for checking drug interactions
use crate::services::{ use crate::services::{
openfda_service::{DrugInteraction, InteractionSeverity}, IngredientMapper,
IngredientMapper, OpenFDAService, OpenFDAService,
openfda_service::{DrugInteraction, InteractionSeverity}
}; };
use mongodb::bson::oid::ObjectId; use mongodb::bson::oid::ObjectId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -27,7 +28,7 @@ impl InteractionService {
/// Maps EU names to US names, then checks interactions /// Maps EU names to US names, then checks interactions
pub async fn check_eu_medications( pub async fn check_eu_medications(
&self, &self,
eu_medications: &[String], eu_medications: &[String]
) -> Result<Vec<DrugInteraction>, Box<dyn std::error::Error>> { ) -> Result<Vec<DrugInteraction>, Box<dyn std::error::Error>> {
// Step 1: Map EU names to US names // Step 1: Map EU names to US names
let us_medications: Vec<String> = self.mapper.map_many_to_us(eu_medications); let us_medications: Vec<String> = self.mapper.map_many_to_us(eu_medications);
@ -41,18 +42,10 @@ impl InteractionService {
.map(|mut interaction| { .map(|mut interaction| {
// Map US names back to EU names if they were mapped // Map US names back to EU names if they were mapped
for (i, eu_name) in eu_medications.iter().enumerate() { for (i, eu_name) in eu_medications.iter().enumerate() {
if us_medications if us_medications.get(i).map(|us| us == &interaction.drug1).unwrap_or(false) {
.get(i)
.map(|us| us == &interaction.drug1)
.unwrap_or(false)
{
interaction.drug1 = eu_name.clone(); interaction.drug1 = eu_name.clone();
} }
if us_medications if us_medications.get(i).map(|us| us == &interaction.drug2).unwrap_or(false) {
.get(i)
.map(|us| us == &interaction.drug2)
.unwrap_or(false)
{
interaction.drug2 = eu_name.clone(); interaction.drug2 = eu_name.clone();
} }
} }
@ -67,7 +60,7 @@ impl InteractionService {
pub async fn check_new_medication( pub async fn check_new_medication(
&self, &self,
new_medication: &str, new_medication: &str,
existing_medications: &[String], existing_medications: &[String]
) -> Result<DrugInteractionCheckResult, Box<dyn std::error::Error>> { ) -> Result<DrugInteractionCheckResult, Box<dyn std::error::Error>> {
let all_meds = { let all_meds = {
let mut meds = vec![new_medication.to_string()]; let mut meds = vec![new_medication.to_string()];
@ -77,8 +70,7 @@ impl InteractionService {
let interactions = self.check_eu_medications(&all_meds).await?; let interactions = self.check_eu_medications(&all_meds).await?;
let has_severe = interactions let has_severe = interactions.iter()
.iter()
.any(|i| matches!(i.severity, InteractionSeverity::Severe)); .any(|i| matches!(i.severity, InteractionSeverity::Severe));
Ok(DrugInteractionCheckResult { Ok(DrugInteractionCheckResult {

View file

@ -6,9 +6,9 @@
//! - Interaction Checker (combined service) //! - Interaction Checker (combined service)
pub mod ingredient_mapper; pub mod ingredient_mapper;
pub mod interaction_service;
pub mod openfda_service; pub mod openfda_service;
pub mod interaction_service;
pub use ingredient_mapper::IngredientMapper; pub use ingredient_mapper::IngredientMapper;
pub use interaction_service::InteractionService;
pub use openfda_service::OpenFDAService; pub use openfda_service::OpenFDAService;
pub use interaction_service::InteractionService;

View file

@ -2,7 +2,7 @@ use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum InteractionSeverity { pub enum InteractionSeverity {
Mild, Mild,
@ -32,17 +32,20 @@ impl OpenFDAService {
} }
} }
/// Check for interactions between multiple drugs /// Check interactions between multiple medications
pub async fn check_interactions( pub async fn check_interactions(
&self, &self,
drugs: &[String], medications: &[String],
) -> Result<Vec<DrugInteraction>, Box<dyn std::error::Error>> { ) -> Result<Vec<DrugInteraction>, Box<dyn std::error::Error>> {
let mut interactions = Vec::new(); let mut interactions = Vec::new();
// Check each pair of drugs // Check all pairs
for i in 0..drugs.len() { for i in 0..medications.len() {
for j in (i + 1)..drugs.len() { for j in (i + 1)..medications.len() {
if let Some(interaction) = self.check_pair(&drugs[i], &drugs[j]).await { if let Some(interaction) = self
.check_pair_interaction(&medications[i], &medications[j])
.await
{
interactions.push(interaction); interactions.push(interaction);
} }
} }
@ -51,48 +54,32 @@ impl OpenFDAService {
Ok(interactions) Ok(interactions)
} }
/// Check for interaction between a specific pair of drugs /// Check interaction between two specific drugs
async fn check_pair(&self, drug1: &str, drug2: &str) -> Option<DrugInteraction> { async fn check_pair_interaction(
&self,
drug1: &str,
drug2: &str,
) -> Option<DrugInteraction> {
// For MVP, use a hardcoded database of known interactions // For MVP, use a hardcoded database of known interactions
// In production, you would: // In production, you would:
// 1. Query OpenFDA drug event endpoint // 1. Query OpenFDA drug event endpoint
// 2. Use a professional interaction database // 2. Use a professional interaction database
// 3. Integrate with user-provided data // 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) // Known severe interactions (for demonstration)
let known_interactions = vec![ let known_interactions = [
( ("warfarin+aspirin", InteractionSeverity::Severe, "Increased risk of bleeding"),
"warfarin+aspirin", ("warfarin+ibuprofen", InteractionSeverity::Severe, "Increased risk of bleeding"),
InteractionSeverity::Severe, ("acetaminophen+alcohol", InteractionSeverity::Severe, "Increased risk of liver damage"),
"Increased risk of bleeding", ("ssri+maoi", InteractionSeverity::Severe, "Serotonin syndrome risk"),
), ("digoxin+verapamil", InteractionSeverity::Moderate, "Increased digoxin levels"),
( ("acei+arb", InteractionSeverity::Moderate, "Increased risk of hyperkalemia"),
"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 { for (known_pair, severity, desc) in known_interactions {
@ -116,7 +103,8 @@ impl OpenFDAService {
) -> Result<serde_json::Value, Box<dyn std::error::Error>> { ) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
let query = format!( let query = format!(
"{}?search=patient.drug.medicinalproduct:{}&limit=10", "{}?search=patient.drug.medicinalproduct:{}&limit=10",
self.base_url, drug_name self.base_url,
drug_name
); );
let response = self let response = self

View file

@ -6,8 +6,7 @@ const BASE_URL: &str = "http://127.0.0.1:8000";
#[tokio::test] #[tokio::test]
async fn test_health_check() { async fn test_health_check() {
let client = Client::new(); let client = Client::new();
let response = client let response = client.get(&format!("{}/health", BASE_URL))
.get(&format!("{}/health", BASE_URL))
.send() .send()
.await .await
.expect("Failed to send request"); .expect("Failed to send request");
@ -18,8 +17,7 @@ async fn test_health_check() {
#[tokio::test] #[tokio::test]
async fn test_ready_check() { async fn test_ready_check() {
let client = Client::new(); let client = Client::new();
let response = client let response = client.get(&format!("{}/ready", BASE_URL))
.get(&format!("{}/ready", BASE_URL))
.send() .send()
.await .await
.expect("Failed to send request"); .expect("Failed to send request");
@ -40,8 +38,7 @@ async fn test_register_user() {
"recovery_phrase_auth_tag": "auth_tag_placeholder" "recovery_phrase_auth_tag": "auth_tag_placeholder"
}); });
let response = client let response = client.post(&format!("{}/api/auth/register", BASE_URL))
.post(&format!("{}/api/auth/register", BASE_URL))
.json(&payload) .json(&payload)
.send() .send()
.await .await
@ -68,8 +65,7 @@ async fn test_login() {
"recovery_phrase_auth_tag": "auth_tag_placeholder" "recovery_phrase_auth_tag": "auth_tag_placeholder"
}); });
let _reg_response = client let _reg_response = client.post(&format!("{}/api/auth/register", BASE_URL))
.post(&format!("{}/api/auth/register", BASE_URL))
.json(&register_payload) .json(&register_payload)
.send() .send()
.await .await
@ -81,8 +77,7 @@ async fn test_login() {
"password_hash": "hashed_password_placeholder" "password_hash": "hashed_password_placeholder"
}); });
let response = client let response = client.post(&format!("{}/api/auth/login", BASE_URL))
.post(&format!("{}/api/auth/login", BASE_URL))
.json(&login_payload) .json(&login_payload)
.send() .send()
.await .await
@ -100,8 +95,7 @@ async fn test_login() {
async fn test_get_profile_without_auth() { async fn test_get_profile_without_auth() {
let client = Client::new(); let client = Client::new();
let response = client let response = client.get(&format!("{}/api/users/me", BASE_URL))
.get(&format!("{}/api/users/me", BASE_URL))
.send() .send()
.await .await
.expect("Failed to send request"); .expect("Failed to send request");
@ -124,8 +118,7 @@ async fn test_get_profile_with_auth() {
"recovery_phrase_auth_tag": "auth_tag_placeholder" "recovery_phrase_auth_tag": "auth_tag_placeholder"
}); });
client client.post(&format!("{}/api/auth/register", BASE_URL))
.post(&format!("{}/api/auth/register", BASE_URL))
.json(&register_payload) .json(&register_payload)
.send() .send()
.await .await
@ -136,21 +129,17 @@ async fn test_get_profile_with_auth() {
"password_hash": "hashed_password_placeholder" "password_hash": "hashed_password_placeholder"
}); });
let login_response = client let login_response = client.post(&format!("{}/api/auth/login", BASE_URL))
.post(&format!("{}/api/auth/login", BASE_URL))
.json(&login_payload) .json(&login_payload)
.send() .send()
.await .await
.expect("Failed to send request"); .expect("Failed to send request");
let login_json: Value = login_response.json().await.expect("Failed to parse JSON"); let login_json: Value = login_response.json().await.expect("Failed to parse JSON");
let access_token = login_json["access_token"] let access_token = login_json["access_token"].as_str().expect("No access token");
.as_str()
.expect("No access token");
// Get profile with auth token // Get profile with auth token
let response = client let response = client.get(&format!("{}/api/users/me", BASE_URL))
.get(&format!("{}/api/users/me", BASE_URL))
.header("Authorization", format!("Bearer {}", access_token)) .header("Authorization", format!("Bearer {}", access_token))
.send() .send()
.await .await

View file

@ -47,10 +47,7 @@ mod medication_tests {
async fn test_get_medication_requires_auth() { async fn test_get_medication_requires_auth() {
let client = Client::new(); let client = Client::new();
let response = client let response = client
.get(&format!( .get(&format!("{}/api/medications/507f1f77bcf86cd799439011", BASE_URL))
"{}/api/medications/507f1f77bcf86cd799439011",
BASE_URL
))
.send() .send()
.await .await
.expect("Failed to send request"); .expect("Failed to send request");