From b07c8e7669393385b2de0f64c980a1e1b388214c Mon Sep 17 00:00:00 2001 From: Jack Foltz Date: Wed, 11 Mar 2026 18:46:40 +0900 Subject: [PATCH 1/4] improved cf turnstile logging --- src/utils/cloudflare.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/utils/cloudflare.rs b/src/utils/cloudflare.rs index 4d23d38..8ceebc8 100644 --- a/src/utils/cloudflare.rs +++ b/src/utils/cloudflare.rs @@ -41,9 +41,9 @@ impl Cloudflare { #[derive(serde::Deserialize, serde::Serialize, Debug)] struct Response { success: bool, - hostname: String, - challenge_ts: DateTime, - #[serde(rename = "error-codes")] + hostname: Option, + challenge_ts: Option>, + #[serde(default, rename = "error-codes")] error_codes: Vec, } let res: Response = req.json().await?; @@ -54,10 +54,18 @@ impl Cloudflare { let challenge_ok = res.success; let domain_ok = match self.app_domain.as_str() { "localhost" => true, - domain => res.hostname == domain, + domain => res.hostname.is_some_and(|host| host == domain), }; - let age_ok = Utc::now().signed_duration_since(res.challenge_ts).num_minutes() < 5; + let age_ok = res + .challenge_ts + .is_some_and(|ts| Utc::now().signed_duration_since(ts).num_minutes() < 5); + let ok = challenge_ok && domain_ok && age_ok; - Ok(challenge_ok && domain_ok && age_ok) + tracing::info!( + "Turnstile token={token} client_ip={client_ip} -> ok={ok} challenge_ok={challenge_ok} domain_ok={domain_ok} age_ok={age_ok} errors={:?}", + res.error_codes + ); + + Ok(ok) } } From 9de5d09179cfd7e5b0967f6090200bc79d1575c8 Mon Sep 17 00:00:00 2001 From: Jack Foltz Date: Wed, 11 Mar 2026 19:43:11 +0900 Subject: [PATCH 2/4] events: simplify json parse in error enums --- src/app/events.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/events.rs b/src/app/events.rs index 50f6329..2bcd4fa 100644 --- a/src/app/events.rs +++ b/src/app/events.rs @@ -1790,8 +1790,8 @@ mod rsvp { #[derive(thiserror::Error, Debug)] pub enum ParseSelectionError { - #[error("failed to parse request JSON")] - Parse, + #[error("failed to parse request: {0}")] + Parse(#[from] serde_json::Error), #[error("unknown spot_id={spot_id}")] UnknownSpot { spot_id: i64 }, @@ -1810,7 +1810,7 @@ mod rsvp { contribution: Option, } - let rsvps: Vec = serde_json::from_str(selection).map_err(|_| Error::Parse)?; + let rsvps: Vec = serde_json::from_str(selection)?; let mut parsed = vec![]; for rsvp in rsvps { @@ -1849,8 +1849,8 @@ mod rsvp { } #[derive(thiserror::Error, Debug)] pub enum ParseAttendeesError { - #[error("failed to parse request JSON")] - Parse, + #[error("failed to parse request: {0}")] + Parse(#[from] serde_json::Error), #[error("unknown or duplicate rsvp_id={rsvp_id}")] UnknownOrDuplicateRsvp { rsvp_id: i64 }, @@ -1887,7 +1887,7 @@ mod rsvp { is_me: bool, } - let attendees: Vec = serde_json::from_str(attendees).map_err(|_| Error::Parse)?; + let attendees: Vec = serde_json::from_str(attendees)?; // Track available rsvp_ids, seen email/phones for duplicate detection let mut remaining_rsvps: HashSet = HashSet::from_iter(rsvps.iter().map(|r| r.rsvp_id)); From dbaec18c33695c9b548d2afde7753c104db670b2 Mon Sep 17 00:00:00 2001 From: Jack Foltz Date: Wed, 11 Mar 2026 19:45:57 +0900 Subject: [PATCH 3/4] errors: add into for bail! --- src/app/webhooks.rs | 2 +- src/utils/error.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/webhooks.rs b/src/app/webhooks.rs index e8c813b..b54db93 100644 --- a/src/app/webhooks.rs +++ b/src/app/webhooks.rs @@ -75,7 +75,7 @@ pub mod stripe { object: T, } let event: Type = serde_json::from_str(&body).map_err(|_| invalid())?; - fn parse(body: &str) -> AppResult { + fn parse(body: &str) -> Result { let event: Event = serde_json::from_str(body).map_err(|_| invalid())?; Ok(event.data.object) } diff --git a/src/utils/error.rs b/src/utils/error.rs index 7894f97..3f41ed5 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -57,10 +57,10 @@ impl From for AnyError { #[macro_export] macro_rules! bail { ( $fmt:expr ) => { - return Err(AnyError::new($fmt)); + return Err(AnyError::new($fmt).into()); }; ( $fmt:expr, $($arg:expr),* $(,)?) => { - return Err(AnyError::new(format!("{}", format_args!($fmt, $($arg),*)))); + return Err(AnyError::new(format!("{}", format_args!($fmt, $($arg),*))).into()); } } pub use bail; @@ -84,7 +84,6 @@ pub enum AppError { Unauthorized(Backtrace), Invalid(Backtrace), } -pub type AppResult = Result; impl AppError { pub fn message(&self) -> &'static str { From d53f495b4e0a42289636f06f53e2c2113135acc0 Mon Sep 17 00:00:00 2001 From: Jack Foltz Date: Wed, 11 Mar 2026 19:51:52 +0900 Subject: [PATCH 4/4] events: fix slug validation --- frontend/templates/events/edit.html | 27 +++++++++++----------- src/app/events.rs | 35 +++++++++++++++-------------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/frontend/templates/events/edit.html b/frontend/templates/events/edit.html index 7350790..6006f63 100644 --- a/frontend/templates/events/edit.html +++ b/frontend/templates/events/edit.html @@ -10,9 +10,6 @@

Edit Event

@@ -351,12 +348,6 @@

Available Spots

return; } - const slug = $("slug").value; - if (!slug || !/^[a-zA-Z0-9\-]+$/.test(slug)) { - alert("Slug can only contain letters, numbers, and dashes."); - return; - } - // Retrieve the value of an input, with adjustments for HTML input type weirdness let value = (name, root = ui.form) => { const el = root.querySelector(`[name="${name}"]`); @@ -399,10 +390,16 @@

Available Spots

return [start, end]; })(); + const slug = value("slug"); + if (!slug || !/^[a-zA-Z0-9\-]+$/.test(slug)) { + alert("Slug can only contain letters, numbers, and dashes."); + return; + } + const body = { - id: parseInt("{{ event.id }}") || undefined, + id: parseInt(`{{ event.id }}`), title: value("title"), - slug: value("slug"), + slug, start, end, capacity: value("capacity"), @@ -436,7 +433,6 @@

Available Spots

}), }; - const slug = value("slug"); try { const formData = new FormData(); formData.append("data", JSON.stringify(body)); @@ -452,7 +448,12 @@

Available Spots

}); if (resp.ok) { - window.location.href = `/e/${slug}`; + const payload = await resp.json(); + if (payload?.error) { + alert(`Error: ${payload.error}`); + } else { + window.location.href = `/e/${slug}`; + } } else { alert(`Error: ${await resp.text()}`); } diff --git a/src/app/events.rs b/src/app/events.rs index 2bcd4fa..8a167f0 100644 --- a/src/app/events.rs +++ b/src/app/events.rs @@ -213,14 +213,14 @@ mod edit { // Handle edit submission. #[derive(Debug, serde::Deserialize)] pub struct EditForm { - id: Option, + id: i64, #[serde(flatten)] event: UpdateEvent, spots: Vec, } pub async fn edit_form( State(state): State, mut multipart: axum::extract::Multipart, - ) -> HtmlResult { + ) -> JsonResult<()> { let mut form: Option = None; let mut flyer: Option = None; @@ -245,12 +245,24 @@ mod edit { if form.event.slug.is_empty() || !form.event.slug.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') { - return Ok((StatusCode::BAD_REQUEST, "Slug can only contain letters, numbers, and dashes.") - .into_response()); + bail!("Slug can only contain letters, numbers, and dashes."); } match form.id { - Some(id) => { + 0 => { + tracing::info!("create: {:?}", &form.event); + let event_id = Event::create(&state.db, &form.event, &flyer).await?; + + let mut spot_ids = vec![]; + for spot in form.spots { + let id = Spot::create(&state.db, &spot).await?; + spot_ids.push(id); + } + + Spot::add_to_event(&state.db, event_id, spot_ids).await?; + } + id => { + tracing::info!("edit: {:?}", &form.event); Event::update(&state.db, id, &form.event, &flyer).await?; let rsvp_counts = Spot::rsvp_counts_for_event(&state.db, id).await?; @@ -277,20 +289,9 @@ mod edit { Spot::add_to_event(&state.db, id, to_add).await?; Spot::remove_from_event(&state.db, id, to_delete).await?; } - None => { - let event_id = Event::create(&state.db, &form.event, &flyer).await?; - - let mut spot_ids = vec![]; - for spot in form.spots { - let id = Spot::create(&state.db, &spot).await?; - spot_ids.push(id); - } - - Spot::add_to_event(&state.db, event_id, spot_ids).await?; - } } - Ok(Redirect::to("/events").into_response()) + Ok(Json(())) } // Edit invite page.