//! 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). Opens its own connection (CLI /// one-shot); reads the password from the `BOOTSTRAP_PASSWORD` env var if set, /// otherwise prompts (hidden input). The plaintext is not zeroized, but it is /// confined to the scope below and dropped before any network I/O. 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}"))?; // Read, validate, and hash the password in a scope so the plaintext `String` is // dropped before we open a connection / run any awaits. let password_hash = { let password = match std::env::var("BOOTSTRAP_PASSWORD") { Ok(p) => p, Err(_) => rpassword::prompt_password("Password: ").context("reading password")?, }; anyhow::ensure!( password.chars().count() >= 8, "password must be at least 8 characters" ); 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(()) }