@@ -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 50f6329..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.
@@ -1790,8 +1791,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 +1811,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 +1850,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 +1888,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));
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/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)
}
}
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 {