mod api; mod app_state; mod audit_log; mod auth; mod config; mod control_plane; mod db; mod models; mod oidc; mod permissions; mod rbac; use crate::api::auth::ensure_bootstrap_admin; use crate::app_state::AppState; use crate::config::Config; use axum::routing::get; use axum::Router; use std::net::SocketAddr; use tower_http::cors::{AllowOrigin, CorsLayer, Any}; use tower_http::services::{ServeDir, ServeFile}; use tower_http::trace::TraceLayer; use tracing_subscriber::EnvFilter; #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt() .with_env_filter(EnvFilter::from_default_env()) .init(); let config = Config::load()?; let pool = db::connect(&config.database).await?; let mut disable_migration_locking = config.database.disable_migration_locking; if !disable_migration_locking { match db::is_cockroach(&pool).await { Ok(true) => { tracing::info!("detected CockroachDB, disabling SQLx migration locking"); disable_migration_locking = true; } Ok(false) => {} Err(err) => { tracing::warn!( error = ?err, "failed to detect database engine, keeping migration locking enabled" ); } } } let mut migrator = sqlx::migrate!("./migrations"); if disable_migration_locking { // CockroachDB does not implement pg_advisory_lock used by SQLx migration locking. migrator.set_locking(false); } migrator.run(&pool).await?; ensure_bootstrap_admin(&pool, &config).await?; let oidc = oidc::load_providers(&config.oidc, &config.server.base_url).await?; let state = AppState { pool, config: config.clone(), oidc, }; let mut app = Router::new() .route("/healthz", get(|| async { "ok" })) .nest("/admin/api", api::router()) .layer(TraceLayer::new_for_http()); if let Some(static_dir) = &config.server.static_dir { let index_path = format!("{}/index.html", static_dir.trim_end_matches('/')); let service = ServeDir::new(static_dir).fallback(ServeFile::new(index_path)); app = app.nest_service("/", service); } if let Some(cors_layer) = build_cors(&config.server.allowed_origins) { app = app.layer(cors_layer); } let app = app.with_state(state); let addr: SocketAddr = config.server.bind.parse()?; tracing::info!("lightscale-admin listening on {}", addr); let listener = tokio::net::TcpListener::bind(addr).await?; axum::serve(listener, app).await?; Ok(()) } fn build_cors(allowed_origins: &[String]) -> Option { if allowed_origins.is_empty() { return None; } let origins = allowed_origins .iter() .map(|origin| origin.parse()) .collect::, _>>() .ok()?; Some( CorsLayer::new() .allow_origin(AllowOrigin::list(origins)) .allow_methods(Any) .allow_headers(Any) .allow_credentials(true), ) }