fix: validate cafe URL scheme on create and update

Make is_valid_url_scheme pub(crate) and use it to filter cafe website
URLs on both create and update, matching roaster validation behavior.
Add UpdateCafe::normalize() method for consistent sanitization.
This commit is contained in:
Jon Seager 2026-02-13 13:05:29 +00:00
parent b5d5949adf
commit ff2c602139
No known key found for this signature in database
3 changed files with 22 additions and 2 deletions

View file

@ -192,6 +192,7 @@ pub(crate) async fn update_cafe(
) -> Result<Response, ApiError> { ) -> Result<Response, ApiError> {
let (submission, source) = payload.into_parts(); let (submission, source) = payload.into_parts();
let (update, image_data_url) = submission.into_parts(); let (update, image_data_url) = submission.into_parts();
let update = update.normalize();
validate_update(&update, image_data_url.as_ref())?; validate_update(&update, image_data_url.as_ref())?;

View file

@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
use crate::domain::ids::CafeId; use crate::domain::ids::CafeId;
use crate::domain::listing::{SortDirection, SortKey}; use crate::domain::listing::{SortDirection, SortKey};
use crate::domain::roasters::is_valid_url_scheme;
use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail}; use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -62,7 +63,8 @@ impl NewCafe {
self.name = self.name.trim().to_string(); self.name = self.name.trim().to_string();
self.city = self.city.trim().to_string(); self.city = self.city.trim().to_string();
self.country = self.country.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 self
} }
@ -94,6 +96,23 @@ pub struct UpdateCafe {
pub created_at: Option<DateTime<Utc>>, pub created_at: Option<DateTime<Utc>>,
} }
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)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum CafeSortKey { pub enum CafeSortKey {
CreatedAt, CreatedAt,

View file

@ -58,7 +58,7 @@ fn normalize_optional_field(value: Option<String>) -> Option<String> {
/// Returns `true` if the URL starts with `http://` or `https://`. /// Returns `true` if the URL starts with `http://` or `https://`.
/// Rejects `javascript:`, `data:`, and other potentially dangerous schemes. /// 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(); let lower = url.trim().to_lowercase();
lower.starts_with("http://") || lower.starts_with("https://") lower.starts_with("http://") || lower.starts_with("https://")
} }