//! API server binary entrypoint. //! //! On native targets: starts a Tokio/Axum HTTP server on `0.0.0.0:3000`. //! On wasm32 targets: exports a `fetch` event handler for Cloudflare Workers, //! wiring Cloudflare D1 through [`db::d1::D1Repository`] into the Axum router. // On wasm32 there is no `main` entry point; the Workers runtime invokes the // exported `fetch` function produced by `#[worker::event(fetch)]` instead. #![cfg_attr(target_arch = "wasm32", no_main)] mod db; mod handlers; #[cfg(not(target_arch = "wasm32"))] use std::sync::Arc; #[cfg(not(target_arch = "wasm32"))] use db::QuoteRepository as _; #[cfg(not(target_arch = "wasm32"))] use quotesdb::generate_auth_code; #[cfg(not(target_arch = "wasm32"))] #[tokio::main] async fn main() { let db_path = std::env::var("DATABASE_URL").unwrap_or_else(|_| "quotesdb.sqlite".to_string()); let repo = db::connection::open(&db_path) .await .expect("failed to open database"); repo.run_migrations().await.expect("migrations failed"); // Seed admin auth code on first startup if repo .get_admin_auth_code() .await .expect("failed to check admin code") .is_none() { let code = generate_auth_code(); repo.seed_admin_auth_code(&code) .await .expect("failed to seed admin code"); } // Seed submissions_locked = '0' on first startup (INSERT OR IGNORE — no-op // if the key already exists, so this never overwrites an active lock). repo.seed_submissions_locked() .await .expect("failed to seed submissions lock"); let admin_code = repo .get_admin_auth_code() .await .expect("failed to read admin code") .unwrap(); eprintln!("╔══════════════════════════════════════════════╗"); eprintln!("║ ADMIN AUTH CODE: {admin_code:<28}║"); eprintln!("╚══════════════════════════════════════════════╝"); let repo: Arc = Arc::new(repo); let app = handlers::router(repo); let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string()); let addr = format!("0.0.0.0:{port}"); eprintln!("quotesdb API listening on {addr}"); let listener = tokio::net::TcpListener::bind(&addr) .await .expect("failed to bind listener"); axum::serve(listener, app).await.expect("server error"); } /// Cloudflare Workers `fetch` event handler. /// /// With the `http` feature enabled on `worker`, the macro converts the /// incoming JS request into an `http::Request` before passing /// it here. The Axum router is called directly and its response is returned /// as `http::Response`, which the macro converts back to a /// JS response before returning it to the runtime. /// /// D1 bindings are retrieved from the Worker environment and wrapped in an /// `Arc` for sharing across Axum state. #[cfg(target_arch = "wasm32")] #[worker::event(fetch)] pub async fn fetch( req: worker::HttpRequest, env: worker::Env, _ctx: worker::Context, ) -> worker::Result> { use std::sync::Arc; use tower_service::Service; // Retrieve the D1 database binding named "DB" from the Worker environment. let db = env.d1("DB")?; // Create the D1-backed repository and run schema migrations on startup. let repo = db::d1::D1Repository::new(db); // Bring QuoteRepository trait into scope for `run_migrations`. use db::QuoteRepository as _; repo.run_migrations() .await .map_err(|e| worker::Error::RustError(e.to_string()))?; // Seed admin auth code on first startup (no-op if already present). if repo .get_admin_auth_code() .await .map_err(|e| worker::Error::RustError(e.to_string()))? .is_none() { let code = quotesdb::generate_auth_code(); repo.seed_admin_auth_code(&code) .await .map_err(|e| worker::Error::RustError(e.to_string()))?; } // Seed submissions_locked = '0' on first startup (INSERT OR IGNORE — no-op // if the key already exists, so this never overwrites an active lock). repo.seed_submissions_locked() .await .map_err(|e| worker::Error::RustError(e.to_string()))?; // Wrap in Arc so it can be shared across handlers via Axum state. // D1Repository is `unsafe impl Send + Sync` — safe on single-threaded wasm32. let repo: Arc = Arc::new(repo); // Build the Axum router with all routes registered. let mut router = handlers::router(repo); // Call the Axum router directly; the http feature ensures request/response // types are compatible with standard http crate types that Axum expects. Ok(router .call(req) .await .map_err(|e| worker::Error::RustError(e.to_string()))?) } #[cfg(test)] mod tests {}