From ee252aec132b3b4a6e31876207bf45851e30469f Mon Sep 17 00:00:00 2001 From: Chris Cochrun Date: Mon, 23 Jun 2025 15:13:42 -0500 Subject: [PATCH] fixing up the api to work better for nix and move the table format --- Cargo.lock | 117 +++++++++++++++++++++++++++++++++- Cargo.toml | 3 +- flake.nix | 16 +++-- justfile | 4 +- src/api/camp_form.rs | 59 +++++++++-------- src/api/health_form.rs | 140 +++++++++++++++++++++++++++++++---------- src/main.rs | 62 +++++++++++------- 7 files changed, 305 insertions(+), 96 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94bfa3b..dd1bd86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,6 +335,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.98" @@ -639,6 +689,46 @@ dependencies = [ "stacker", ] +[[package]] +name = "clap" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.103", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + [[package]] name = "color-eyre" version = "0.6.5" @@ -666,6 +756,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1624,6 +1720,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.15" @@ -2030,6 +2132,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "openssl" version = "0.10.73" @@ -3094,7 +3202,7 @@ dependencies = [ ] [[package]] -name = "tfcapi" +name = "tfcsite" version = "0.1.0" dependencies = [ "actix-cors", @@ -3103,6 +3211,7 @@ dependencies = [ "actix-rt", "actix-web", "async-std", + "clap", "color-eyre", "env_logger", "futures", @@ -3479,6 +3588,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index d46a7bc..0d64ed5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "tfcapi" +name = "tfcsite" version = "0.1.0" edition = "2021" @@ -32,6 +32,7 @@ tracing-actix-web = "0.7.14" color-eyre = "0.6.3" pretty_assertions = "1.4.1" sqlx = { version = "0.8.2", features = ["sqlite"] } +clap = { version = "4.5.40", features = ["derive"] } [profile.dev] opt-level = 0 diff --git a/flake.nix b/flake.nix index 34e4dbd..232cc95 100644 --- a/flake.nix +++ b/flake.nix @@ -66,15 +66,13 @@ # cp -r ${blowfish} themes/blowfish # ls themes/blowfish # ''; - # buildPhase = '' - # NODE_ENV=production ./themes/blowfish/node_modules/tailwindcss/lib/cli.js -c ./themes/blowfish/tailwind.config.js -i ./themes/blowfish/assets/css/main.css -o ./assets/css/compiled/main.css --jit && hugo --gc --minify - # ${pkgs.hugo}/bin/hugo --minify - # ''; - # installPhase = '' - # ls -l - # cp -r public $out/ - # ls -l $out - # ''; + buildPhase = '' + ${pkgs.tailwindcss_4}/bin/tailwindcss -i static/css/base.css -o static/css/main.css + ${pkgs.zola}/bin/zola build + ''; + installPhase = '' + cp -r public $out/ + ''; buildInputs = bi; nativeBuildInputs = nbi; }; diff --git a/justfile b/justfile index 5a39c36..b933bc9 100644 --- a/justfile +++ b/justfile @@ -3,7 +3,7 @@ default: build: tailwindcss -i static/css/base.css -o static/css/main.css && zola build serve: - zola serve + zola serve -p 4242 uglify: uglifyjs ./src/js/main.js --compress --mangle -o ./static/js/main.js && uglifyjs ./src/js/page.js --compress --mangle -o ./static/js/page.js && uglifyjs ./src/js/search.js --compress --mangle -o ./static/js/search.js && uglifyjs ./src/js/lang.js --compress --mangle -o ./static/js/lang.js dev: @@ -14,3 +14,5 @@ clean: cargo clean test: RUST_LOG=debug cargo test --benches --tests --all-features -- --nocapture +run: + tailwindcss -i static/css/base.css -o static/css/main.css --watch & zola build && cargo run -- -d diff --git a/src/api/camp_form.rs b/src/api/camp_form.rs index 73937a8..d82a5be 100644 --- a/src/api/camp_form.rs +++ b/src/api/camp_form.rs @@ -6,8 +6,8 @@ use color_eyre::eyre::{Context, Result}; use futures::FutureExt; use lettre::{message::SinglePart, Message}; use maud::{html, Markup, DOCTYPE}; -use reqwest::Client; -use tracing::{error, info}; +use reqwest::{Client, Response}; +use tracing::{debug, error, info}; #[derive(Debug, MultipartForm)] struct CampForm { @@ -39,27 +39,30 @@ struct CampForm { health_form: Text, } -impl From<&CampForm> for HashMap { +impl From<&CampForm> for HashMap<&str, String> { fn from(form: &CampForm) -> Self { let mut map = HashMap::new(); - map.insert(63, format!("{} {}", form.first_name.0, form.last_name.0)); map.insert( - 64, + "Full_Name", + format!("{} {}", form.first_name.0, form.last_name.0), + ); + map.insert( + "Parent_Name", format!("{} {}", form.parent_first_name.0, form.parent_last_name.0), ); - map.insert(65, form.parent_phone.0.clone()); - map.insert(66, form.parent_email.0.clone().clone()); - map.insert(67, form.birthdate.0.clone()); - map.insert(69, form.gender.0.clone()); - map.insert(70, form.street.0.clone()); - map.insert(71, form.city.0.clone()); - map.insert(72, form.state.0.clone()); - map.insert(73, form.zip.0.clone().to_string()); - map.insert(74, form.grade.0.clone()); - map.insert(75, form.week.0.clone()); - map.insert(76, form.shirt.0.clone()); - map.insert(77, form.registration.0.clone()); - map.insert(115, form.health_form.0.clone()); + map.insert("Parent_Phone", form.parent_phone.0.clone()); + map.insert("Parent_Email", form.parent_email.0.clone().clone()); + map.insert("Birth_Date", form.birthdate.0.clone()); + map.insert("Gender", form.gender.0.clone()); + map.insert("Street_Address", form.street.0.clone()); + map.insert("City", form.city.0.clone()); + map.insert("State", form.state.0.clone()); + map.insert("Zipcode", form.zip.0.clone().to_string()); + map.insert("Grade", form.grade.0.clone()); + map.insert("Week_Chosen", form.week.0.clone()); + map.insert("Shirt_Size", form.shirt.0.clone()); + map.insert("Registration", form.registration.0.clone()); + map.insert("Health_Form", form.health_form.0.clone()); map } } @@ -292,19 +295,21 @@ pub async fn camp_form(MultipartForm(form): MultipartForm) -> HttpResp } } -async fn store_camp_form(map: HashMap) -> Result<()> { +async fn store_camp_form(map: HashMap<&str, String>) -> Result { let request = Client::new(); let mut json = HashMap::new(); - json.insert("data", map); - request - .post("https://staff.tfcconnection.org/apps/tables/api/1/tables/5/rows") - .basic_auth("chris", Some("2VHeGxeC^Zf9KqFK^G@Pt!zu2q^6@b")) - .header("OCS-APIRequest", "true") + let mut fields = HashMap::new(); + fields.insert("fields", map); + json.insert("records", vec![fields]); + let response = request + .post("https://table.tfcconnection.org/api/docs/e8SFoTHpmJuFQsiMhRTXCi/tables/Camp_Data/records") + .bearer_auth("b8189d1b315548aa610db2fd3a43177a967cd41f") .header("Content-Type", "application/json") .json(&json) .send() .await?; - Ok(()) + debug!(?response); + Ok(response) } #[cfg(test)] @@ -329,7 +334,7 @@ mod test { grade: Text(String::from("junior")), shirt: Text(String::from("medium")), allergies: Text(String::from("Cool beans")), - week: Text(String::from("1")), + week: Text(String::from("week1")), registration: Text(String::from("later")), health_form: Text(String::from("I guess")), } @@ -342,7 +347,7 @@ mod test { let map = HashMap::from(&form); let res = store_camp_form(map).await; match res { - Ok(_) => assert!(true), + Ok(r) => assert!(r.status().is_success(), "Failed to store: {:?}", r), Err(e) => assert!(false, "Failed storing test: {e}"), } } diff --git a/src/api/health_form.rs b/src/api/health_form.rs index 5196a0a..d2f0300 100644 --- a/src/api/health_form.rs +++ b/src/api/health_form.rs @@ -12,7 +12,7 @@ use lettre::{ Message, }; use maud::{html, Markup, DOCTYPE}; -use reqwest::Client; +use reqwest::{Client, Response}; use tracing::{error, info}; use crate::email::send_email; @@ -75,40 +75,46 @@ struct HealthForm { registration: Text, } -impl From<&HealthForm> for HashMap { +impl From<&HealthForm> for HashMap<&str, String> { fn from(form: &HealthForm) -> Self { let mut map = HashMap::new(); - map.insert(37, format!("{} {}", form.first_name.0, form.last_name.0)); map.insert( - 38, + "Student_Name", + format!("{} {}", form.first_name.0, form.last_name.0), + ); + map.insert( + "Parent_Name", format!("{} {}", form.parent_first_name.0, form.parent_last_name.0), ); - map.insert(39, form.birthdate.0.clone()); - map.insert(40, form.street.0.clone()); - map.insert(41, form.city.0.clone()); - map.insert(42, form.state.0.clone()); - map.insert(43, form.zip.0.clone()); - map.insert(44, form.parent_cellphone.0.clone()); - map.insert(45, form.homephone.0.clone()); - map.insert(46, format!("{} {}", form.contact.0, form.contact_phone.0)); - map.insert(47, form.doctorname.0.clone()); - map.insert(48, form.doctorcity.0.clone()); - map.insert(49, form.doctorphone.0.clone()); - map.insert(50, form.medical.0.clone()); - map.insert(51, form.insurance.0.clone()); - map.insert(52, form.policy_number.0.clone()); - map.insert(54, form.agreement.0.clone()); + map.insert("Birthdate", form.birthdate.0.clone()); + map.insert("Street", form.street.0.clone()); + map.insert("City", form.city.0.clone()); + map.insert("State", form.state.0.clone()); + map.insert("Zipcode", form.zip.0.clone()); + map.insert("Parent_Phone", form.parent_cellphone.0.clone()); + map.insert("Homephone", form.homephone.0.clone()); map.insert( - 55, + "Emergency_Contact", + format!("{} {}", form.contact.0, form.contact_phone.0), + ); + map.insert("Doctor", form.doctorname.0.clone()); + map.insert("Doctor_City", form.doctorcity.0.clone()); + map.insert("Doctor_Phone", form.doctorphone.0.clone()); + map.insert("Medical_Coverage", form.medical.0.clone()); + map.insert("Insurance_Name", form.insurance.0.clone()); + map.insert("Policy_Number", form.policy_number.0.clone()); + map.insert("Agreement", form.agreement.0.clone()); + map.insert( + "Allergies", format!("{} \n {}", form.allergies.0, form.allergies_other.0), ); - map.insert(56, form.specific_allergies.0.clone()); - map.insert(57, form.treatment.0.clone()); - map.insert(58, form.conditions.0.clone()); - map.insert(59, form.tetanus.0.clone()); - map.insert(60, form.medication.0.clone()); - map.insert(61, form.notes.0.clone()); - map.insert(62, form.swimming.0.clone()); + map.insert("Specific_Allergies", form.specific_allergies.0.clone()); + map.insert("Allergic_Treatments", form.treatment.0.clone()); + map.insert("Conditions", form.conditions.0.clone()); + map.insert("Tetanus_Shot_Date", form.tetanus.0.clone()); + map.insert("Medication_Schedule", form.medication.0.clone()); + map.insert("Other_Notes", form.notes.0.clone()); + map.insert("Swimming_Ability", form.swimming.0.clone()); map } } @@ -431,22 +437,22 @@ pub async fn health_form(MultipartForm(mut form): MultipartForm) -> // HttpResponse::Ok().body("hi") } -async fn store_form(map: HashMap) -> Result<()> { +async fn store_form(map: HashMap<&str, String>) -> Result { let client = Client::new(); let mut json = HashMap::new(); - json.insert("data", map); + let mut fields = HashMap::new(); + fields.insert("fields", map); + json.insert("records", vec![fields]); let res = client - .post("https://staff.tfcconnection.org/ocs/v2.php/apps/tables/api/2/tables/4/rows") - .basic_auth("chris", Some("2VHeGxeC^Zf9KqFK^G@Pt!zu2q^6@b")) - .header("OCS-APIRequest", "true") + .post("https://table.tfcconnection.org/api/docs/e8SFoTHpmJuFQsiMhRTXCi/tables/Health_Data/records") + .bearer_auth("b8189d1b315548aa610db2fd3a43177a967cd41f") .header("Content-Type", "application/json") .json(&json) .send() .await?; if res.status().is_success() { - let res = res.text().await.unwrap(); - Ok(()) + Ok(res) } else { Err(eyre!( "Problem in storing data: {:?}", @@ -454,3 +460,69 @@ async fn store_form(map: HashMap) -> Result<()> { )) } } + +#[cfg(test)] +mod test { + use super::*; + use actix_web::test; + + fn form() -> HealthForm { + HealthForm { + first_name: Text("Frodo".into()), + last_name: Text("Braggins".into()), + parent_first_name: Text("Bilbo".into()), + parent_last_name: Text("Braggins".into()), + birthdate: Text(String::from("1845-09-12")), + street: Text(String::from("1234 Bag End")), + city: Text(String::from("The Shire")), + state: Text(String::from("Hobbiton")), + zip: Text(String::from("88888")), + allergies: Text(String::from("Cool beans")), + registration: Text(String::from("later")), + parent_cellphone: Text(String::from("later")), + homephone: Text("test".into()), + contact: Text("test".into()), + contact_phone: Text("test".into()), + doctorname: Text("test".into()), + doctorcity: Text("test".into()), + doctorphone: Text("test".into()), + medical: Text("test".into()), + insurance: Text("test".into()), + policy_number: Text("test".into()), + allergies_other: Text("test".into()), + specific_allergies: Text("test".into()), + treatment: Text("test".into()), + conditions: Text("test".into()), + tetanus: Text("test".into()), + swimming: Text("test".into()), + medication: Text("test".into()), + notes: Text("test".into()), + agreement: Text("test".into()), + file: None, + } + } + + #[test] + async fn test_nc_post() { + let form = form(); + assert!(!form.first_name.is_empty()); + let map = HashMap::from(&form); + let res = store_form(map).await; + match res { + Ok(r) => assert!(r.status().is_success(), "Failed to store: {:?}", r), + Err(e) => assert!(false, "Failed storing test: {e}"), + } + } + + #[test] + async fn test_email() { + let mut form = form(); + assert!(!form.first_name.is_empty()); + match form.send_email() { + Ok(m) => { + assert!(crate::email::send_email(m).await.is_ok()) + } + Err(e) => assert!(false, "Failed emailing test: {e}"), + } + } +} diff --git a/src/main.rs b/src/main.rs index 9c8e783..9b9a00c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use actix_web::body::MessageBody; use actix_web::dev::{ServiceRequest, ServiceResponse}; use actix_web::{web, App, Error, HttpServer}; use api::camp_form::camp_form; -use api::contact::{self, contact_form}; +use api::contact::contact_form; use api::health_form::health_form; use api::local_trip_form::local_form; use api::mt_form::mt_form; @@ -15,6 +15,7 @@ use api::{ mt_church_form::mt_church_form, mt_parent_form::mt_parent_form, mt_teacher_form::mt_teacher_form, }; +use clap::Parser; use color_eyre::eyre::Context; use color_eyre::Result; use sqlx::{Connection, SqliteConnection}; @@ -45,45 +46,64 @@ impl RootSpanBuilder for DomainRootSpanBuilder { fn on_request_end(_span: Span, _outcome: &Result, Error>) {} } +#[derive(Debug, Parser)] +#[command(name = "tfcsite", version, about)] +struct Cli { + #[arg(short, long)] + dev: bool, +} + #[actix_web::main] async fn main() -> std::io::Result<()> { + let site; + let temp; + let logs; + if Cli::parse().dev { + site = "./public"; + logs = "/tmp/tfcsite"; + temp = "/tmp/tfcsite"; + } else { + site = "../public"; + logs = "/storage/logs/tfcsite"; + temp = "/tmp/tfcsite"; + } + + std::fs::create_dir_all(&logs)?; + std::fs::create_dir_all(&temp)?; + let timer = tracing_subscriber::fmt::time::ChronoLocal::new("%Y-%m-%d_%I:%M:%S%.6f %P".to_owned()); let logfile = RollingFileAppender::builder() .rotation(Rotation::DAILY) .filename_prefix("api") .filename_suffix("log") - .build(if DEV_MODE { - "/tmp/tfcsite" - } else { - "/storage/logs/tfcsite" - }) + .build(&logs) .expect("Shouldn't"); - let filter = EnvFilter::builder() + let file_filter = EnvFilter::builder() .with_default_directive(LevelFilter::WARN.into()) - .parse_lossy("tfcapi=debug"); + .parse_lossy("tfcsite=debug"); let logfile_layer = tracing_subscriber::fmt::layer() .with_writer(logfile) .with_line_number(true) .with_level(true) .with_target(true) .with_ansi(false) - .with_timer(timer.clone()); + .with_timer(timer.clone()) + .with_filter(file_filter); + + let stdout_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::WARN.into()) + .parse_lossy("tfcsite=debug"); let stdout_layer = tracing_subscriber::fmt::layer() .pretty() .with_line_number(true) .with_target(true) .with_timer(timer) - .with_filter(filter); - let filter = EnvFilter::builder() - .with_default_directive(LevelFilter::WARN.into()) - .parse_lossy("tfcapi=debug"); - let subscriber = tracing_subscriber::registry() - .with(logfile_layer.with_filter(filter).and_then(stdout_layer)); - let _ = tracing::subscriber::set_global_default(subscriber).wrap_err("Tracing broked"); + .with_filter(stdout_filter); - std::fs::create_dir_all("/tmp/tfcsite")?; + let subscriber = tracing_subscriber::registry().with(logfile_layer.and_then(stdout_layer)); + let _ = tracing::subscriber::set_global_default(subscriber).wrap_err("Tracing broked"); info!("starting HTTP server at http://localhost:4242"); @@ -96,11 +116,7 @@ async fn main() -> std::io::Result<()> { App::new() .app_data(data.clone()) .wrap(TracingLogger::::new()) - .app_data(TempFileConfig::default().directory(if DEV_MODE { - "/tmp/tfcsite" - } else { - "/storage/logs/tfcsite" - })) + .app_data(TempFileConfig::default().directory(&temp)) .service(mt_form) .service(health_form) .service(mt_parent_form) @@ -109,7 +125,7 @@ async fn main() -> std::io::Result<()> { .service(local_form) .service(camp_form) .service(contact_form) - .service(Files::new("/", "./public").index_file("index.html")) + .service(Files::new("/", &site).index_file("index.html")) }) .bind(("localhost", 4242))? .workers(4)