use std::sync::Arc; use axum::Router; use sqlx::postgres::PgPoolOptions; use tokio::net::TcpListener; use tower_http::cors::{Any, CorsLayer}; use tower_http::trace::TraceLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; mod api; mod config; mod db; mod middleware; mod models; mod services; use config::AppConfig; /// Shared application state available to all handlers pub struct AppState { pub db: sqlx::PgPool, pub nats: Option, pub config: AppConfig, } #[tokio::main] async fn main() -> anyhow::Result<()> { // Load environment variables dotenvy::dotenv().ok(); // Initialize tracing tracing_subscriber::registry() .with(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { "corrosion_api=debug,tower_http=debug".into() })) .with(tracing_subscriber::fmt::layer()) .init(); let config = AppConfig::from_env()?; // Database connection pool let db = PgPoolOptions::new() .max_connections(config.database_max_connections) .connect(&config.database_url) .await?; tracing::info!("Connected to PostgreSQL"); // Run migrations sqlx::migrate!("./migrations").run(&db).await?; tracing::info!("Database migrations applied"); // NATS connection (optional for dev) let nats = match async_nats::connect(&config.nats_url).await { Ok(client) => { tracing::info!("Connected to NATS at {}", config.nats_url); Some(client) } Err(e) => { tracing::warn!("NATS not available ({}), running without event bus", e); None } }; // Bootstrap: create admin user + license on first run bootstrap_admin(&db).await; // Initialize background services if NATS is available if let Some(ref nats_client) = nats { let nats_bridge = Arc::new(services::nats_bridge::NatsBridge::new(nats_client.clone())); // Start stats consumer let stats_consumer = services::stats_consumer::StatsConsumerService::new( db.clone(), nats_bridge.clone(), ); if let Err(e) = stats_consumer.start().await { tracing::error!("Failed to start stats consumer: {}", e); } // Start scheduler service let scheduler = services::scheduler::SchedulerService::new( db.clone(), nats_bridge.clone(), ) .await?; // Register stats jobs if let Err(e) = scheduler.register_stats_aggregation().await { tracing::error!("Failed to register stats aggregation job: {}", e); } if let Err(e) = scheduler.register_stats_cleanup().await { tracing::error!("Failed to register stats cleanup job: {}", e); } if let Err(e) = scheduler.start().await { tracing::error!("Failed to start scheduler: {}", e); } else { tracing::info!("Scheduler service started"); } } else { tracing::warn!("Skipping background services (NATS not available)"); } let state = Arc::new(AppState { db, nats, config }); // CORS — permissive in dev, locked down in production let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any); // Build router let app = Router::new() .nest("/api/auth", api::auth::router()) .nest("/api/servers", api::servers::router()) .nest("/api/wipes", api::wipes::router()) .nest("/api/maps", api::maps::router()) .nest("/api/plugins", api::plugins::router()) .nest("/api/panels", api::panels::router()) .nest("/api/schedules", api::schedules::router()) .nest("/api/logs", api::logs::router()) .nest("/api/public", api::public::router()) .nest("/api/team", api::team::router()) .nest("/api/notifications", api::notifications::router()) .nest("/api/license", api::license::router()) .nest("/api/store", api::store::router()) .nest("/api/early-access", api::early_access::router()) .nest("/api/admin", api::admin::router()) .nest("/api/ws", api::ws::router()) .nest("/api/analytics", api::analytics::router()) .nest("/api/plugin", api::plugin::router()) .nest("/api/settings", api::settings::router()) .nest("/api/modules", api::modules::router()) .layer(cors) .layer(TraceLayer::new_for_http()) .with_state(state); let bind_addr = "0.0.0.0:3000"; let listener = TcpListener::bind(bind_addr).await?; tracing::info!("Corrosion API listening on {}", bind_addr); axum::serve(listener, app).await?; Ok(()) } /// Bootstrap: if no users exist and ADMIN_EMAIL/ADMIN_PASSWORD are set, /// create the initial admin user and a dev license key. async fn bootstrap_admin(db: &sqlx::PgPool) { let admin_email = std::env::var("ADMIN_EMAIL").unwrap_or_default(); let admin_password = std::env::var("ADMIN_PASSWORD").unwrap_or_default(); let admin_username = std::env::var("ADMIN_USERNAME").unwrap_or_else(|_| "Commander".to_string()); if admin_email.is_empty() || admin_password.is_empty() { return; } // Check if any users exist let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users") .fetch_one(db) .await .unwrap_or((0,)); if count.0 > 0 { tracing::debug!("Users already exist, skipping bootstrap"); return; } tracing::info!("No users found — bootstrapping admin user: {}", admin_email); // Hash password let password_hash = match services::auth::hash_password(&admin_password) { Ok(h) => h, Err(e) => { tracing::error!("Failed to hash admin password: {e}"); return; } }; // Create admin user let user_id = match db::users::create_user(db, &admin_email, &admin_username, &password_hash).await { Ok(id) => id, Err(e) => { tracing::error!("Failed to create admin user: {e}"); return; } }; // Flag as super-admin if let Err(e) = sqlx::query("UPDATE users SET is_super_admin = true WHERE id = $1") .bind(user_id) .execute(db) .await { tracing::error!("Failed to set super-admin flag: {e}"); } // Create a license for the admin let license_key = std::env::var("ADMIN_LICENSE_KEY") .unwrap_or_else(|_| format!("CORROSION-{}", services::encryption::generate_token(8).to_uppercase())); match db::licenses::create_license(db, &license_key, user_id).await { Ok(_) => { tracing::info!("Bootstrap complete — admin: {}, license: {}", admin_email, license_key); } Err(e) => { tracing::error!("Failed to create admin license: {e}"); } } }