diff --git a/src/application/routes/api/coffee/cafes.rs b/src/application/routes/api/coffee/cafes.rs index 1e07686..40e9ebc 100644 --- a/src/application/routes/api/coffee/cafes.rs +++ b/src/application/routes/api/coffee/cafes.rs @@ -192,6 +192,7 @@ pub(crate) async fn update_cafe( ) -> Result { let (submission, source) = payload.into_parts(); let (update, image_data_url) = submission.into_parts(); + let update = update.normalize(); validate_update(&update, image_data_url.as_ref())?; diff --git a/src/domain/coffee/cafes.rs b/src/domain/coffee/cafes.rs index bb23121..c3f9dde 100644 --- a/src/domain/coffee/cafes.rs +++ b/src/domain/coffee/cafes.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::domain::ids::CafeId; use crate::domain::listing::{SortDirection, SortKey}; +use crate::domain::roasters::is_valid_url_scheme; use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -62,7 +63,8 @@ impl NewCafe { self.name = self.name.trim().to_string(); self.city = self.city.trim().to_string(); self.country = self.country.trim().to_string(); - self.website = normalize_optional_field(self.website); + self.website = + normalize_optional_field(self.website).filter(|url| is_valid_url_scheme(url)); self } @@ -94,6 +96,23 @@ pub struct UpdateCafe { pub created_at: Option>, } +impl UpdateCafe { + pub fn normalize(mut self) -> Self { + self.website = self + .website + .and_then(|w| { + let trimmed = w.trim().to_string(); + if trimmed.is_empty() { + None + } else { + Some(trimmed) + } + }) + .filter(|url| is_valid_url_scheme(url)); + self + } +} + #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum CafeSortKey { CreatedAt, diff --git a/src/domain/coffee/roasters.rs b/src/domain/coffee/roasters.rs index 7a3f4ee..7bd81e6 100644 --- a/src/domain/coffee/roasters.rs +++ b/src/domain/coffee/roasters.rs @@ -58,7 +58,7 @@ fn normalize_optional_field(value: Option) -> Option { /// Returns `true` if the URL starts with `http://` or `https://`. /// Rejects `javascript:`, `data:`, and other potentially dangerous schemes. -fn is_valid_url_scheme(url: &str) -> bool { +pub(crate) fn is_valid_url_scheme(url: &str) -> bool { let lower = url.trim().to_lowercase(); lower.starts_with("http://") || lower.starts_with("https://") }