use multithreaded::{config::Config, csrf, AppState}; use sqlx::PgPool; use tokio::net::TcpListener; use tower_http::services::ServeDir; use tower_sessions::SessionManagerLayer; use tower_sessions::cookie::SameSite; use tower_sessions::ExpiredDeletion; use tower_sessions_sqlx_store::PostgresStore; use tracing_subscriber::EnvFilter; #[tokio::main] async fn main() { tracing_subscriber::fmt() .with_env_filter(EnvFilter::from_default_env()) .init(); dotenvy::dotenv().ok(); let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); let pool = PgPool::connect(&database_url) .await .expect("failed to connect to database"); sqlx::migrate!() .run(&pool) .await .expect("failed to run migrations"); tracing::info!("migrations applied"); // Seed initial data if --seed flag is passed, then exit if std::env::args().any(|a| a == "--seed") { multithreaded::seed::run(&pool).await; tracing::info!("seed data inserted"); return; } let config = Config::from_env(); // Optional S3 storage for image uploads let s3 = if let Some(ref s3_config) = config.s3 { match multithreaded::storage::S3Storage::new(s3_config).await { Ok(client) => { tracing::info!("S3 storage configured (bucket: {})", s3_config.bucket); Some(std::sync::Arc::new(client)) } Err(e) => { tracing::warn!("S3 storage unavailable: {e}"); None } } } else { tracing::info!("S3 storage not configured (image uploads disabled)"); None }; let state = AppState { db: pool.clone(), config, http: reqwest::Client::builder() .timeout(std::time::Duration::from_secs(15)) .connect_timeout(std::time::Duration::from_secs(5)) .build() .expect("failed to build HTTP client"), preview_http: multithreaded::link_preview::build_preview_client(), s3, }; // Session store backed by PostgreSQL let session_store = PostgresStore::new(pool); session_store.migrate().await.expect("failed to migrate session store"); let deletion_task = tokio::task::spawn( session_store .clone() .continuously_delete_expired(tokio::time::Duration::from_secs(3600)), ); let session_layer = SessionManagerLayer::new(session_store) .with_name("mt_session") .with_same_site(SameSite::Lax) .with_expiry(tower_sessions::Expiry::OnInactivity( time::Duration::days(7), )) .with_secure(state.config.cookie_secure); let app = multithreaded::routes::forum_routes(state.clone()) .layer(axum::middleware::from_fn(csrf::csrf_middleware)) .layer(session_layer) .layer(tower_http::set_header::SetResponseHeaderLayer::overriding( axum::http::header::CONTENT_SECURITY_POLICY, axum::http::HeaderValue::from_static( "default-src 'self'; img-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'", ), )) .layer(tower_http::set_header::SetResponseHeaderLayer::overriding( axum::http::header::X_CONTENT_TYPE_OPTIONS, axum::http::HeaderValue::from_static("nosniff"), )) .layer(tower_http::set_header::SetResponseHeaderLayer::overriding( axum::http::header::X_FRAME_OPTIONS, axum::http::HeaderValue::from_static("DENY"), )) .layer(tower_http::set_header::SetResponseHeaderLayer::if_not_present( axum::http::header::CACHE_CONTROL, axum::http::HeaderValue::from_static("private, no-cache"), )) // Internal API routes — HMAC auth only, no CSRF/session middleware .merge(multithreaded::routes::internal::internal_routes(state)) .nest_service("/static", ServeDir::new("static")); let host = std::env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); let port = std::env::var("PORT").unwrap_or_else(|_| "3400".to_string()); let addr = format!("{host}:{port}"); let listener = TcpListener::bind(&addr) .await .expect("failed to bind"); tracing::info!("listening on {}", listener.local_addr().unwrap()); axum::serve( listener, app.into_make_service_with_connect_info::(), ) .with_graceful_shutdown(shutdown_signal()) .await .expect("server error"); deletion_task.abort(); let _ = deletion_task.await; } async fn shutdown_signal() { use tokio::signal; let ctrl_c = async { signal::ctrl_c().await.expect("failed to install Ctrl+C handler") }; #[cfg(unix)] let terminate = async { signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("failed to install signal handler") .recv() .await; }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => {}, _ = terminate => {}, } tracing::info!("Shutdown signal received"); }