use std::{ env::VarError, io::Write, net::{Ipv4Addr, SocketAddr}, }; use axum::{ routing::{get, MethodRouter}, Router, }; use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; use tokio::net::TcpListener; use tower_http::services::ServeDir; #[macro_use] extern crate justerror; #[macro_use] extern crate lazy_static; mod handlers; mod templates; mod user; #[tokio::main] async fn main() { use handlers::handlers::{get_signup, payment_success, post_signup}; init(); // for javascript and css // TODO: figure out how to intern these contents let assets_dir = std::env::current_dir().unwrap().join("assets"); let assets_svc = ServeDir::new(assets_dir.as_path()); let pool = db().await; sqlx::migrate!().run(&pool).await.unwrap(); // the core application, defining the routes and handlers let app = Router::new() .nest_service("/assets", assets_svc) .stripped_clone("/signup/", get(get_signup).post(post_signup)) .stripped_clone("/payment_success/", get(payment_success)) .route("/payment_success/:receipt", get(payment_success)) .with_state(pool.clone()) .into_make_service(); let listener = mklistener().await; axum::serve(listener, app) .with_graceful_shutdown(shutdown_signal()) .await .unwrap(); pool.close().await; } //-************************************************************************ // li'l helpers //-************************************************************************ fn init() { dotenvy::dotenv().expect("Could not read .env file."); env_logger::builder() .format(|buf, record| { let ts = buf.timestamp(); writeln!(buf, "{}: {}", ts, record.args()) }) .init(); } async fn db() -> SqlitePool { //let dbfile = std::env::var("DATABASE_URL").unwrap(); let opts = SqliteConnectOptions::new() .foreign_keys(true) .create_if_missing(true) .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) .optimize_on_close(true, None); SqlitePoolOptions::new().connect_with(opts).await.unwrap() } async fn mklistener() -> TcpListener { let ip = std::env::var("LISTENING_ADDR").expect("Could not find $LISTENING_ADDR in environment"); let ip: Ipv4Addr = ip .parse() .unwrap_or_else(|_| panic!("Could not parse {ip} as an IP address")); let port: u16 = std::env::var("LISTENING_PORT") .and_then(|p| p.parse().map_err(|_| VarError::NotPresent)) .unwrap_or_else(|_| { panic!("Could not find LISTENING_PORT in env or parse if present"); }); let addr = SocketAddr::from((ip, port)); TcpListener::bind(&addr).await.unwrap() } /// Adds both routes, with and without a trailing slash. trait RouterPathStrip where S: Clone + Send + Sync + 'static, { fn stripped_clone(self, path: &str, method_router: MethodRouter) -> Self; } impl RouterPathStrip for Router where S: Clone + Send + Sync + 'static, { fn stripped_clone(self, path: &str, method_router: MethodRouter) -> Self { assert!(path.ends_with('/')); self.route(path, method_router.clone()) .route(path.trim_end_matches('/'), method_router) } } async fn shutdown_signal() { use tokio::signal; let ctrl_c = async { signal::ctrl_c() .await .expect("failed to install Ctrl+C handler"); }; #[cfg(unix)] let terminate = async { signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("failed to install signal handler") .recv() .await; }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => {log::info!("shutting down")}, _ = terminate => {}, } }