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<Arc<AppState>>
- 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
This commit is contained in:
jwinterm
2026-06-22 14:36:51 -04:00
parent 3caa678dea
commit 99cfc49cb2
3 changed files with 64 additions and 13 deletions
+1 -1
View File
@@ -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
+20
View File
@@ -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<Arc<AppState>>`.
#[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<RwLock<HashMap<String, WebChallenge>>>,
}
+43 -12
View File
@@ -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<dyn std::error::Error>> {
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(())
}