From dd6b8c76bad4350b9e810f4144270aef77d99642 Mon Sep 17 00:00:00 2001 From: tuxmain Date: Sat, 22 Oct 2022 17:56:24 +0200 Subject: [PATCH] Admin edit comment --- src/db.rs | 1 + src/helpers.rs | 43 ++++++++++-- src/queries.rs | 46 ++++++++----- src/server.rs | 145 ++++++++++++++++++++++++++++------------ src/templates.rs | 1 + templates/comments.html | 44 +++++++++--- 6 files changed, 204 insertions(+), 76 deletions(-) diff --git a/src/db.rs b/src/db.rs index 90144ea..b6b67af 100644 --- a/src/db.rs +++ b/src/db.rs @@ -82,6 +82,7 @@ impl CommentId { } pub fn from_base64(s: &str) -> Result { + // TODO prevent panic when s is too long let mut buf = [0; 16]; base64::decode_config_slice(s, base64::URL_SAFE_NO_PAD, &mut buf).map(|_| Self(buf)) } diff --git a/src/helpers.rs b/src/helpers.rs index 4a537e3..81e9c8f 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,4 +1,4 @@ -use crate::{config::Config, db::*}; +use crate::{config::Config, db::*, queries::*}; use log::error; use std::{net::IpAddr, str::FromStr}; @@ -30,13 +30,18 @@ pub fn approve_comment(comment_id: CommentId, dbs: &Dbs) -> Result<(), sled::Err Ok(()) } -pub fn remove_pending_comment( - comment_id: CommentId, - dbs: &Dbs, -) -> Result, sled::Error> { +pub fn remove_comment(comment_id: CommentId, dbs: &Dbs) -> Result, sled::Error> { if let Some(comment) = dbs.comment.remove(&comment_id)? { - dbs.comment_pending - .remove(&(comment.topic_hash.clone(), comment.post_time, comment_id))?; + dbs.comment_pending.remove(&( + comment.topic_hash.clone(), + comment.post_time, + comment_id.clone(), + ))?; + dbs.comment_approved.remove(&( + comment.topic_hash.clone(), + comment.post_time, + comment_id, + ))?; return Ok(Some(comment)); } Ok(None) @@ -163,6 +168,30 @@ pub fn get_client_addr( )) } +pub fn check_comment(config: &Config, comment: &CommentForm, errors: &mut Vec) { + if comment.author.len() > config.comment_author_max_len { + errors.push(format!( + "Author name length is {} but maximum is {}.", + comment.author.len(), + config.comment_author_max_len + )); + } + if comment.email.len() > config.comment_email_max_len { + errors.push(format!( + "E-mail length is {} but maximum is {}.", + comment.email.len(), + config.comment_email_max_len + )); + } + if comment.text.len() > config.comment_text_max_len { + errors.push(format!( + "Comment length is {} but maximum is {}.", + comment.text.len(), + config.comment_text_max_len + )); + } +} + #[cfg(test)] mod test { use super::*; diff --git a/src/queries.rs b/src/queries.rs index 21d0d2a..f59a70e 100644 --- a/src/queries.rs +++ b/src/queries.rs @@ -1,4 +1,4 @@ -use serde::{Deserialize, Serialize}; +use serde::Deserialize; #[derive(Clone, Debug, Deserialize)] pub struct AdminLoginQuery { @@ -8,37 +8,35 @@ pub struct AdminLoginQuery { #[derive(Clone, Debug, Deserialize)] pub struct AdminEditCommentQuery { pub author: String, - pub comment_id: String, + pub id: String, pub email: String, pub text: String, } #[derive(Clone, Debug, Deserialize)] pub struct AdminRmCommentQuery { - pub comment_id: String, + pub id: String, } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize)] pub struct NewCommentQuery { - pub author: String, - pub email: String, - pub text: String, + #[serde(flatten)] + pub comment: CommentForm, } #[derive(Clone, Debug, Deserialize)] pub struct EditCommentQuery { - pub author: String, - pub comment_id: String, - pub email: String, - pub text: String, - pub token: String, + #[serde(flatten)] + pub comment: CommentForm, + pub id: String, + //pub token: String, } -#[derive(Clone, Debug, Deserialize)] +/*#[derive(Clone, Debug, Deserialize)] pub struct RmCommentQuery { - pub comment_id: String, - pub token: String, -} + pub id: String, + //pub token: String, +}*/ #[derive(Clone, Debug, Deserialize)] #[serde(tag = "a")] @@ -47,8 +45,8 @@ pub enum CommentQuery { NewComment(NewCommentQuery), #[serde(rename = "edit_comment")] EditComment(EditCommentQuery), - #[serde(rename = "rm_comment")] - RmComment(RmCommentQuery), + /*#[serde(rename = "rm_comment")] + RmComment(RmCommentQuery),*/ } #[derive(Clone, Debug, Deserialize)] @@ -71,3 +69,15 @@ pub struct ApproveQuery { pub struct RemoveQuery { pub remove: String, } + +#[derive(Clone, Debug, Deserialize)] +pub struct EditQuery { + pub edit: String, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct CommentForm { + pub author: String, + pub email: String, + pub text: String, +} diff --git a/src/server.rs b/src/server.rs index 189bb62..43b9232 100644 --- a/src/server.rs +++ b/src/server.rs @@ -23,8 +23,8 @@ pub async fn run_server(config: Arc, dbs: Dbs, templates: Arc config.clone(), templates.clone(), dbs.clone(), - &[], Context::new(), + 200, ) } }); @@ -63,8 +63,8 @@ async fn serve_comments<'a>( config: Arc, templates: Arc, dbs: Dbs, - errors: &[String], mut context: Context, + status_code: u16, ) -> tide::Result { let Ok(topic) = req.param("topic") else { return Err(tide::Error::from_str(404, "No topic")) @@ -76,10 +76,8 @@ async fn serve_comments<'a>( let topic_hash = TopicHash::from_topic(topic); - //let mut context = Context::new(); context.insert("config", &config); context.insert("admin", &admin); - context.insert("new_comment_errors", errors); if admin { if let Ok(query) = req.query::() { @@ -91,17 +89,28 @@ async fn serve_comments<'a>( } if let Ok(query) = req.query::() { if let Ok(comment_id) = CommentId::from_base64(&query.remove) { - helpers::remove_pending_comment(comment_id, &dbs) + helpers::remove_comment(comment_id, &dbs) .map_err(|e| error!("Removing comment: {:?}", e)) .ok(); } } + if let Ok(query) = req.query::() { + if let Ok(comment_id) = CommentId::from_base64(&query.edit) { + if let Some(comment) = dbs.comment.get(&comment_id).unwrap() { + context.insert("edit_comment", &comment_id.to_base64()); + context.insert("edit_comment_author", &comment.author); + context.insert("edit_comment_email", &comment.email); + context.insert("edit_comment_text", &comment.text); + } + } + } context.insert( "comments_pending", &helpers::iter_pending_comments_by_topic(topic_hash.clone(), &dbs) .map(|(comment_id, comment)| CommentWithId { author: comment.author, + editable: admin, id: comment_id.to_base64(), needs_approval: true, post_time: comment.post_time, @@ -116,6 +125,7 @@ async fn serve_comments<'a>( &helpers::iter_approved_comments_by_topic(topic_hash, &dbs) .map(|(comment_id, comment)| CommentWithId { author: comment.author, + editable: admin, id: comment_id.to_base64(), needs_approval: false, post_time: comment.post_time, @@ -124,12 +134,10 @@ async fn serve_comments<'a>( .collect::>(), ); - Ok( - tide::Response::builder(if errors.is_empty() { 200 } else { 400 }) - .content_type(tide::http::mime::HTML) - .body(templates.tera.render("comments.html", &context)?) - .build(), - ) + Ok(tide::Response::builder(status_code) + .content_type(tide::http::mime::HTML) + .body(templates.tera.render("comments.html", &context)?) + .build()) } async fn serve_admin<'a>( @@ -161,6 +169,7 @@ async fn serve_admin<'a>( })?; Some(CommentWithId { author: comment.author, + editable: true, id: comment_id.to_base64(), needs_approval: true, post_time: comment.post_time, @@ -197,7 +206,11 @@ async fn handle_post_comments( dbs: Dbs, notify_send: Sender<()>, ) -> tide::Result { - let client_addr = if config.antispam_enable { + let admin = req.cookie("admin").map_or(false, |psw| { + check_admin_password_hash(&config, &String::from(psw.value())) + }); + + let client_addr = if !admin && config.antispam_enable { match helpers::get_client_addr(&config, &req) { Some(Ok(addr)) => { if config.antispam_whitelist.contains(&addr) { @@ -228,27 +241,8 @@ async fn handle_post_comments( return Err(tide::Error::from_str(404, "No topic")) }; - if query.author.len() > config.comment_author_max_len { - errors.push(format!( - "Author name length is {} but maximum is {}.", - query.author.len(), - config.comment_author_max_len - )); - } - if query.email.len() > config.comment_email_max_len { - errors.push(format!( - "E-mail length is {} but maximum is {}.", - query.email.len(), - config.comment_email_max_len - )); - } - if query.text.len() > config.comment_text_max_len { - errors.push(format!( - "Comment length is {} but maximum is {}.", - query.text.len(), - config.comment_text_max_len - )); - } + helpers::check_comment(&config, &query.comment, &mut errors); + if let Some(client_addr) = &client_addr { if let Some(antispam_timeout) = helpers::antispam_check_client_mutation(client_addr, &dbs, &config).unwrap() @@ -274,29 +268,95 @@ async fn handle_post_comments( let comment = Comment { topic_hash, - author: query.author, - email: if query.email.is_empty() { + author: query.comment.author, + email: if query.comment.email.is_empty() { None } else { - Some(query.email) + Some(query.comment.email) }, last_edit_time: None, post_time: time, - text: query.text, + text: query.comment.text, }; helpers::new_pending_comment(&comment, &dbs) .map_err(|e| error!("Adding pending comment: {:?}", e)) .ok(); notify_send.send(()).ok(); } else { - context.insert("new_comment_author", &query.author); - context.insert("new_comment_email", &query.email); - context.insert("new_comment_text", &query.text); + context.insert("new_comment_author", &query.comment.author); + context.insert("new_comment_email", &query.comment.email); + context.insert("new_comment_text", &query.comment.text); } + context.insert("new_comment_errors", &errors); + } + CommentQuery::EditComment(query) => { + if !admin { + return Err(tide::Error::from_str(403, "Forbidden")); + } + + helpers::check_comment(&config, &query.comment, &mut errors); + + let comment_id = if let Ok(comment_id) = CommentId::from_base64(&query.id) { + comment_id + } else { + return Err(tide::Error::from_str(400, "Invalid comment id")); + }; + + let mut comment = if let Some(comment) = dbs.comment.get(&comment_id).unwrap() { + comment + } else { + return Err(tide::Error::from_str(404, "Not found")); + }; + + if let Some(client_addr) = &client_addr { + if let Some(antispam_timeout) = + helpers::antispam_check_client_mutation(client_addr, &dbs, &config).unwrap() + { + errors.push(format!( + "The edition quota from your IP is reached. You will be unblocked in {}s.", + antispam_timeout + )); + } + } + + if errors.is_empty() { + if let Some(client_addr) = &client_addr { + helpers::antispam_update_client_mutation(client_addr, &dbs).unwrap(); + } + + let time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + comment.author = query.comment.author; + comment.email = if query.comment.email.is_empty() { + None + } else { + Some(query.comment.email) + }; + comment.text = query.comment.text; + comment.last_edit_time = Some(time); + + dbs.comment.insert(&comment_id, &comment).unwrap(); + } else { + context.insert("edit_comment", &comment_id.to_base64()); + context.insert("edit_comment_author", &query.comment.author); + context.insert("edit_comment_email", &query.comment.email); + context.insert("edit_comment_text", &query.comment.text); + } + context.insert("edit_comment_errors", &errors); } - _ => {} } - serve_comments(req, config, templates, dbs, &errors, context).await + serve_comments( + req, + config, + templates, + dbs, + context, + if errors.is_empty() { 200 } else { 400 }, + ) + .await } async fn handle_post_admin( @@ -307,6 +367,7 @@ async fn handle_post_admin( ) -> tide::Result { if let Some(psw) = req.cookie("admin") { if check_admin_password(&config, &String::from(psw.value())).is_some() { + #[allow(clippy::match_single_binding)] match req.body_form::().await? { _ => serve_admin(req, config, templates, dbs).await, } diff --git a/src/templates.rs b/src/templates.rs index d78a64f..bb9e827 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -38,6 +38,7 @@ impl Templates { #[derive(Clone, Debug, Serialize)] pub struct CommentWithId { pub author: String, + pub editable: bool, pub id: String, pub needs_approval: bool, pub post_time: Time, diff --git a/templates/comments.html b/templates/comments.html index d9757a1..70a720b 100644 --- a/templates/comments.html +++ b/templates/comments.html @@ -8,14 +8,17 @@ {% if comments_pending %}
{% for comment in comments_pending %} -
+
{{ comment.author }} {{ comment.post_time | date(format="%F %R", locale=config.lang) }} + {% if comment.editable %} + Edit + {% endif %} {% if admin and comment.needs_approval %} - Approve + Approve {% endif %} {% if admin %} - Remove + Remove {% endif %}

{{ comment.text }}

@@ -24,14 +27,17 @@ {% endif %}
{% for comment in comments %} -
+
{{ comment.author }} {{ comment.post_time | date(format="%F %R", locale=config.lang) }} + {% if comment.editable %} + Edit + {% endif %} {% if admin and comment.needs_approval %} - Approve + Approve {% endif %} {% if admin %} - Remove + Remove {% endif %}

{{ comment.text }}

@@ -47,12 +53,32 @@ {% endif %} -
+
-
+

-
+
+ {% if edit_comment %} +
+ {% if edit_comment_errors %} +

Whoops, the following error occurred:

+
    + {% for error in edit_comment_errors %} +
  • {{ error | safe }}
  • + {% endfor %} +
+ {% endif %} + + +
+ +
+
+
+ +
+ {% endif %}