From 154c3d1152324cde912fbdb8afafefce6d258c1e Mon Sep 17 00:00:00 2001 From: goose Date: Sat, 14 Feb 2026 15:37:02 -0300 Subject: [PATCH] Phase 2.2: MongoDB connection and models --- backend/src/config/mod.rs | 108 ++++++++++++++++++++++++++++ backend/src/db/mod.rs | 44 ++++++++++++ backend/src/main.rs | 47 ++++++++++-- backend/src/models/appointment.rs | 31 ++++++++ backend/src/models/family.rs | 43 +++++++++++ backend/src/models/health_data.rs | 32 +++++++++ backend/src/models/lab_result.rs | 21 ++++++ backend/src/models/medication.rs | 31 ++++++++ backend/src/models/mod.rs | 9 +++ backend/src/models/profile.rs | 49 +++++++++++++ backend/src/models/refresh_token.rs | 22 ++++++ backend/src/models/share.rs | 24 +++++++ backend/src/models/user.rs | 88 +++++++++++++++++++++++ 13 files changed, 544 insertions(+), 5 deletions(-) create mode 100644 backend/src/config/mod.rs create mode 100644 backend/src/db/mod.rs create mode 100644 backend/src/models/appointment.rs create mode 100644 backend/src/models/family.rs create mode 100644 backend/src/models/health_data.rs create mode 100644 backend/src/models/lab_result.rs create mode 100644 backend/src/models/medication.rs create mode 100644 backend/src/models/mod.rs create mode 100644 backend/src/models/profile.rs create mode 100644 backend/src/models/refresh_token.rs create mode 100644 backend/src/models/share.rs create mode 100644 backend/src/models/user.rs diff --git a/backend/src/config/mod.rs b/backend/src/config/mod.rs new file mode 100644 index 0000000..e2d5557 --- /dev/null +++ b/backend/src/config/mod.rs @@ -0,0 +1,108 @@ +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub server: ServerConfig, + pub database: DatabaseConfig, + pub jwt: JwtConfig, + pub encryption: EncryptionConfig, + pub cors: CorsConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServerConfig { + pub host: String, + pub port: u16, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DatabaseConfig { + pub uri: String, + pub database: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JwtConfig { + pub secret: String, + pub access_token_expiry_minutes: i64, + pub refresh_token_expiry_days: i64, +} + +impl JwtConfig { + pub fn access_token_expiry_duration(&self) -> Duration { + Duration::from_secs(self.access_token_expiry_minutes as u64 * 60) + } + + pub fn refresh_token_expiry_duration(&self) -> Duration { + Duration::from_secs(self.refresh_token_expiry_days as u64 * 86400) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EncryptionConfig { + pub algorithm: String, + pub key_length: usize, + pub pbkdf2_iterations: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CorsConfig { + pub allowed_origins: Vec, +} + +impl Config { + pub fn from_env() -> anyhow::Result { + dotenv::dotenv().ok(); + + let server_host = std::env::var("SERVER_HOST") + .unwrap_or_else(|_| "0.0.0.0".to_string()); + let server_port = std::env::var("SERVER_PORT") + .unwrap_or_else(|_| "8000".to_string()) + .parse::()?; + + let mongodb_uri = std::env::var("MONGODB_URI") + .unwrap_or_else(|_| "mongodb://localhost:27017".to_string()); + let mongodb_database = std::env::var("MONGODB_DATABASE") + .unwrap_or_else(|_| "normogen".to_string()); + + let jwt_secret = std::env::var("JWT_SECRET") + .expect("JWT_SECRET must be set"); + let access_token_expiry = std::env::var("JWT_ACCESS_TOKEN_EXPIRY_MINUTES") + .unwrap_or_else(|_| "15".to_string()) + .parse::()?; + let refresh_token_expiry = std::env::var("JWT_REFRESH_TOKEN_EXPIRY_DAYS") + .unwrap_or_else(|_| "30".to_string()) + .parse::()?; + + let cors_origins = std::env::var("CORS_ALLOWED_ORIGINS") + .unwrap_or_else(|_| "http://localhost:3000,http://localhost:6001".to_string()) + .split(',') + .map(|s| s.trim().to_string()) + .collect(); + + Ok(Config { + server: ServerConfig { + host: server_host, + port: server_port, + }, + database: DatabaseConfig { + uri: mongodb_uri, + database: mongodb_database, + }, + jwt: JwtConfig { + secret: jwt_secret, + access_token_expiry_minutes: access_token_expiry, + refresh_token_expiry_days: refresh_token_expiry, + }, + encryption: EncryptionConfig { + algorithm: "aes-256-gcm".to_string(), + key_length: 32, + pbkdf2_iterations: 100000, + }, + cors: CorsConfig { + allowed_origins: cors_origins, + }, + }) + } +} diff --git a/backend/src/db/mod.rs b/backend/src/db/mod.rs new file mode 100644 index 0000000..3d8ce95 --- /dev/null +++ b/backend/src/db/mod.rs @@ -0,0 +1,44 @@ +use mongodb::{Client, Database, options::{ClientOptions, ServerApi, ServerApiVersion}}; +use anyhow::Result; + +pub struct MongoDb { + pub client: Client, + pub database: Database, +} + +impl MongoDb { + pub async fn new(uri: &str, database_name: &str) -> Result { + let mut client_options = ClientOptions::parse(uri).await?; + + let server_api = ServerApi::builder() + .version(ServerApiVersion::V1) + .build(); + client_options.server_api = Some(server_api); + + let client = Client::with_options(client_options)?; + let database = client.database(database_name); + + Ok(MongoDb { client, database }) + } + + pub fn get_database(&self) -> Database { + self.database.clone() + } + + pub fn collection(&self, name: &str) -> mongodb::Collection { + self.database.collection(name) + } + + pub async fn health_check(&self) -> Result { + let result = self.client + .database("admin") + .run_command(mongodb::bson::doc! { "ping": 1 }, None) + .await?; + + if result.get_i32("ok").unwrap_or(0) == 1 { + Ok("connected".to_string()) + } else { + Ok("error".to_string()) + } + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs index aa897e8..256eb05 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -2,13 +2,31 @@ use axum::{ routing::get, Router, response::Json, + extract::State, }; use serde_json::json; use tower_http::trace::TraceLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use std::sync::Arc; + +mod config; +mod db; +mod models; + +use config::Config; +use db::MongoDb; + +#[derive(Clone)] +struct AppState { + db: Arc, +} #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { + // Load configuration + let config = Config::from_env()?; + + // Initialize tracing tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() @@ -18,22 +36,39 @@ async fn main() { .init(); tracing::info!("Starting Normogen backend server"); + tracing::info!("MongoDB URI: {}", config.database.uri); + tracing::info!("Database: {}", config.database.database); + + // Connect to MongoDB + let mongodb = MongoDb::new(&config.database.uri, &config.database.database).await?; + tracing::info!("Connected to MongoDB"); + + // Health check + let health_status = mongodb.health_check().await?; + tracing::info!("MongoDB health: {}", health_status); + + let app_state = AppState { + db: Arc::new(mongodb), + }; let app = Router::new() .route("/health", get(health_check)) .route("/ready", get(readiness_check)) + .with_state(app_state) .layer(TraceLayer::new_for_http()); - let addr = std::net::SocketAddr::from(([0, 0, 0, 0], 8000)); + let addr = format!("{}:{}", config.server.host, config.server.port); tracing::info!("Listening on {}", addr); - let listener = tokio::net::TcpListener::bind(addr) + let listener = tokio::net::TcpListener::bind(&addr) .await .expect("Failed to bind address"); axum::serve(listener, app) .await .expect("Server error"); + + Ok(()) } async fn health_check() -> Json { @@ -43,10 +78,12 @@ async fn health_check() -> Json { })) } -async fn readiness_check() -> Json { +async fn readiness_check(State(state): State) -> Json { + let db_status = state.db.health_check().await.unwrap_or_else(|_| "disconnected".to_string()); + Json(json!({ "status": "ready", - "database": "not_connected", + "database": db_status, "timestamp": chrono::Utc::now().to_rfc3339(), })) } diff --git a/backend/src/models/appointment.rs b/backend/src/models/appointment.rs new file mode 100644 index 0000000..0e0b144 --- /dev/null +++ b/backend/src/models/appointment.rs @@ -0,0 +1,31 @@ +use serde::{Deserialize, Serialize}; +use mongodb::bson::{oid::ObjectId, DateTime}; +use super::health_data::EncryptedField; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Appointment { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "appointmentId")] + pub appointment_id: String, + #[serde(rename = "userId")] + pub user_id: String, + #[serde(rename = "profileId")] + pub profile_id: String, + #[serde(rename = "appointmentData")] + pub appointment_data: EncryptedField, + #[serde(rename = "reminders")] + pub reminders: Vec, + #[serde(rename = "createdAt")] + pub created_at: DateTime, + #[serde(rename = "updatedAt")] + pub updated_at: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AppointmentReminder { + #[serde(rename = "reminderId")] + pub reminder_id: String, + #[serde(rename = "scheduledTime")] + pub scheduled_time: String, +} diff --git a/backend/src/models/family.rs b/backend/src/models/family.rs new file mode 100644 index 0000000..cd2d4f1 --- /dev/null +++ b/backend/src/models/family.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; +use mongodb::{bson::{doc, oid::ObjectId, DateTime}, Collection}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Family { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "familyId")] + pub family_id: String, + #[serde(rename = "name")] + pub name: String, + #[serde(rename = "nameIv")] + pub name_iv: String, + #[serde(rename = "nameAuthTag")] + pub name_auth_tag: String, + #[serde(rename = "memberIds")] + pub member_ids: Vec, + #[serde(rename = "createdAt")] + pub created_at: DateTime, + #[serde(rename = "updatedAt")] + pub updated_at: DateTime, +} + +pub struct FamilyRepository { + collection: Collection, +} + +impl FamilyRepository { + pub fn new(collection: Collection) -> Self { + Self { collection } + } + + pub async fn create(&self, family: &Family) -> mongodb::error::Result<()> { + self.collection.insert_one(family, None).await?; + Ok(()) + } + + pub async fn find_by_family_id(&self, family_id: &str) -> mongodb::error::Result> { + self.collection + .find_one(doc! { "familyId": family_id }, None) + .await + } +} diff --git a/backend/src/models/health_data.rs b/backend/src/models/health_data.rs new file mode 100644 index 0000000..0242da2 --- /dev/null +++ b/backend/src/models/health_data.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; +use mongodb::bson::{oid::ObjectId, DateTime}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HealthData { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "healthDataId")] + pub health_data_id: String, + #[serde(rename = "userId")] + pub user_id: String, + #[serde(rename = "profileId")] + pub profile_id: String, + #[serde(rename = "familyId")] + pub family_id: Option, + #[serde(rename = "healthData")] + pub health_data: Vec, + #[serde(rename = "createdAt")] + pub created_at: DateTime, + #[serde(rename = "updatedAt")] + pub updated_at: DateTime, + #[serde(rename = "dataSource")] + pub data_source: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EncryptedField { + pub encrypted: bool, + pub data: String, + pub iv: String, + pub auth_tag: String, +} diff --git a/backend/src/models/lab_result.rs b/backend/src/models/lab_result.rs new file mode 100644 index 0000000..5e770b4 --- /dev/null +++ b/backend/src/models/lab_result.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; +use mongodb::bson::{oid::ObjectId, DateTime}; +use super::health_data::EncryptedField; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LabResult { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "labResultId")] + pub lab_result_id: String, + #[serde(rename = "userId")] + pub user_id: String, + #[serde(rename = "profileId")] + pub profile_id: String, + #[serde(rename = "labData")] + pub lab_data: EncryptedField, + #[serde(rename = "createdAt")] + pub created_at: DateTime, + #[serde(rename = "updatedAt")] + pub updated_at: DateTime, +} diff --git a/backend/src/models/medication.rs b/backend/src/models/medication.rs new file mode 100644 index 0000000..93b6264 --- /dev/null +++ b/backend/src/models/medication.rs @@ -0,0 +1,31 @@ +use serde::{Deserialize, Serialize}; +use mongodb::bson::{oid::ObjectId, DateTime}; +use super::health_data::EncryptedField; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Medication { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "medicationId")] + pub medication_id: String, + #[serde(rename = "userId")] + pub user_id: String, + #[serde(rename = "profileId")] + pub profile_id: String, + #[serde(rename = "medicationData")] + pub medication_data: EncryptedField, + #[serde(rename = "reminders")] + pub reminders: Vec, + #[serde(rename = "createdAt")] + pub created_at: DateTime, + #[serde(rename = "updatedAt")] + pub updated_at: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MedicationReminder { + #[serde(rename = "reminderId")] + pub reminder_id: String, + #[serde(rename = "scheduledTime")] + pub scheduled_time: String, +} diff --git a/backend/src/models/mod.rs b/backend/src/models/mod.rs new file mode 100644 index 0000000..5f093c9 --- /dev/null +++ b/backend/src/models/mod.rs @@ -0,0 +1,9 @@ +pub mod user; +pub mod family; +pub mod profile; +pub mod health_data; +pub mod lab_result; +pub mod medication; +pub mod appointment; +pub mod share; +pub mod refresh_token; diff --git a/backend/src/models/profile.rs b/backend/src/models/profile.rs new file mode 100644 index 0000000..1def644 --- /dev/null +++ b/backend/src/models/profile.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; +use mongodb::{bson::{doc, oid::ObjectId, DateTime}, Collection}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Profile { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "profileId")] + pub profile_id: String, + #[serde(rename = "userId")] + pub user_id: String, + #[serde(rename = "familyId")] + pub family_id: Option, + #[serde(rename = "name")] + pub name: String, + #[serde(rename = "nameIv")] + pub name_iv: String, + #[serde(rename = "nameAuthTag")] + pub name_auth_tag: String, + #[serde(rename = "role")] + pub role: String, + #[serde(rename = "permissions")] + pub permissions: Vec, + #[serde(rename = "createdAt")] + pub created_at: DateTime, + #[serde(rename = "updatedAt")] + pub updated_at: DateTime, +} + +pub struct ProfileRepository { + collection: Collection, +} + +impl ProfileRepository { + pub fn new(collection: Collection) -> Self { + Self { collection } + } + + pub async fn create(&self, profile: &Profile) -> mongodb::error::Result<()> { + self.collection.insert_one(profile, None).await?; + Ok(()) + } + + pub async fn find_by_profile_id(&self, profile_id: &str) -> mongodb::error::Result> { + self.collection + .find_one(doc! { "profileId": profile_id }, None) + .await + } +} diff --git a/backend/src/models/refresh_token.rs b/backend/src/models/refresh_token.rs new file mode 100644 index 0000000..e50dfaf --- /dev/null +++ b/backend/src/models/refresh_token.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; +use mongodb::bson::{oid::ObjectId, DateTime}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RefreshToken { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "tokenId")] + pub token_id: String, + #[serde(rename = "userId")] + pub user_id: String, + #[serde(rename = "tokenHash")] + pub token_hash: String, + #[serde(rename = "expiresAt")] + pub expires_at: DateTime, + #[serde(rename = "createdAt")] + pub created_at: DateTime, + #[serde(rename = "revoked")] + pub revoked: bool, + #[serde(rename = "revokedAt")] + pub revoked_at: Option, +} diff --git a/backend/src/models/share.rs b/backend/src/models/share.rs new file mode 100644 index 0000000..f038c24 --- /dev/null +++ b/backend/src/models/share.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; +use mongodb::bson::{oid::ObjectId, DateTime}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Share { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "shareId")] + pub share_id: String, + #[serde(rename = "userId")] + pub user_id: String, + #[serde(rename = "encryptedDataKey")] + pub encrypted_data_key: String, + #[serde(rename = "dataKeyIv")] + pub data_key_iv: String, + #[serde(rename = "dataKeyAuthTag")] + pub data_key_auth_tag: String, + #[serde(rename = "expiresAt")] + pub expires_at: DateTime, + #[serde(rename = "createdAt")] + pub created_at: DateTime, + #[serde(rename = "accessCount")] + pub access_count: i32, +} diff --git a/backend/src/models/user.rs b/backend/src/models/user.rs new file mode 100644 index 0000000..27746fd --- /dev/null +++ b/backend/src/models/user.rs @@ -0,0 +1,88 @@ +use serde::{Deserialize, Serialize}; +use mongodb::{bson::{doc, oid::ObjectId, DateTime}, Collection}; +use chrono::{Utc, TimeZone}; +use validator::Validate; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct User { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "userId")] + pub user_id: String, + #[serde(rename = "email")] + pub email: String, + #[serde(rename = "passwordHash")] + pub password_hash: String, + #[serde(rename = "encryptedRecoveryPhrase")] + pub encrypted_recovery_phrase: String, + #[serde(rename = "recoveryPhraseIv")] + pub recovery_phrase_iv: String, + #[serde(rename = "recoveryPhraseAuthTag")] + pub recovery_phrase_auth_tag: String, + #[serde(rename = "tokenVersion")] + pub token_version: i32, + #[serde(rename = "familyId")] + pub family_id: Option, + #[serde(rename = "profileIds")] + pub profile_ids: Vec, + #[serde(rename = "createdAt")] + pub created_at: DateTime, + #[serde(rename = "updatedAt")] + pub updated_at: DateTime, +} + +#[derive(Debug, Deserialize, Validate)] +pub struct RegisterUserRequest { + #[validate(email)] + pub email: String, + pub password_hash: String, + pub encrypted_recovery_phrase: String, + pub recovery_phrase_iv: String, + pub recovery_phrase_auth_tag: String, +} + +#[derive(Debug, Deserialize, Validate)] +pub struct LoginRequest { + #[validate(email)] + pub email: String, + pub password_hash: String, +} + +pub struct UserRepository { + collection: Collection, +} + +impl UserRepository { + pub fn new(collection: Collection) -> Self { + Self { collection } + } + + pub async fn create(&self, user: &User) -> mongodb::error::Result<()> { + self.collection.insert_one(user, None).await?; + Ok(()) + } + + pub async fn find_by_email(&self, email: &str) -> mongodb::error::Result> { + self.collection + .find_one(doc! { "email": email }, None) + .await + } + + pub async fn find_by_user_id(&self, user_id: &str) -> mongodb::error::Result> { + self.collection + .find_one(doc! { "userId": user_id }, None) + .await + } + + pub async fn update_token_version(&self, user_id: &str, version: i32) -> mongodb::error::Result<()> { + let now = DateTime::now(); + self.collection + .update_one( + doc! { "userId": user_id }, + doc! { "$set": { "tokenVersion": version, "updatedAt": now } }, + None, + ) + .await?; + Ok(()) + } +}