lightscale-admin/backend/src/main.rs
centra 98eb7057a5
Some checks failed
build-local-image / build (push) Has been cancelled
Implement user-bound join flows and add admin image build pipeline
2026-02-14 15:46:25 +09:00

110 lines
3.1 KiB
Rust

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<CorsLayer> {
if allowed_origins.is_empty() {
return None;
}
let origins = allowed_origins
.iter()
.map(|origin| origin.parse())
.collect::<Result<Vec<_>, _>>()
.ok()?;
Some(
CorsLayer::new()
.allow_origin(AllowOrigin::list(origins))
.allow_methods(Any)
.allow_headers(Any)
.allow_credentials(true),
)
}