You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

145 lines
5.0 KiB
Rust

//! 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<dyn db::QuoteRepository + Send + Sync> = 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<worker::Body>` before passing
/// it here. The Axum router is called directly and its response is returned
/// as `http::Response<axum::body::Body>`, 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<http::Response<axum::body::Body>> {
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<dyn db::QuoteRepository + Send + Sync> = 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 {}