//! Server wiring: configuration and startup. mod config; pub use config::Config; use anyhow::Context; use api::{AppState, build_app, migrate_sessions}; use db::Db; use domain::{AuditActor, Email, NewUser, Role}; use tokio::net::TcpListener; /// Connect dependencies from `config` and serve until shutdown. pub async fn run(config: Config) -> anyhow::Result<()> { let db = Db::connect(&config.database_url) .await .context("connecting to the database")?; db.migrate().await.context("running database migrations")?; migrate_sessions(&db) .await .context("creating the session store")?; let state = AppState { db, app_name: config.app_name.clone(), cookie_secure: config.cookie_secure, }; let listener = TcpListener::bind(&config.bind_addr) .await .with_context(|| format!("binding to {}", config.bind_addr))?; tracing::info!(addr = %config.bind_addr, "server listening"); serve(listener, state).await } /// Serve the API on an already-bound listener (used by `run` and tests). pub async fn serve(listener: TcpListener, state: AppState) -> anyhow::Result<()> { let app = build_app(state); axum::serve(listener, app) .await .context("running the HTTP server")?; Ok(()) } /// Create a user from the CLI (admin bootstrap). Reads the password from the /// `BOOTSTRAP_PASSWORD` env var if set, otherwise prompts (hidden input). pub async fn create_user(database_url: &str, email: &str, role: Role) -> anyhow::Result<()> { let email = Email::parse(email).map_err(|err| anyhow::anyhow!("{err}"))?; let password = match std::env::var("BOOTSTRAP_PASSWORD") { Ok(p) => p, Err(_) => rpassword::prompt_password("Password: ").context("reading password")?, }; anyhow::ensure!( password.len() >= 8, "password must be at least 8 characters" ); let password_hash = auth::hash_password(&password).map_err(|err| anyhow::anyhow!("hashing password: {err}"))?; let db = Db::connect(database_url) .await .context("connecting to the database")?; let mut tx = db.pool().begin().await?; let id = db::users::create_user( &mut tx, AuditActor::System, &NewUser { email, password_hash, role, }, ) .await .context("creating the user (is the email already taken?)")?; tx.commit().await?; println!("created user {id} ({role:?})"); Ok(()) }