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