mirror of
https://git.alemi.dev/guestbook.rs.git
synced 2024-12-19 02:54:52 +01:00
fix: properly catch empty contact, small refactor
This commit is contained in:
parent
c959b4e18a
commit
0ebf01ceb1
3 changed files with 44 additions and 17 deletions
14
src/model.rs
14
src/model.rs
|
@ -3,18 +3,22 @@ use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct GuestBookPage {
|
pub struct GuestBookPage {
|
||||||
pub author: Option<String>,
|
pub author: String,
|
||||||
pub contact: Option<String>,
|
|
||||||
pub body: String,
|
pub body: String,
|
||||||
pub date: DateTime<Utc>,
|
pub date: DateTime<Utc>,
|
||||||
pub avatar: String,
|
pub avatar: String,
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
|
pub contact: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct Insertion {
|
pub struct Insertion {
|
||||||
|
#[serde(deserialize_with = "non_empty_str")]
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
|
|
||||||
|
#[serde(deserialize_with = "non_empty_str")]
|
||||||
pub contact: Option<String>,
|
pub contact: Option<String>,
|
||||||
|
|
||||||
pub body: String,
|
pub body: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,3 +44,9 @@ pub struct PageOptions {
|
||||||
pub limit: Option<usize>,
|
pub limit: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fn non_empty_str<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
|
||||||
|
Ok(Option::deserialize(d)?.filter(|s: &String| !s.is_empty()))
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,6 @@ pub struct ConsolePrettyNotifier {}
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl NotificationProcessor<GuestBookPage> for ConsolePrettyNotifier {
|
impl NotificationProcessor<GuestBookPage> for ConsolePrettyNotifier {
|
||||||
async fn process(&self, suggestion: &GuestBookPage) {
|
async fn process(&self, suggestion: &GuestBookPage) {
|
||||||
println!("{} -- {} <{}>", suggestion.body, suggestion.author.as_deref().unwrap_or("anon"), suggestion.contact.as_deref().unwrap_or(""));
|
println!("{} -- {} <{}>", suggestion.body, suggestion.author, suggestion.contact.as_deref().unwrap_or(""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,28 +41,25 @@ impl Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_suggestion(unsafe_payload: Insertion, state: SafeContext) -> Result<Redirect, String> {
|
async fn send_suggestion(unsafe_payload: Insertion, state: SafeContext) -> Result<Redirect, String> {
|
||||||
|
// sanitize all user input! we don't want XSS or html injections!
|
||||||
let payload = unsafe_payload.sanitize();
|
let payload = unsafe_payload.sanitize();
|
||||||
|
// limit author and contact fields to 25 and 50 characters, TODO don't hardcode limits
|
||||||
|
let contact_limited = payload.contact.clone().map(|x| limit_string(&x, 50));
|
||||||
|
let author_limited = payload.author.map(|x| limit_string(&x, 25));
|
||||||
|
// calculate contact hash for libravatar
|
||||||
let mut hasher = Md5::new();
|
let mut hasher = Md5::new();
|
||||||
let id = payload.contact.clone().unwrap_or(Uuid::new_v4().to_string());
|
hasher.update(contact_limited.as_deref().unwrap_or(&Uuid::new_v4().to_string()).as_bytes());
|
||||||
hasher.update(id.as_bytes());
|
|
||||||
let avatar = hasher.finalize();
|
let avatar = hasher.finalize();
|
||||||
|
// populate guestbook page struct
|
||||||
let page = GuestBookPage {
|
let page = GuestBookPage {
|
||||||
avatar: format!("{:x}", avatar),
|
avatar: format!("{:x}", avatar),
|
||||||
author: payload.author.map(|x| x.chars().take(25).collect()), // TODO don't hardcode char limits!
|
author: author_limited.unwrap_or("anonymous".to_string()),
|
||||||
contact: payload.contact.clone().map(|x| x.chars().take(50).collect()),
|
url: url_from_contact(contact_limited.clone()),
|
||||||
|
contact: contact_limited,
|
||||||
body: payload.body,
|
body: payload.body,
|
||||||
date: Utc::now(),
|
date: Utc::now(),
|
||||||
url: match payload.contact {
|
|
||||||
None => None,
|
|
||||||
Some(c) => if c.starts_with("http") {
|
|
||||||
Some(c)
|
|
||||||
} else if c.contains('@') {
|
|
||||||
Some(format!("mailto:{}", c))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
// lock state, process and archive new page
|
||||||
let mut lock = state.write().await;
|
let mut lock = state.write().await;
|
||||||
lock.process(&page).await;
|
lock.process(&page).await;
|
||||||
match lock.storage.archive(page).await {
|
match lock.storage.archive(page).await {
|
||||||
|
@ -84,3 +81,23 @@ async fn get_suggestion(State(state): State<SafeContext>, Query(page): Query<Pag
|
||||||
Err(e) => Err(e.to_string()),
|
Err(e) => Err(e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn url_from_contact(contact: Option<String>) -> Option<String> {
|
||||||
|
match contact {
|
||||||
|
None => None,
|
||||||
|
Some(c) => if c.starts_with("http") {
|
||||||
|
Some(c)
|
||||||
|
} else if c.contains('@') {
|
||||||
|
Some(format!("mailto:{}", c))
|
||||||
|
} else if c.contains('.') {
|
||||||
|
Some(format!("https://{}", c))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn limit_string(s: &str, l: usize) -> String {
|
||||||
|
// TODO is there a better way? slicing doesn't work when l > s.len
|
||||||
|
s.chars().take(l).collect()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue