mod app; mod auth; mod error; mod routes; mod service; mod state; mod storage; use std::{env, net::SocketAddr, path::PathBuf}; use crank_registry::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::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 registry = PostgresRegistry::connect_with_options(database_options_from_env()?).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 secret_crypto = SecretCrypto::new(&env::var("CRANK_MASTER_KEY")?)?; let service = AdminService::new(registry, storage_root, auth_settings, secret_crypto); service.bootstrap_admin_user().await?; if env_flag("CRANK_DEMO_SEED") { service.seed_demo_assets().await?; } let state = AppState { service }; let app = build_app(state); let listener = TcpListener::bind(socket_addr).await?; 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 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)) }