diff --git a/Cargo.toml b/Cargo.toml index a3d4218..12e294d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ base64 = "0.22" thiserror = "2" urlencoding = "2" async-trait = "0.1" +migration = { path = "migration" } [dev-dependencies] axum-test = "20" diff --git a/src/main.rs b/src/main.rs index 1d63d7a..83cb8b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,104 @@ +use axum_extra::extract::cookie::Key; +use metrics_exporter_prometheus::PrometheusBuilder; +use sea_orm::{ConnectOptions, ConnectionTrait, Database, Statement}; +use sea_orm_migration::MigratorTrait; +use std::sync::Arc; use tracing_subscriber::{fmt, EnvFilter}; + +use webfingerd::cache::Cache; +use webfingerd::challenge::RealChallengeVerifier; use webfingerd::config::Settings; +use webfingerd::handler; +use webfingerd::reaper; +use webfingerd::state::AppState; #[tokio::main] async fn main() { + // Structured JSON logging with env filter fmt() .with_env_filter(EnvFilter::from_default_env()) .json() .init(); + // Load configuration let settings = Settings::load().expect("failed to load configuration"); tracing::info!(listen = %settings.server.listen, "starting webfingerd"); + + // Connect to database + let db_url = format!("sqlite://{}?mode=rwc", settings.database.path); + let opt = ConnectOptions::new(&db_url); + let db = Database::connect(opt) + .await + .expect("failed to connect to database"); + + // Enable WAL mode for better concurrent read performance + if settings.database.wal_mode { + db.execute(Statement::from_string( + sea_orm::DatabaseBackend::Sqlite, + "PRAGMA journal_mode=WAL".to_string(), + )) + .await + .expect("failed to set WAL mode"); + tracing::info!("SQLite WAL mode enabled"); + } + + // Run migrations + migration::Migrator::up(&db, None) + .await + .expect("failed to run migrations"); + tracing::info!("database migrations applied"); + + // Hydrate cache from database + let cache = Cache::new(); + cache + .hydrate(&db) + .await + .expect("failed to hydrate cache"); + tracing::info!("cache hydrated"); + + // Install Prometheus metrics recorder + let metrics_handle = PrometheusBuilder::new() + .install_recorder() + .expect("failed to install metrics recorder"); + + // Derive cookie signing key from session secret + let cookie_key = Key::from(settings.ui.session_secret.as_bytes()); + + // Spawn background reaper for expired links + reaper::spawn_reaper( + db.clone(), + cache.clone(), + settings.cache.reaper_interval_secs, + ); + tracing::info!( + interval_secs = settings.cache.reaper_interval_secs, + "reaper task spawned" + ); + + // Build application state + let state = AppState { + db, + cache, + settings: Arc::new(settings.clone()), + challenge_verifier: Arc::new(RealChallengeVerifier), + metrics_handle, + cookie_key, + }; + + // Build router + let app = handler::router(state); + + // Bind and serve with graceful shutdown + let listener = tokio::net::TcpListener::bind(&settings.server.listen) + .await + .expect("failed to bind"); + tracing::info!(listen = %settings.server.listen, "webfingerd started"); + + axum::serve(listener, app) + .with_graceful_shutdown(async { + tokio::signal::ctrl_c().await.ok(); + tracing::info!("shutting down"); + }) + .await + .expect("server error"); }