fixing up the api to work better for nix and move the table format

This commit is contained in:
Chris Cochrun 2025-06-23 15:13:42 -05:00
parent 774328aa5b
commit ee252aec13
7 changed files with 305 additions and 96 deletions

117
Cargo.lock generated
View file

@ -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"

View file

@ -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

View file

@ -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;
};

View file

@ -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

View file

@ -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<String>,
}
impl From<&CampForm> for HashMap<i32, String> {
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<CampForm>) -> HttpResp
}
}
async fn store_camp_form(map: HashMap<i32, String>) -> Result<()> {
async fn store_camp_form(map: HashMap<&str, String>) -> Result<Response> {
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}"),
}
}

View file

@ -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<String>,
}
impl From<&HealthForm> for HashMap<i32, String> {
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<HealthForm>) ->
// HttpResponse::Ok().body("hi")
}
async fn store_form(map: HashMap<i32, String>) -> Result<()> {
async fn store_form(map: HashMap<&str, String>) -> Result<Response> {
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<i32, String>) -> 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}"),
}
}
}

View file

@ -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<B: MessageBody>(_span: Span, _outcome: &Result<ServiceResponse<B>, 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::<DomainRootSpanBuilder>::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)