Phase 2.2: MongoDB connection and models

This commit is contained in:
goose 2026-02-14 15:37:02 -03:00
parent 1cf927f527
commit 154c3d1152
13 changed files with 544 additions and 5 deletions

108
backend/src/config/mod.rs Normal file
View file

@ -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<String>,
}
impl Config {
pub fn from_env() -> anyhow::Result<Self> {
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::<u16>()?;
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::<i64>()?;
let refresh_token_expiry = std::env::var("JWT_REFRESH_TOKEN_EXPIRY_DAYS")
.unwrap_or_else(|_| "30".to_string())
.parse::<i64>()?;
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,
},
})
}
}

44
backend/src/db/mod.rs Normal file
View file

@ -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<Self> {
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<T>(&self, name: &str) -> mongodb::Collection<T> {
self.database.collection(name)
}
pub async fn health_check(&self) -> Result<String> {
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())
}
}
}

View file

@ -2,13 +2,31 @@ use axum::{
routing::get, routing::get,
Router, Router,
response::Json, response::Json,
extract::State,
}; };
use serde_json::json; use serde_json::json;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 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<MongoDb>,
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() -> anyhow::Result<()> {
// Load configuration
let config = Config::from_env()?;
// Initialize tracing
tracing_subscriber::registry() tracing_subscriber::registry()
.with( .with(
tracing_subscriber::EnvFilter::try_from_default_env() tracing_subscriber::EnvFilter::try_from_default_env()
@ -18,22 +36,39 @@ async fn main() {
.init(); .init();
tracing::info!("Starting Normogen backend server"); 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() let app = Router::new()
.route("/health", get(health_check)) .route("/health", get(health_check))
.route("/ready", get(readiness_check)) .route("/ready", get(readiness_check))
.with_state(app_state)
.layer(TraceLayer::new_for_http()); .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); tracing::info!("Listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr) let listener = tokio::net::TcpListener::bind(&addr)
.await .await
.expect("Failed to bind address"); .expect("Failed to bind address");
axum::serve(listener, app) axum::serve(listener, app)
.await .await
.expect("Server error"); .expect("Server error");
Ok(())
} }
async fn health_check() -> Json<serde_json::Value> { async fn health_check() -> Json<serde_json::Value> {
@ -43,10 +78,12 @@ async fn health_check() -> Json<serde_json::Value> {
})) }))
} }
async fn readiness_check() -> Json<serde_json::Value> { async fn readiness_check(State(state): State<AppState>) -> Json<serde_json::Value> {
let db_status = state.db.health_check().await.unwrap_or_else(|_| "disconnected".to_string());
Json(json!({ Json(json!({
"status": "ready", "status": "ready",
"database": "not_connected", "database": db_status,
"timestamp": chrono::Utc::now().to_rfc3339(), "timestamp": chrono::Utc::now().to_rfc3339(),
})) }))
} }

View file

@ -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<ObjectId>,
#[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<AppointmentReminder>,
#[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,
}

View file

@ -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<ObjectId>,
#[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<String>,
#[serde(rename = "createdAt")]
pub created_at: DateTime,
#[serde(rename = "updatedAt")]
pub updated_at: DateTime,
}
pub struct FamilyRepository {
collection: Collection<Family>,
}
impl FamilyRepository {
pub fn new(collection: Collection<Family>) -> 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<Option<Family>> {
self.collection
.find_one(doc! { "familyId": family_id }, None)
.await
}
}

View file

@ -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<ObjectId>,
#[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<String>,
#[serde(rename = "healthData")]
pub health_data: Vec<EncryptedField>,
#[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,
}

View file

@ -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<ObjectId>,
#[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,
}

View file

@ -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<ObjectId>,
#[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<MedicationReminder>,
#[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,
}

View file

@ -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;

View file

@ -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<ObjectId>,
#[serde(rename = "profileId")]
pub profile_id: String,
#[serde(rename = "userId")]
pub user_id: String,
#[serde(rename = "familyId")]
pub family_id: Option<String>,
#[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<String>,
#[serde(rename = "createdAt")]
pub created_at: DateTime,
#[serde(rename = "updatedAt")]
pub updated_at: DateTime,
}
pub struct ProfileRepository {
collection: Collection<Profile>,
}
impl ProfileRepository {
pub fn new(collection: Collection<Profile>) -> 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<Option<Profile>> {
self.collection
.find_one(doc! { "profileId": profile_id }, None)
.await
}
}

View file

@ -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<ObjectId>,
#[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<DateTime>,
}

View file

@ -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<ObjectId>,
#[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,
}

View file

@ -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<ObjectId>,
#[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<String>,
#[serde(rename = "profileIds")]
pub profile_ids: Vec<String>,
#[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<User>,
}
impl UserRepository {
pub fn new(collection: Collection<User>) -> 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<Option<User>> {
self.collection
.find_one(doc! { "email": email }, None)
.await
}
pub async fn find_by_user_id(&self, user_id: &str) -> mongodb::error::Result<Option<User>> {
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(())
}
}