From 99cfc49cb2b6c909afa1a68a37bf7b2720f85a4d Mon Sep 17 00:00:00 2001 From: jwinterm Date: Mon, 22 Jun 2026 14:36:51 -0400 Subject: [PATCH] feat: AppState + server wiring (config, pool, migrations, session manager) - AppState (lib.rs): Config + Database + SessionManager + in-memory web-challenge store; injected into handlers via State> - main.rs: fail-closed Config::from_env, pg pool, run embedded migrations on start, build state, serve /health (auth/wallet routes added as handlers land) - Cargo.toml: sqlx 'migrate' feature for embedded migrations --- Cargo.toml | 2 +- src/lib.rs | 20 +++++++++++++++++++ src/main.rs | 55 +++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e5a8546..ef1f976 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ tower_governor = "0.4" governor = "0.6" # Database -sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid", "ipnetwork"] } +sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid", "ipnetwork", "migrate"] } ipnetwork = "0.20" # Serialization diff --git a/src/lib.rs b/src/lib.rs index c09bfc8..f2c634e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,3 +14,23 @@ pub mod core; pub mod error; pub mod infra; pub mod models; + +use std::collections::HashMap; +use std::sync::Arc; + +use tokio::sync::RwLock; + +use crate::config::Config; +use crate::core::session::{SessionManager, WebChallenge}; +use crate::infra::db::Database; + +/// Shared application state injected into handlers via `State>`. +#[derive(Clone)] +pub struct AppState { + pub config: Config, + pub db: Database, + pub sessions: SessionManager, + /// In-memory website-auth challenges, keyed by nonce. Single-node store; + /// a shared/stateless variant is the load-balanced-fleet path. + pub web_challenges: Arc>>, +} diff --git a/src/main.rs b/src/main.rs index f5fdc44..40d197e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,54 @@ //! smirk-backend-core server entry point. //! -//! Minimal server skeleton. Configuration, application state, the full router, -//! and background tasks are added as modules land; for now it serves the -//! health endpoint so the build, the OpenAPI contract, and deployment wiring -//! are exercised end to end. +//! Loads and validates configuration (fail-closed), connects the database and +//! runs migrations, builds the shared [`AppState`], and serves the HTTP API. +//! Routes are added as handler modules land; for now it serves health. + +use std::sync::Arc; use axum::{routing::get, Router}; -use smirk_backend_core::api::health; +use sqlx::postgres::PgPoolOptions; + +use smirk_backend_core::{ + api::health, config::Config, core::session::SessionManager, infra::db::Database, AppState, +}; #[tokio::main] -async fn main() { +async fn main() -> Result<(), Box> { tracing_subscriber::fmt::init(); + let _ = dotenvy::dotenv(); - let app = Router::new().route("/health", get(health::health)); + // Fail-closed: aborts on a weak/missing/inconsistent secret. + let config = Config::from_env()?; + tracing::info!(environment = %config.environment, "configuration loaded"); - let addr = "0.0.0.0:8080"; - let listener = tokio::net::TcpListener::bind(addr) - .await - .expect("bind listener"); + let pool = PgPoolOptions::new() + .max_connections(20) + .connect(&config.database_url) + .await?; + sqlx::migrate!("./migrations").run(&pool).await?; + + let db = Database::new( + pool, + config.secrets.seed_fingerprint_pepper.clone(), + config.secrets.ip_salt.clone(), + ); + let sessions = SessionManager::new(&config.auth.jwt_secret, config.auth.jwt_expiry_hours); + + let addr = format!("{}:{}", config.server_host, config.server_port); + let state = Arc::new(AppState { + config, + db, + sessions, + web_challenges: Arc::default(), + }); + + let app = Router::new() + .route("/health", get(health::health)) + .with_state(state); + + let listener = tokio::net::TcpListener::bind(&addr).await?; tracing::info!("smirk-backend-core listening on http://{addr}"); - axum::serve(listener, app).await.expect("server error"); + axum::serve(listener, app).await?; + Ok(()) }