use actix_cors::Cors; use actix_web::{ App, HttpServer, cookie::{Key, SameSite}, middleware::Logger, web::Data, }; use clap::Parser; use utoipa::OpenApi; use crate::cli::{CliArgs, Command}; mod api; mod app; mod cli; mod storage; use actix_identity::IdentityMiddleware; use actix_session::{SessionMiddleware, storage::CookieSessionStore}; #[actix_web::main] #[expect( clippy::needless_for_each, reason = "utoipa generates this, we can't change it" )] async fn main() -> Result<(), std::io::Error> { #[derive(OpenApi)] #[openapi( paths( api::get::auth::product::product_by_id, api::get::auth::product::product_by_name, api::get::auth::product::product_suggestion_by_name, api::get::auth::product::products_registered, api::get::auth::product::products_in_storage, api::get::auth::product::products_by_product_parent_id_indirect, api::get::auth::product::products_by_product_parent_id_direct, api::get::auth::product_parent::product_parents, api::get::auth::product_parent::product_parents_toplevel, api::get::auth::product_parent::product_parents_under, api::get::auth::recipe::recipe_by_id, api::get::auth::recipe::recipes, api::get::auth::unit::units, api::get::auth::unit::units_by_property_id, api::get::auth::unit::unit_by_id, api::get::auth::unit_property::unit_property_by_id, api::get::auth::unit_property::unit_properties, api::get::auth::inventory::amount_by_id, api::get::auth::user::users, api::get::auth::user::user_by_id, // api::set::auth::product::register_product, api::set::auth::product::associate_barcode, api::set::auth::product_parent::register_product_parent, api::set::auth::recipe::add_recipe, api::set::auth::unit::register_unit, api::set::auth::unit_property::register_unit_property, api::set::auth::barcode::buy_barcode, api::set::auth::barcode::consume_barcode, api::set::auth::user::register_user, // api::set::no_auth::user::login, api::set::no_auth::user::logout, api::set::no_auth::user::provision, ), )] struct ApiDoc; env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); // When using `Key::generate()` it is important to initialize outside of the // `HttpServer::new` closure. When deployed the secret key should be read from a // configuration file or environment variables. // TODO: Load from a config file. <2025-12-07> let secret_key = Key::generate(); let args = CliArgs::parse(); match args.command { Command::Serve { host, port, db_path, print_port, } => { let data = Data::new( app::App::new(db_path) .await .map_err(|err| std::io::Error::other(main::Error::AppInit(err)))?, ); let srv = HttpServer::new(move || { App::new() // TODO: Remove before an actual deploy <2025-09-26> .wrap(Cors::permissive()) .wrap(Logger::new( r#"%a "%r" -> %s %b ("%{Referer}i" "%{User-Agent}i" %T s)"#, )) // Install the identity framework before middleware (as actix uses FILO). .wrap(IdentityMiddleware::default()) .wrap( SessionMiddleware::builder( CookieSessionStore::default(), secret_key.clone(), ) .cookie_secure(true) .cookie_http_only(true) .cookie_same_site(SameSite::Strict) .build(), ) .app_data(Data::clone(&data)) .configure(api::get::auth::register_paths) .configure(api::get::no_auth::register_paths) .configure(api::set::auth::register_paths) .configure(api::set::no_auth::register_paths) }) .bind((host, port.unwrap_or(0)))?; let addr = srv.addrs()[0]; let run = srv.run(); if print_port { println!("{}", addr.port()); } eprintln!("Serving at http://{addr}"); run.await } Command::OpenApi => { let openapi = ApiDoc::openapi(); println!("{}", openapi.to_pretty_json().expect("Comp-time constant")); Ok(()) } } } pub(crate) mod main { use crate::app::app_create; #[derive(thiserror::Error, Debug)] pub(crate) enum Error { #[error("Failed to initialize shared application state: {0}")] AppInit(#[from] app_create::Error), } }