feat: initialize rust backend with axum server, seaorm models, and project scaffolding

This commit is contained in:
2025-12-18 18:46:56 +05:30
parent 9c0715637f
commit e2930d1cd5
17 changed files with 576 additions and 3 deletions

43
backend/Cargo.toml Normal file
View File

@@ -0,0 +1,43 @@
[package]
name = "zhealth-backend"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "zhealth"
path = "src/main.rs"
[dependencies]
# Web Framework
axum = "0.8"
tokio = { version = "1", features = ["full"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace"] }
# Database
sea-orm = { version = "1.1", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros"] }
# Serialization
serde = { version = "1", features = ["derive"] }
serde_yaml = "0.9"
# Auth
argon2 = "0.5"
rand = "0.8"
# Time
chrono = { version = "0.4", features = ["serde"] }
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Error handling
thiserror = "2"
anyhow = "1"
# Regex for config env expansion
regex = "1"
# CLI
argh = "0.1"

View File

@@ -0,0 +1,25 @@
# Sample Configuration for zhealth-backend
# Copy this to config.yaml and update values as needed.
# Secrets should be loaded from environment variables using ${VAR_NAME} syntax.
server:
host: "127.0.0.1"
port: 3000
paths:
database: "./data/zhealth.db"
logs: "./logs"
logging:
level: "info" # Options: trace | debug | info | warn | error
auth:
session_secret: "${SESSION_SECRET}"
session_expiry_hours: 24
cookie_name: "zhealth_session"
cookie_secure: false # Set to true in production with HTTPS
ai:
provider: "gemini" # Options: gemini | openai | anthropic
model: "gemini-3-flash-preview"
api_key: "${AI_API_KEY}"

33
backend/src/cli.rs Normal file
View File

@@ -0,0 +1,33 @@
//! CLI definition using argh.
use argh::FromArgs;
/// zhealth: AI-powered health management platform
#[derive(FromArgs)]
pub struct Args {
#[argh(subcommand)]
pub command: Command,
}
#[derive(FromArgs)]
#[argh(subcommand)]
pub enum Command {
Serve(ServeCommand),
Migrate(MigrateCommand),
Version(VersionCommand),
}
/// Start the server (runs migrations first)
#[derive(FromArgs)]
#[argh(subcommand, name = "serve")]
pub struct ServeCommand {}
/// Run database migrations only
#[derive(FromArgs)]
#[argh(subcommand, name = "migrate")]
pub struct MigrateCommand {}
/// Show version information
#[derive(FromArgs)]
#[argh(subcommand, name = "version")]
pub struct VersionCommand {}

71
backend/src/config.rs Normal file
View File

@@ -0,0 +1,71 @@
//! Configuration loading and parsing.
use serde::Deserialize;
use std::fs;
use std::path::Path;
#[derive(Debug, Deserialize)]
pub struct Config {
pub server: ServerConfig,
pub paths: PathsConfig,
pub logging: LoggingConfig,
pub auth: AuthConfig,
pub ai: AiConfig,
}
#[derive(Debug, Deserialize)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
}
#[derive(Debug, Deserialize)]
pub struct PathsConfig {
pub database: String,
pub logs: String,
}
#[derive(Debug, Deserialize)]
pub struct LoggingConfig {
pub level: String,
}
#[derive(Debug, Deserialize)]
pub struct AuthConfig {
pub session_secret: String,
pub session_expiry_hours: u32,
pub cookie_name: String,
pub cookie_secure: bool,
}
#[derive(Debug, Deserialize)]
pub struct AiConfig {
pub provider: String,
pub model: String,
pub api_key: String,
}
impl Config {
/// Load configuration from a YAML file.
pub fn load<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
let content = fs::read_to_string(path)?;
// Expand environment variables in the format ${VAR_NAME}
let expanded = expand_env_vars(&content);
let config: Config = serde_yaml::from_str(&expanded)?;
Ok(config)
}
}
/// Expand environment variables in the format ${VAR_NAME}.
fn expand_env_vars(content: &str) -> String {
let mut result = content.to_string();
let re = regex::Regex::new(r"\$\{([^}]+)\}").unwrap();
for cap in re.captures_iter(content) {
let var_name = &cap[1];
let var_value = std::env::var(var_name).unwrap_or_default();
result = result.replace(&cap[0], &var_value);
}
result
}

42
backend/src/db.rs Normal file
View File

@@ -0,0 +1,42 @@
//! Database connection and migrations.
use sea_orm::{ConnectionTrait, Database, DatabaseConnection, DbBackend, DbErr, Schema, Statement};
use sea_orm::sea_query::SqliteQueryBuilder;
use crate::config::Config;
use crate::models::bio::{biomarker_entry, biomarker_type};
use crate::models::user::{session, user};
/// Connect to the SQLite database.
pub async fn connect(config: &Config) -> Result<DatabaseConnection, DbErr> {
let db_path = &config.paths.database;
// Ensure the data directory exists
if let Some(parent) = std::path::Path::new(db_path).parent() {
std::fs::create_dir_all(parent).ok();
}
let db_url = format!("sqlite:{}?mode=rwc", db_path);
Database::connect(&db_url).await
}
/// Run migrations to create tables if they don't exist.
pub async fn run_migrations(db: &DatabaseConnection) -> Result<(), DbErr> {
let schema = Schema::new(DbBackend::Sqlite);
// Create table statements
let statements = vec![
schema.create_table_from_entity(user::Entity),
schema.create_table_from_entity(session::Entity),
schema.create_table_from_entity(biomarker_type::Entity),
schema.create_table_from_entity(biomarker_entry::Entity),
];
for mut stmt in statements {
let sql = stmt.if_not_exists().to_string(SqliteQueryBuilder);
db.execute(Statement::from_string(DbBackend::Sqlite, sql))
.await?;
}
Ok(())
}

75
backend/src/main.rs Normal file
View File

@@ -0,0 +1,75 @@
mod cli;
mod config;
mod db;
mod models;
use axum::{routing::get, Router};
use std::net::SocketAddr;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use cli::{Args, Command};
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args: Args = argh::from_env();
match args.command {
Command::Version(_) => {
println!("zhealth {}", VERSION);
}
Command::Migrate(_) => {
let config = config::Config::load("config.yaml")?;
init_logging(&config);
tracing::info!("Running migrations...");
let db = db::connect(&config).await?;
db::run_migrations(&db).await?;
tracing::info!("Migrations complete.");
}
Command::Serve(_) => {
let config = config::Config::load("config.yaml")?;
init_logging(&config);
// Run migrations first
tracing::info!("Running migrations...");
let db = db::connect(&config).await?;
db::run_migrations(&db).await?;
tracing::info!("Migrations complete.");
// Start server
tracing::info!("Starting zhealth-backend...");
let app = Router::new()
.route("/", get(root))
.route("/health", get(health_check));
let addr: SocketAddr = format!("{}:{}", config.server.host, config.server.port)
.parse()
.expect("Invalid server address");
tracing::info!("Listening on http://{}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
}
}
Ok(())
}
fn init_logging(config: &config::Config) {
let log_level = config.logging.level.parse().unwrap_or(tracing::Level::INFO);
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(tracing_subscriber::filter::LevelFilter::from_level(log_level))
.init();
}
async fn root() -> &'static str {
"zhealth API"
}
async fn health_check() -> &'static str {
"OK"
}

View File

@@ -0,0 +1,56 @@
//! BiomarkerEntry entity - user-logged biomarker values.
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "biomarker_entries")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub user_id: i32,
pub biomarker_type_id: i32,
/// The measured value
pub value: f64,
/// Date when the measurement was taken
pub measured_at: Date,
#[sea_orm(column_type = "Text", nullable)]
pub notes: Option<String>,
pub created_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "crate::models::user::user::Entity",
from = "Column::UserId",
to = "crate::models::user::user::Column::Id"
)]
User,
#[sea_orm(
belongs_to = "super::biomarker_type::Entity",
from = "Column::BiomarkerTypeId",
to = "super::biomarker_type::Column::Id"
)]
BiomarkerType,
}
impl Related<crate::models::user::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl Related<super::biomarker_type::Entity> for Entity {
fn to() -> RelationDef {
Relation::BiomarkerType.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,43 @@
//! BiomarkerType entity - the knowledge base of available biomarkers.
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "biomarker_types")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub name: String,
/// Category: lipid_panel, metabolic, vitamins, hormones, etc.
pub category: String,
/// Unit of measurement: mg/dL, mmol/L, ng/mL, etc.
pub unit: String,
/// Lower bound of normal reference range
pub reference_min: Option<f64>,
/// Upper bound of normal reference range
pub reference_max: Option<f64>,
#[sea_orm(column_type = "Text", nullable)]
pub description: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::biomarker_entry::Entity")]
Entries,
}
impl Related<super::biomarker_entry::Entity> for Entity {
fn to() -> RelationDef {
Relation::Entries.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,7 @@
//! Biomarker entities for health data tracking.
pub mod biomarker_entry;
pub mod biomarker_type;
pub use biomarker_entry::Entity as BiomarkerEntry;
pub use biomarker_type::Entity as BiomarkerType;

View File

@@ -0,0 +1,7 @@
//! Database models for zhealth.
pub mod bio;
pub mod user;
pub use bio::{BiomarkerEntry, BiomarkerType};
pub use user::{Session, User};

View File

@@ -0,0 +1,7 @@
//! User and session entities for authentication.
pub mod session;
pub mod user;
pub use session::Entity as Session;
pub use user::Entity as User;

View File

@@ -0,0 +1,33 @@
//! Session entity for cookie-based authentication.
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "sessions")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub user_id: i32,
pub expires_at: DateTime,
pub created_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,37 @@
//! User entity for authentication.
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "users")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub username: String,
#[sea_orm(column_type = "Text")]
pub password_hash: String,
#[sea_orm(unique)]
pub email: String,
pub created_at: DateTime,
pub updated_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::session::Entity")]
Sessions,
}
impl Related<super::session::Entity> for Entity {
fn to() -> RelationDef {
Relation::Sessions.def()
}
}
impl ActiveModelBehavior for ActiveModel {}