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
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 {}
|