mod app; mod auth; mod error; mod rate_limit; mod request_context; mod routes; mod service; mod state; mod storage; use std::{env, net::SocketAddr, path::PathBuf}; use crank_registry::{PostgresPoolConfig, PostgresRegistry}; use sqlx::postgres::PgConnectOptions; use tokio::net::TcpListener; use tracing::info; use crate::{ app::build_app, auth::{AuthSettings, BootstrapAdminConfig}, service::AdminService, state::AppState, }; use crank_runtime::{ RequestRateLimitConfig, RequestRateLimiter, RuntimeExecutor, RuntimeLimits, SecretCrypto, }; #[tokio::main] async fn main() -> Result<(), Box> { tracing_subscriber::fmt() .with_env_filter( env::var("CRANK_LOG_LEVEL").unwrap_or_else(|_| "admin_api=info,tower_http=info".into()), ) .init(); let storage_root = PathBuf::from( env::var("CRANK_STORAGE_ROOT").unwrap_or_else(|_| "/var/lib/crank/storage".into()), ); let bind_addr = env::var("CRANK_ADMIN_BIND").unwrap_or_else(|_| "0.0.0.0:3001".into()); let base_url = env::var("CRANK_BASE_URL").unwrap_or_else(|_| "http://localhost:3000".into()); let socket_addr: SocketAddr = bind_addr.parse()?; let pool_config = PostgresPoolConfig::from_env()?; let registry = PostgresRegistry::connect_with_options_and_pool_config( database_options_from_env()?, pool_config, ) .await?; let auth_settings = AuthSettings { session_secret: env::var("CRANK_SESSION_SECRET")?, password_pepper: env::var("CRANK_PASSWORD_PEPPER")?, session_ttl_hours: env::var("CRANK_SESSION_TTL_HOURS") .ok() .and_then(|value| value.parse::().ok()) .unwrap_or(24), cookie_secure: base_url.starts_with("https://"), bootstrap_admin: BootstrapAdminConfig { email: env::var("CRANK_BOOTSTRAP_ADMIN_EMAIL")?, password: env::var("CRANK_BOOTSTRAP_ADMIN_PASSWORD")?, display_name: env::var("CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME") .unwrap_or_else(|_| "Crank Owner".into()), }, }; let runtime_limits = RuntimeLimits::from_env()?; let api_rate_limit = admin_api_rate_limit_config_from_env()?; let secret_crypto = SecretCrypto::new(&env::var("CRANK_MASTER_KEY")?)?; let runtime = RuntimeExecutor::with_limits(runtime_limits); let service = AdminService::new_with_runtime( registry, storage_root, auth_settings, secret_crypto, runtime, ); service.bootstrap_admin_user().await?; if env_flag("CRANK_DEMO_SEED") { service.seed_demo_assets().await?; } let state = AppState { service, api_rate_limiter: RequestRateLimiter::new(api_rate_limit), }; let app = build_app(state); let listener = TcpListener::bind(socket_addr).await?; info!( runtime_max_concurrent_unary = runtime_limits.max_concurrent_unary, runtime_max_concurrent_window = runtime_limits.max_concurrent_window, runtime_max_concurrent_sessions = runtime_limits.max_concurrent_sessions, runtime_max_concurrent_jobs = runtime_limits.max_concurrent_jobs, admin_rate_limit_rps = api_rate_limit.requests_per_second, admin_rate_limit_burst = api_rate_limit.burst, max_connections = pool_config.max_connections, min_connections = pool_config.min_connections, acquire_timeout_ms = pool_config.acquire_timeout_ms, idle_timeout_ms = pool_config.idle_timeout_ms, max_lifetime_ms = pool_config.max_lifetime_ms, "postgres pool configured" ); info!("admin-api listening on {}", socket_addr); axum::serve(listener, app).await?; Ok(()) } fn env_flag(name: &str) -> bool { matches!( env::var(name) .ok() .as_deref() .map(str::to_ascii_lowercase) .as_deref(), Some("1" | "true" | "yes" | "on") ) } fn admin_api_rate_limit_config_from_env() -> Result> { let requests_per_second = env::var("CRANK_ADMIN_RATE_LIMIT_RPS") .ok() .and_then(|value| value.parse::().ok()) .unwrap_or(30); let burst = env::var("CRANK_ADMIN_RATE_LIMIT_BURST") .ok() .and_then(|value| value.parse::().ok()) .unwrap_or(60); Ok(RequestRateLimitConfig::new(requests_per_second, burst)?) } fn database_options_from_env() -> Result> { if let Ok(database_url) = env::var("CRANK_DATABASE_URL") { return Ok(database_url.parse::()?); } let host = env::var("POSTGRES_HOST").unwrap_or_else(|_| "postgres".into()); let port = env::var("POSTGRES_PORT") .ok() .and_then(|value| value.parse::().ok()) .unwrap_or(5432); let database = env::var("POSTGRES_DB").unwrap_or_else(|_| "crank".into()); let username = env::var("POSTGRES_USER").unwrap_or_else(|_| "crank".into()); let password = env::var("POSTGRES_PASSWORD").unwrap_or_else(|_| "crank".into()); Ok(PgConnectOptions::new() .host(&host) .port(port) .database(&database) .username(&username) .password(&password)) }