started implementing API
This commit is contained in:
parent
c183747729
commit
8629bba07f
14 changed files with 928 additions and 719 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3370,6 +3370,7 @@ dependencies = [
|
|||
"rand_core 0.6.4",
|
||||
"rpassword",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.6",
|
||||
"sled",
|
||||
"tera",
|
||||
|
|
|
@ -24,6 +24,7 @@ rand = "0.8.5"
|
|||
rand_core = { version = "0.6.4", features = ["std"] }
|
||||
rpassword = "7.2.0"
|
||||
serde = { version = "1.0.152", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.91"
|
||||
sha2 = "0.10.6"
|
||||
sled = "0.34.7"
|
||||
tera = { version = "1.17.1", features = ["builtins", "date-locale"] }
|
||||
|
|
12
README.md
12
README.md
|
@ -47,9 +47,19 @@ Uses no cookie, no unique user identifier. At each mutation (i.e. new comment or
|
|||
|
||||
However, keep in mind that if a reverse proxy (or any other intermediate tool) is used, IP addresses and other metadata may be logged somewhere.
|
||||
|
||||
## API
|
||||
|
||||
/api/post_comment
|
||||
/api/comments_by_topic
|
||||
/api/edit_comment
|
||||
/api/remove_comment
|
||||
/api/get_comment
|
||||
/api/admin/approve_comment
|
||||
/api/admin/remove_comment
|
||||
|
||||
## License
|
||||
|
||||
CopyLeft 2022 Pascal Engélibert [(why copyleft?)](https://txmn.tk/blog/why-copyleft/)
|
||||
CopyLeft 2022-2023 Pascal Engélibert [(why copyleft?)](https://txmn.tk/blog/why-copyleft/)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License.
|
||||
|
||||
|
|
13
client/basic.html
Normal file
13
client/basic.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Webcomment</title>
|
||||
<script type="text/javascript" src="js/jquery.js"></script>
|
||||
<script type="text/javascript" src="js/webcomment.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="comments"></div>
|
||||
<script type="text/javascript">webcomment_topic("comments", "http://127.0.0.1:31720", "test");</script>
|
||||
</body>
|
||||
</html>
|
2
client/js/jquery.js
vendored
Normal file
2
client/js/jquery.js
vendored
Normal file
File diff suppressed because one or more lines are too long
50
client/js/webcomment.js
Normal file
50
client/js/webcomment.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
CopyLeft 2022-2023 Pascal Engélibert (why copyleft? -> https://txmn.tk/blog/why-copyleft/)
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License.
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
|
||||
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
var webcomments = {};
|
||||
|
||||
const MODE_TOPIC = 1;// param: {topic:str}
|
||||
|
||||
class Webcomment {
|
||||
constructor(root, api, mode, mode_param) {
|
||||
this.root = root;
|
||||
this.api = api;
|
||||
this.mode = mode;
|
||||
this.mode_param = mode_param;
|
||||
|
||||
console.log("constr");
|
||||
|
||||
switch(mode) {
|
||||
case MODE_TOPIC:
|
||||
this.query_comments_by_topic(mode_param.topic);
|
||||
break;
|
||||
default:
|
||||
console.log("Webcomment: invalid mode");
|
||||
}
|
||||
}
|
||||
|
||||
query_comments_by_topic(topic) {
|
||||
console.log("query");
|
||||
$.ajax({
|
||||
method: "POST",
|
||||
url: this.api+"/api/comments_by_topic",
|
||||
data: JSON.stringify({
|
||||
mutation_token: "",
|
||||
topic: topic,
|
||||
}),
|
||||
success: function(resp) {
|
||||
console.log(resp);
|
||||
},
|
||||
dataType: "json",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function webcomment_topic(root_id, api, topic) {
|
||||
webcomments[root_id] = (new Webcomment(document.getElementById(root_id), api, MODE_TOPIC, {topic: topic}));
|
||||
}
|
|
@ -36,6 +36,8 @@ pub struct Config {
|
|||
pub cookies_https_only: bool,
|
||||
#[serde(default = "Config::default_cookies_domain")]
|
||||
pub cookies_domain: Option<String>,
|
||||
#[serde(default = "Config::default_cors_allow_origin")]
|
||||
pub cors_allow_origin: String,
|
||||
/// Format: "language_REGION"
|
||||
#[serde(default = "Config::default_default_lang")]
|
||||
pub default_lang: String,
|
||||
|
@ -110,6 +112,9 @@ impl Config {
|
|||
fn default_cookies_domain() -> Option<String> {
|
||||
None
|
||||
}
|
||||
fn default_cors_allow_origin() -> String {
|
||||
"*".into()
|
||||
}
|
||||
fn default_default_lang() -> String {
|
||||
"en_US".into()
|
||||
}
|
||||
|
@ -163,6 +168,7 @@ impl Default for Config {
|
|||
comment_text_max_len: Self::default_comment_text_max_len(),
|
||||
cookies_https_only: Self::default_cookies_https_only(),
|
||||
cookies_domain: Self::default_cookies_domain(),
|
||||
cors_allow_origin: Self::default_cors_allow_origin(),
|
||||
default_lang: Self::default_default_lang(),
|
||||
listen: Self::default_listen(),
|
||||
matrix_notify: Self::default_matrix_notify(),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{config::Config, db::*, locales::Locales, queries::*};
|
||||
use crate::{config::Config, db::*, locales::Locales};
|
||||
|
||||
use fluent_bundle::FluentArgs;
|
||||
use log::error;
|
||||
|
@ -313,7 +313,7 @@ pub fn check_comment(
|
|||
config: &Config,
|
||||
locales: &Locales,
|
||||
langs: &[LanguageIdentifier],
|
||||
comment: &CommentForm,
|
||||
comment: &crate::server::page::queries::CommentForm,
|
||||
errors: &mut Vec<String>,
|
||||
) {
|
||||
if comment.author.len() > config.comment_author_max_len {
|
||||
|
|
|
@ -5,9 +5,7 @@ mod db;
|
|||
mod helpers;
|
||||
mod locales;
|
||||
mod notify;
|
||||
mod queries;
|
||||
mod server;
|
||||
mod templates;
|
||||
|
||||
use argon2::{
|
||||
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
|
||||
|
@ -85,11 +83,11 @@ async fn main() {
|
|||
fn init_all(
|
||||
opt: cli::MainCommonOpt,
|
||||
subopt: cli::StartOpt,
|
||||
) -> (config::Config, db::Dbs, templates::Templates) {
|
||||
) -> (config::Config, db::Dbs, server::page::templates::Templates) {
|
||||
std::fs::create_dir_all(&opt.dir.0).expect("Cannot create dir");
|
||||
let config = config::read_config(&opt.dir.0);
|
||||
let dbs = db::load_dbs((!subopt.tmp).then_some(&opt.dir.0));
|
||||
let templates = templates::Templates::new(&opt.dir.0, &config);
|
||||
let templates = server::page::templates::Templates::new(&opt.dir.0, &config);
|
||||
|
||||
(config, dbs, templates)
|
||||
}
|
||||
|
|
727
src/server.rs
727
src/server.rs
|
@ -1,20 +1,16 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use crate::{
|
||||
config::*, db::*, helpers, locales::*, notify::Notification, queries::*, templates::*,
|
||||
};
|
||||
pub mod api;
|
||||
pub mod page;
|
||||
|
||||
use crate::{config::*, db::*, locales::*};
|
||||
|
||||
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
||||
use crossbeam_channel::Sender;
|
||||
use fluent_bundle::FluentArgs;
|
||||
use log::{error, warn};
|
||||
use tera::Context;
|
||||
use unic_langid::LanguageIdentifier;
|
||||
|
||||
pub async fn run_server(
|
||||
config: &'static Config,
|
||||
dbs: Dbs,
|
||||
templates: &'static Templates,
|
||||
templates: &'static page::templates::Templates,
|
||||
locales: &'static Locales,
|
||||
) {
|
||||
tide::log::start();
|
||||
|
@ -23,711 +19,22 @@ pub async fn run_server(
|
|||
tokio::spawn(crate::notify::run_notifier(config, notify_recv));
|
||||
|
||||
let mut app = tide::new();
|
||||
app.at(&format!("{}t/:topic", config.root_url)).get({
|
||||
let dbs = dbs.clone();
|
||||
move |req: tide::Request<()>| {
|
||||
let client_langs = get_client_langs(&req);
|
||||
serve_comments(
|
||||
req,
|
||||
config,
|
||||
templates,
|
||||
dbs.clone(),
|
||||
client_langs,
|
||||
Context::new(),
|
||||
200,
|
||||
)
|
||||
}
|
||||
});
|
||||
app.at(&format!("{}t/:topic", config.root_url)).post({
|
||||
let dbs = dbs.clone();
|
||||
let notify_send = notify_send.clone();
|
||||
move |req: tide::Request<()>| {
|
||||
handle_post_comments(
|
||||
req,
|
||||
config,
|
||||
templates,
|
||||
dbs.clone(),
|
||||
locales,
|
||||
notify_send.clone(),
|
||||
)
|
||||
}
|
||||
});
|
||||
app.at(&format!(
|
||||
"{}t/:topic/edit/:comment_id/:mutation_token",
|
||||
config.root_url
|
||||
))
|
||||
.get({
|
||||
let dbs = dbs.clone();
|
||||
move |req: tide::Request<()>| {
|
||||
let client_langs = get_client_langs(&req);
|
||||
serve_edit_comment(
|
||||
req,
|
||||
config,
|
||||
templates,
|
||||
dbs.clone(),
|
||||
client_langs,
|
||||
Context::new(),
|
||||
200,
|
||||
)
|
||||
}
|
||||
});
|
||||
app.at(&format!(
|
||||
"{}t/:topic/edit/:comment_id/:mutation_token",
|
||||
config.root_url
|
||||
))
|
||||
.post({
|
||||
let dbs = dbs.clone();
|
||||
move |req: tide::Request<()>| {
|
||||
handle_post_comments(
|
||||
req,
|
||||
config,
|
||||
templates,
|
||||
dbs.clone(),
|
||||
locales,
|
||||
notify_send.clone(),
|
||||
)
|
||||
}
|
||||
});
|
||||
app.at(&format!("{}admin", config.root_url))
|
||||
.get(move |req: tide::Request<()>| {
|
||||
let client_langs = get_client_langs(&req);
|
||||
serve_admin_login(req, config, templates, client_langs)
|
||||
});
|
||||
app.at(&format!("{}admin", config.root_url)).post({
|
||||
let dbs = dbs.clone();
|
||||
move |req: tide::Request<()>| handle_post_admin(req, config, templates, dbs.clone())
|
||||
|
||||
// CORS sucks
|
||||
app.at(&format!("{}*", config.root_url))
|
||||
.options(|_req: tide::Request<()>| async {
|
||||
Ok(tide::Response::builder(200)
|
||||
.header("Access-Control-Allow-Origin", &config.cors_allow_origin)
|
||||
.header("Access-Control-Allow-Headers", "*")
|
||||
.build())
|
||||
});
|
||||
|
||||
api::init_routes(&mut app, config, dbs.clone()).await;
|
||||
page::init_routes(&mut app, config, dbs, templates, locales, notify_send).await;
|
||||
|
||||
app.listen(config.listen).await.unwrap();
|
||||
}
|
||||
|
||||
async fn serve_edit_comment<'a>(
|
||||
req: tide::Request<()>,
|
||||
config: &Config,
|
||||
templates: &Templates,
|
||||
dbs: Dbs,
|
||||
client_langs: Vec<LanguageIdentifier>,
|
||||
mut context: Context,
|
||||
status_code: u16,
|
||||
) -> tide::Result<tide::Response> {
|
||||
let (Ok(comment_id_str), Ok(mutation_token_str)) = (req.param("comment_id"), req.param("mutation_token")) else {
|
||||
context.insert("log", &["no comment id or no token"]);
|
||||
return serve_comments(req, config, templates, dbs, client_langs, context, 400).await;
|
||||
};
|
||||
|
||||
let (Ok(comment_id), Ok(mutation_token)) = (CommentId::from_base64(comment_id_str), MutationToken::from_base64(mutation_token_str)) else {
|
||||
context.insert("log", &["badly encoded comment id or token"]);
|
||||
return serve_comments(req, config, templates, dbs, client_langs, context, 400).await;
|
||||
};
|
||||
|
||||
let Some((comment, _edited_comment)) = dbs.comment.get(&comment_id).unwrap() else {
|
||||
context.insert("log", &["not found comment"]);
|
||||
return serve_comments(req, config, templates, dbs, client_langs, context, 404).await;
|
||||
};
|
||||
|
||||
if let Err(e) = helpers::check_can_edit_comment(config, &comment, &mutation_token) {
|
||||
context.insert("log", &[e]);
|
||||
return serve_comments(req, config, templates, dbs, client_langs, context, 403).await;
|
||||
}
|
||||
|
||||
context.insert("edit_comment", &comment_id.to_base64());
|
||||
context.insert("edit_comment_mutation_token", &mutation_token.to_base64());
|
||||
context.insert("edit_comment_author", &comment.author);
|
||||
context.insert("edit_comment_email", &comment.email);
|
||||
context.insert("edit_comment_text", &comment.text);
|
||||
|
||||
serve_comments(
|
||||
req,
|
||||
config,
|
||||
templates,
|
||||
dbs,
|
||||
client_langs,
|
||||
context,
|
||||
status_code,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn serve_comments<'a>(
|
||||
req: tide::Request<()>,
|
||||
config: &Config,
|
||||
templates: &Templates,
|
||||
dbs: Dbs,
|
||||
client_langs: Vec<LanguageIdentifier>,
|
||||
mut context: Context,
|
||||
status_code: u16,
|
||||
) -> tide::Result<tide::Response> {
|
||||
let Ok(topic) = req.param("topic") else {
|
||||
return Err(tide::Error::from_str(404, "No topic"))
|
||||
};
|
||||
|
||||
let admin = req.cookie("admin").map_or(false, |psw| {
|
||||
check_admin_password_hash(config, &String::from(psw.value()))
|
||||
});
|
||||
|
||||
let topic_hash = TopicHash::from_topic(topic);
|
||||
|
||||
context.insert("config", &config);
|
||||
context.insert("admin", &admin);
|
||||
let time_lang = get_time_lang(&client_langs);
|
||||
context.insert(
|
||||
"time_lang",
|
||||
time_lang.as_ref().unwrap_or(&config.default_lang),
|
||||
);
|
||||
context.insert(
|
||||
"l",
|
||||
&client_langs
|
||||
.iter()
|
||||
.map(|lang| lang.language.as_str())
|
||||
.collect::<Vec<&str>>(),
|
||||
);
|
||||
|
||||
if admin {
|
||||
if let Ok(query) = req.query::<ApproveQuery>() {
|
||||
if let Ok(comment_id) = CommentId::from_base64(&query.approve) {
|
||||
helpers::approve_comment(comment_id, &dbs)
|
||||
.map_err(|e| error!("Approving comment: {:?}", e))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
if let Ok(query) = req.query::<ApproveEditQuery>() {
|
||||
if let Ok(comment_id) = CommentId::from_base64(&query.approve_edit) {
|
||||
helpers::approve_edit(comment_id, &dbs)
|
||||
.map_err(|e| error!("Approving edit: {:?}", e))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
if let Ok(query) = req.query::<RemoveQuery>() {
|
||||
if let Ok(comment_id) = CommentId::from_base64(&query.remove) {
|
||||
helpers::remove_comment(comment_id, &dbs)
|
||||
.map_err(|e| error!("Removing comment: {:?}", e))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
if let Ok(query) = req.query::<RemoveEditQuery>() {
|
||||
if let Ok(comment_id) = CommentId::from_base64(&query.remove_edit) {
|
||||
helpers::remove_edit(comment_id, &dbs)
|
||||
.map_err(|e| error!("Removing edit: {:?}", e))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
if let Ok(query) = req.query::<EditQuery>() {
|
||||
if let Ok(comment_id) = CommentId::from_base64(&query.edit) {
|
||||
if let Some((comment, _comment_status)) = 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, addr, comment_status)| {
|
||||
if let CommentStatus::ApprovedEdited(edited_comment) = comment_status {
|
||||
CommentWithId {
|
||||
addr: addr.map(|addr| addr.to_string()),
|
||||
author: edited_comment.author,
|
||||
editable: true,
|
||||
id: comment_id.to_base64(),
|
||||
last_edit_time: edited_comment.last_edit_time,
|
||||
needs_approval: true,
|
||||
original: Some(OriginalComment {
|
||||
author: comment.author,
|
||||
editable: true,
|
||||
last_edit_time: comment.last_edit_time,
|
||||
post_time: comment.post_time,
|
||||
text: comment.text,
|
||||
}),
|
||||
post_time: edited_comment.post_time,
|
||||
text: edited_comment.text,
|
||||
}
|
||||
} else {
|
||||
CommentWithId {
|
||||
addr: addr.map(|addr| addr.to_string()),
|
||||
author: comment.author,
|
||||
editable: true,
|
||||
id: comment_id.to_base64(),
|
||||
last_edit_time: comment.last_edit_time,
|
||||
needs_approval: true,
|
||||
original: None,
|
||||
post_time: comment.post_time,
|
||||
text: comment.text,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<CommentWithId>>(),
|
||||
);
|
||||
}
|
||||
|
||||
context.insert(
|
||||
"comments",
|
||||
&helpers::iter_approved_comments_by_topic(topic_hash, &dbs)
|
||||
.map(|(comment_id, comment, _comment_status)| CommentWithId {
|
||||
addr: None,
|
||||
author: comment.author,
|
||||
editable: admin,
|
||||
id: comment_id.to_base64(),
|
||||
last_edit_time: comment.last_edit_time,
|
||||
needs_approval: false,
|
||||
original: None,
|
||||
post_time: comment.post_time,
|
||||
text: comment.text,
|
||||
})
|
||||
.collect::<Vec<CommentWithId>>(),
|
||||
);
|
||||
|
||||
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>(
|
||||
_req: tide::Request<()>,
|
||||
config: &Config,
|
||||
templates: &Templates,
|
||||
dbs: Dbs,
|
||||
client_langs: &[LanguageIdentifier],
|
||||
) -> tide::Result<tide::Response> {
|
||||
let mut context = Context::new();
|
||||
context.insert("config", &config);
|
||||
context.insert("admin", &true);
|
||||
let time_lang = get_time_lang(client_langs);
|
||||
context.insert(
|
||||
"time_lang",
|
||||
time_lang.as_ref().unwrap_or(&config.default_lang),
|
||||
);
|
||||
context.insert(
|
||||
"l",
|
||||
&client_langs
|
||||
.iter()
|
||||
.map(|lang| lang.language.as_str())
|
||||
.collect::<Vec<&str>>(),
|
||||
);
|
||||
|
||||
context.insert(
|
||||
"comments",
|
||||
&dbs.comment_pending
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
let ((_topic_hash, _time, comment_id), (addr, _is_edit)) = entry
|
||||
.map_err(|e| error!("Reading comment_pending: {:?}", e))
|
||||
.ok()?;
|
||||
let (comment, comment_status) = dbs
|
||||
.comment
|
||||
.get(&comment_id)
|
||||
.map_err(|e| error!("Reading comment: {:?}", e))
|
||||
.ok()?
|
||||
.or_else(|| {
|
||||
error!("Comment not found");
|
||||
None
|
||||
})?;
|
||||
if let CommentStatus::ApprovedEdited(edited_comment) = comment_status {
|
||||
Some(CommentWithId {
|
||||
addr: addr.map(|addr| addr.to_string()),
|
||||
author: edited_comment.author,
|
||||
editable: true,
|
||||
id: comment_id.to_base64(),
|
||||
last_edit_time: edited_comment.last_edit_time,
|
||||
needs_approval: true,
|
||||
original: Some(OriginalComment {
|
||||
author: comment.author,
|
||||
editable: true,
|
||||
last_edit_time: comment.last_edit_time,
|
||||
post_time: comment.post_time,
|
||||
text: comment.text,
|
||||
}),
|
||||
post_time: edited_comment.post_time,
|
||||
text: edited_comment.text,
|
||||
})
|
||||
} else {
|
||||
Some(CommentWithId {
|
||||
addr: addr.map(|addr| addr.to_string()),
|
||||
author: comment.author,
|
||||
editable: true,
|
||||
id: comment_id.to_base64(),
|
||||
last_edit_time: comment.last_edit_time,
|
||||
needs_approval: true,
|
||||
original: None,
|
||||
post_time: comment.post_time,
|
||||
text: comment.text,
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Vec<CommentWithId>>(),
|
||||
);
|
||||
|
||||
Ok(tide::Response::builder(200)
|
||||
.content_type(tide::http::mime::HTML)
|
||||
.body(templates.tera.render("comments.html", &context)?)
|
||||
.build())
|
||||
}
|
||||
|
||||
async fn serve_admin_login(
|
||||
_req: tide::Request<()>,
|
||||
config: &Config,
|
||||
templates: &Templates,
|
||||
client_langs: Vec<LanguageIdentifier>,
|
||||
) -> tide::Result<tide::Response> {
|
||||
let mut context = Context::new();
|
||||
context.insert("config", &config);
|
||||
let time_lang = get_time_lang(&client_langs);
|
||||
context.insert(
|
||||
"time_lang",
|
||||
time_lang.as_ref().unwrap_or(&config.default_lang),
|
||||
);
|
||||
context.insert(
|
||||
"l",
|
||||
&client_langs
|
||||
.iter()
|
||||
.map(|lang| lang.language.as_str())
|
||||
.collect::<Vec<&str>>(),
|
||||
);
|
||||
|
||||
Ok(tide::Response::builder(200)
|
||||
.content_type(tide::http::mime::HTML)
|
||||
.body(templates.tera.render("admin_login.html", &context)?)
|
||||
.build())
|
||||
}
|
||||
|
||||
async fn handle_post_comments(
|
||||
mut req: tide::Request<()>,
|
||||
config: &Config,
|
||||
templates: &Templates,
|
||||
dbs: Dbs,
|
||||
locales: &Locales,
|
||||
notify_send: Sender<Notification>,
|
||||
) -> tide::Result<tide::Response> {
|
||||
let admin = req.cookie("admin").map_or(false, |psw| {
|
||||
check_admin_password_hash(config, &String::from(psw.value()))
|
||||
});
|
||||
|
||||
let client_langs = get_client_langs(&req);
|
||||
|
||||
let client_addr = match helpers::get_client_addr(config, &req) {
|
||||
Some(Ok(addr)) => Some(addr),
|
||||
Some(Err(e)) => {
|
||||
warn!("Unable to parse client addr: {}", e);
|
||||
None
|
||||
}
|
||||
None => {
|
||||
warn!("No client addr");
|
||||
None
|
||||
}
|
||||
};
|
||||
let antispam_enabled = !admin
|
||||
&& config.antispam_enable
|
||||
&& client_addr
|
||||
.as_ref()
|
||||
.map_or(false, |addr| !config.antispam_whitelist.contains(addr));
|
||||
|
||||
let mut errors = Vec::new();
|
||||
let mut context = Context::new();
|
||||
|
||||
match req.body_form::<CommentQuery>().await? {
|
||||
CommentQuery::NewComment(query) => {
|
||||
let Ok(topic) = req.param("topic") else {
|
||||
return Err(tide::Error::from_str(404, "No topic"))
|
||||
};
|
||||
|
||||
helpers::check_comment(config, locales, &client_langs, &query.comment, &mut errors);
|
||||
|
||||
if let Some(client_addr) = &client_addr {
|
||||
if antispam_enabled {
|
||||
if let Some(antispam_timeout) =
|
||||
helpers::antispam_check_client_mutation(client_addr, &dbs, config).unwrap()
|
||||
{
|
||||
errors.push(
|
||||
locales
|
||||
.tr(
|
||||
&client_langs,
|
||||
"error-antispam",
|
||||
Some(&FluentArgs::from_iter([(
|
||||
"antispam_timeout",
|
||||
antispam_timeout,
|
||||
)])),
|
||||
)
|
||||
.unwrap()
|
||||
.into_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
if let Some(client_addr) = &client_addr {
|
||||
if antispam_enabled {
|
||||
helpers::antispam_update_client_mutation(client_addr, &dbs).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let topic_hash = TopicHash::from_topic(topic);
|
||||
|
||||
let time = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
let comment = Comment {
|
||||
topic_hash,
|
||||
author: if query.comment.author.is_empty() {
|
||||
petname::Petnames::large().generate_one(2, " ")
|
||||
} else {
|
||||
query.comment.author
|
||||
},
|
||||
email: if query.comment.email.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(query.comment.email)
|
||||
},
|
||||
last_edit_time: None,
|
||||
mutation_token: MutationToken::new(),
|
||||
post_time: time,
|
||||
text: query.comment.text,
|
||||
};
|
||||
match helpers::new_pending_comment(&comment, client_addr, &dbs) {
|
||||
Ok(comment_id) => {
|
||||
notify_send
|
||||
.send(Notification {
|
||||
topic: topic.to_string(),
|
||||
})
|
||||
.ok();
|
||||
context.insert(
|
||||
"log",
|
||||
&[locales
|
||||
.tr(
|
||||
&client_langs,
|
||||
if config.comment_approve {
|
||||
"new_comment-success_pending"
|
||||
} else {
|
||||
"new_comment-success"
|
||||
},
|
||||
Some(&FluentArgs::from_iter([(
|
||||
"edit_link",
|
||||
format!(
|
||||
"{}t/{}/edit/{}/{}",
|
||||
&config.root_url,
|
||||
topic,
|
||||
comment_id.to_base64(),
|
||||
comment.mutation_token.to_base64(),
|
||||
),
|
||||
)])),
|
||||
)
|
||||
.unwrap()],
|
||||
);
|
||||
}
|
||||
// TODO add message to client log and change http code
|
||||
Err(e) => error!("Adding pending comment: {:?}", e),
|
||||
}
|
||||
} else {
|
||||
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) => {
|
||||
let Ok(topic) = req.param("topic") else {
|
||||
return Err(tide::Error::from_str(404, "No topic"))
|
||||
};
|
||||
|
||||
let Ok(comment_id) = CommentId::from_base64(&query.id) else {
|
||||
return Err(tide::Error::from_str(400, "Invalid comment id"));
|
||||
};
|
||||
|
||||
let Some((old_comment, old_edited_comment)) = dbs.comment.get(&comment_id).unwrap() else {
|
||||
return Err(tide::Error::from_str(404, "Not found"));
|
||||
};
|
||||
|
||||
helpers::check_comment(config, locales, &client_langs, &query.comment, &mut errors);
|
||||
|
||||
let mutation_token = if admin {
|
||||
None
|
||||
} else {
|
||||
'mutation_token: {
|
||||
let Ok(mutation_token_str) = req.param("mutation_token") else {
|
||||
errors.push("no mutation token".into());
|
||||
break 'mutation_token None;
|
||||
};
|
||||
|
||||
let Ok(mutation_token) = MutationToken::from_base64(mutation_token_str) else {
|
||||
errors.push("badly encoded token".into());
|
||||
break 'mutation_token None;
|
||||
};
|
||||
|
||||
if let Err(e) =
|
||||
helpers::check_can_edit_comment(config, &old_comment, &mutation_token)
|
||||
{
|
||||
errors.push(e.to_string());
|
||||
}
|
||||
|
||||
Some(mutation_token)
|
||||
}
|
||||
};
|
||||
|
||||
if !admin {
|
||||
if let Some(client_addr) = &client_addr {
|
||||
if let Some(antispam_timeout) =
|
||||
helpers::antispam_check_client_mutation(client_addr, &dbs, config).unwrap()
|
||||
{
|
||||
let client_langs = get_client_langs(&req);
|
||||
errors.push(
|
||||
locales
|
||||
.tr(
|
||||
&client_langs,
|
||||
"error-antispam",
|
||||
Some(&FluentArgs::from_iter([(
|
||||
"antispam_timeout",
|
||||
antispam_timeout,
|
||||
)])),
|
||||
)
|
||||
.unwrap()
|
||||
.into_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
if !admin {
|
||||
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();
|
||||
|
||||
let mut comment = old_comment.clone();
|
||||
|
||||
comment.author = if query.comment.author.is_empty() {
|
||||
petname::Petnames::large().generate_one(2, " ")
|
||||
} else {
|
||||
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);
|
||||
|
||||
match helpers::edit_comment(
|
||||
comment_id.clone(),
|
||||
old_comment,
|
||||
old_edited_comment,
|
||||
comment.clone(),
|
||||
client_addr,
|
||||
&dbs,
|
||||
) {
|
||||
Ok(()) => {
|
||||
context.insert(
|
||||
"log",
|
||||
&[locales
|
||||
.tr(
|
||||
&client_langs,
|
||||
if config.comment_approve {
|
||||
"edit_comment-success_pending"
|
||||
} else {
|
||||
"edit_comment-success"
|
||||
},
|
||||
Some(&FluentArgs::from_iter([(
|
||||
"edit_link",
|
||||
format!(
|
||||
"{}t/{}/edit/{}/{}",
|
||||
&config.root_url,
|
||||
topic,
|
||||
comment_id.to_base64(),
|
||||
comment.mutation_token.to_base64(),
|
||||
),
|
||||
)])),
|
||||
)
|
||||
.unwrap()],
|
||||
);
|
||||
}
|
||||
// TODO add message to client log and change http code
|
||||
Err(e) => error!("Editing comment: {:?}", e),
|
||||
}
|
||||
} else {
|
||||
context.insert("edit_comment", &comment_id.to_base64());
|
||||
if let Some(mutation_token) = &mutation_token {
|
||||
context.insert("edit_comment_mutation_token", &mutation_token.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);
|
||||
|
||||
return serve_edit_comment(req, config, templates, dbs, client_langs, context, 400)
|
||||
.await;
|
||||
}
|
||||
context.insert("edit_comment_errors", &errors);
|
||||
}
|
||||
}
|
||||
serve_comments(
|
||||
req,
|
||||
config,
|
||||
templates,
|
||||
dbs,
|
||||
client_langs,
|
||||
context,
|
||||
if errors.is_empty() { 200 } else { 400 },
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn handle_post_admin(
|
||||
mut req: tide::Request<()>,
|
||||
config: &Config,
|
||||
templates: &Templates,
|
||||
dbs: Dbs,
|
||||
) -> tide::Result<tide::Response> {
|
||||
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::<AdminQuery>().await? {
|
||||
_ => {
|
||||
let client_langs = get_client_langs(&req);
|
||||
serve_admin(req, config, templates, dbs, &client_langs).await
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let client_langs = get_client_langs(&req);
|
||||
serve_admin_login(req, config, templates, client_langs).await
|
||||
}
|
||||
} else if let AdminQuery::Login(query) = req.body_form::<AdminQuery>().await? {
|
||||
if let Some(password_hash) = check_admin_password(config, &query.psw) {
|
||||
let client_langs = get_client_langs(&req);
|
||||
serve_admin(req, config, templates, dbs, &client_langs)
|
||||
.await
|
||||
.map(|mut r| {
|
||||
let mut cookie = tide::http::Cookie::new("admin", password_hash);
|
||||
cookie.set_http_only(Some(true));
|
||||
cookie.set_path(config.root_url.clone());
|
||||
if let Some(domain) = &config.cookies_domain {
|
||||
cookie.set_domain(domain.clone());
|
||||
}
|
||||
if config.cookies_https_only {
|
||||
cookie.set_secure(Some(true));
|
||||
}
|
||||
r.insert_cookie(cookie);
|
||||
r
|
||||
})
|
||||
} else {
|
||||
let client_langs = get_client_langs(&req);
|
||||
serve_admin_login(req, config, templates, client_langs).await
|
||||
}
|
||||
} else {
|
||||
let client_langs = get_client_langs(&req);
|
||||
serve_admin_login(req, config, templates, client_langs).await
|
||||
}
|
||||
}
|
||||
|
||||
fn check_admin_password(config: &Config, password: &str) -> Option<String> {
|
||||
let argon2 = Argon2::default();
|
||||
config
|
||||
|
|
92
src/server/api.rs
Normal file
92
src/server/api.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use crate::{config::*, db::*, helpers, notify::Notification};
|
||||
|
||||
use crossbeam_channel::Sender;
|
||||
use log::{error, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
enum ApiError {
|
||||
InvalidAdminPassword,
|
||||
}
|
||||
|
||||
pub async fn init_routes(app: &mut tide::Server<()>, config: &'static Config, dbs: Dbs) {
|
||||
// TODO pagination
|
||||
app.at(&format!("{}api/comments_by_topic", config.root_url))
|
||||
.post({
|
||||
let dbs = dbs.clone();
|
||||
move |req: tide::Request<()>| query_comments_by_topic(req, config, dbs.clone())
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CommentWithId {
|
||||
pub addr: Option<String>,
|
||||
pub author: String,
|
||||
pub editable: bool,
|
||||
pub id: String,
|
||||
pub last_edit_time: Option<Time>,
|
||||
pub status: Option<OriginalComment>,
|
||||
pub post_time: Time,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct OriginalComment {
|
||||
pub author: String,
|
||||
pub editable: bool,
|
||||
pub last_edit_time: Option<Time>,
|
||||
pub post_time: Time,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CommentsByTopicQuery {
|
||||
mutation_token: Option<String>,
|
||||
topic: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CommentsByTopicResp {
|
||||
comments: Vec<CommentWithId>,
|
||||
}
|
||||
|
||||
async fn query_comments_by_topic(
|
||||
mut req: tide::Request<()>,
|
||||
config: &Config,
|
||||
dbs: Dbs,
|
||||
) -> tide::Result<tide::Response> {
|
||||
let Ok(CommentsByTopicQuery {
|
||||
mutation_token,
|
||||
topic,
|
||||
}) = req.body_json().await else {
|
||||
return Err(tide::Error::from_str(400, "Invalid request"));
|
||||
};
|
||||
|
||||
let topic_hash = TopicHash::from_topic(&topic);
|
||||
|
||||
Ok(tide::Response::builder(200)
|
||||
.content_type(tide::http::mime::JSON)
|
||||
.header("Access-Control-Allow-Origin", &config.cors_allow_origin)
|
||||
.body(
|
||||
tide::Body::from_json(&CommentsByTopicResp {
|
||||
comments: helpers::iter_approved_comments_by_topic(topic_hash, &dbs)
|
||||
.map(|(comment_id, comment, _comment_status)| CommentWithId {
|
||||
addr: None,
|
||||
author: comment.author,
|
||||
editable: false,
|
||||
id: comment_id.to_base64(),
|
||||
last_edit_time: comment.last_edit_time,
|
||||
post_time: comment.post_time,
|
||||
status: None,
|
||||
text: comment.text,
|
||||
})
|
||||
.collect::<Vec<CommentWithId>>(),
|
||||
})
|
||||
.map_err(|e| {
|
||||
error!("Serializing CommentsByTopicResp to json: {e:?}");
|
||||
tide::Error::from_str(500, "Internal server error")
|
||||
})?,
|
||||
)
|
||||
.build())
|
||||
}
|
726
src/server/page.rs
Normal file
726
src/server/page.rs
Normal file
|
@ -0,0 +1,726 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
pub mod queries;
|
||||
pub mod templates;
|
||||
|
||||
use super::{check_admin_password, check_admin_password_hash};
|
||||
use crate::{config::*, db::*, helpers, locales::*, notify::Notification};
|
||||
use queries::*;
|
||||
use templates::*;
|
||||
|
||||
use crossbeam_channel::Sender;
|
||||
use fluent_bundle::FluentArgs;
|
||||
use log::{error, warn};
|
||||
use tera::Context;
|
||||
use unic_langid::LanguageIdentifier;
|
||||
|
||||
pub async fn init_routes(
|
||||
app: &mut tide::Server<()>,
|
||||
config: &'static Config,
|
||||
dbs: Dbs,
|
||||
templates: &'static Templates,
|
||||
locales: &'static Locales,
|
||||
notify_send: Sender<Notification>,
|
||||
) {
|
||||
app.at(&format!("{}t/:topic", config.root_url)).get({
|
||||
let dbs = dbs.clone();
|
||||
move |req: tide::Request<()>| {
|
||||
let client_langs = get_client_langs(&req);
|
||||
serve_comments(
|
||||
req,
|
||||
config,
|
||||
templates,
|
||||
dbs.clone(),
|
||||
client_langs,
|
||||
Context::new(),
|
||||
200,
|
||||
)
|
||||
}
|
||||
});
|
||||
app.at(&format!("{}t/:topic", config.root_url)).post({
|
||||
let dbs = dbs.clone();
|
||||
let notify_send = notify_send.clone();
|
||||
move |req: tide::Request<()>| {
|
||||
handle_post_comments(
|
||||
req,
|
||||
config,
|
||||
templates,
|
||||
dbs.clone(),
|
||||
locales,
|
||||
notify_send.clone(),
|
||||
)
|
||||
}
|
||||
});
|
||||
app.at(&format!(
|
||||
"{}t/:topic/edit/:comment_id/:mutation_token",
|
||||
config.root_url
|
||||
))
|
||||
.get({
|
||||
let dbs = dbs.clone();
|
||||
move |req: tide::Request<()>| {
|
||||
let client_langs = get_client_langs(&req);
|
||||
serve_edit_comment(
|
||||
req,
|
||||
config,
|
||||
templates,
|
||||
dbs.clone(),
|
||||
client_langs,
|
||||
Context::new(),
|
||||
200,
|
||||
)
|
||||
}
|
||||
});
|
||||
app.at(&format!(
|
||||
"{}t/:topic/edit/:comment_id/:mutation_token",
|
||||
config.root_url
|
||||
))
|
||||
.post({
|
||||
let dbs = dbs.clone();
|
||||
move |req: tide::Request<()>| {
|
||||
handle_post_comments(
|
||||
req,
|
||||
config,
|
||||
templates,
|
||||
dbs.clone(),
|
||||
locales,
|
||||
notify_send.clone(),
|
||||
)
|
||||
}
|
||||
});
|
||||
app.at(&format!("{}admin", config.root_url))
|
||||
.get(move |req: tide::Request<()>| {
|
||||
let client_langs = get_client_langs(&req);
|
||||
serve_admin_login(req, config, templates, client_langs)
|
||||
});
|
||||
app.at(&format!("{}admin", config.root_url)).post({
|
||||
move |req: tide::Request<()>| handle_post_admin(req, config, templates, dbs.clone())
|
||||
});
|
||||
}
|
||||
|
||||
async fn serve_edit_comment<'a>(
|
||||
req: tide::Request<()>,
|
||||
config: &Config,
|
||||
templates: &Templates,
|
||||
dbs: Dbs,
|
||||
client_langs: Vec<LanguageIdentifier>,
|
||||
mut context: Context,
|
||||
status_code: u16,
|
||||
) -> tide::Result<tide::Response> {
|
||||
let (Ok(comment_id_str), Ok(mutation_token_str)) = (req.param("comment_id"), req.param("mutation_token")) else {
|
||||
context.insert("log", &["no comment id or no token"]);
|
||||
return serve_comments(req, config, templates, dbs, client_langs, context, 400).await;
|
||||
};
|
||||
|
||||
let (Ok(comment_id), Ok(mutation_token)) = (CommentId::from_base64(comment_id_str), MutationToken::from_base64(mutation_token_str)) else {
|
||||
context.insert("log", &["badly encoded comment id or token"]);
|
||||
return serve_comments(req, config, templates, dbs, client_langs, context, 400).await;
|
||||
};
|
||||
|
||||
let Some((comment, _edited_comment)) = dbs.comment.get(&comment_id).unwrap() else {
|
||||
context.insert("log", &["not found comment"]);
|
||||
return serve_comments(req, config, templates, dbs, client_langs, context, 404).await;
|
||||
};
|
||||
|
||||
if let Err(e) = helpers::check_can_edit_comment(config, &comment, &mutation_token) {
|
||||
context.insert("log", &[e]);
|
||||
return serve_comments(req, config, templates, dbs, client_langs, context, 403).await;
|
||||
}
|
||||
|
||||
context.insert("edit_comment", &comment_id.to_base64());
|
||||
context.insert("edit_comment_mutation_token", &mutation_token.to_base64());
|
||||
context.insert("edit_comment_author", &comment.author);
|
||||
context.insert("edit_comment_email", &comment.email);
|
||||
context.insert("edit_comment_text", &comment.text);
|
||||
|
||||
serve_comments(
|
||||
req,
|
||||
config,
|
||||
templates,
|
||||
dbs,
|
||||
client_langs,
|
||||
context,
|
||||
status_code,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn serve_comments<'a>(
|
||||
req: tide::Request<()>,
|
||||
config: &Config,
|
||||
templates: &Templates,
|
||||
dbs: Dbs,
|
||||
client_langs: Vec<LanguageIdentifier>,
|
||||
mut context: Context,
|
||||
status_code: u16,
|
||||
) -> tide::Result<tide::Response> {
|
||||
let Ok(topic) = req.param("topic") else {
|
||||
return Err(tide::Error::from_str(404, "No topic"))
|
||||
};
|
||||
|
||||
let admin = req.cookie("admin").map_or(false, |psw| {
|
||||
check_admin_password_hash(config, &String::from(psw.value()))
|
||||
});
|
||||
|
||||
let topic_hash = TopicHash::from_topic(topic);
|
||||
|
||||
context.insert("config", &config);
|
||||
context.insert("admin", &admin);
|
||||
let time_lang = get_time_lang(&client_langs);
|
||||
context.insert(
|
||||
"time_lang",
|
||||
time_lang.as_ref().unwrap_or(&config.default_lang),
|
||||
);
|
||||
context.insert(
|
||||
"l",
|
||||
&client_langs
|
||||
.iter()
|
||||
.map(|lang| lang.language.as_str())
|
||||
.collect::<Vec<&str>>(),
|
||||
);
|
||||
|
||||
if admin {
|
||||
if let Ok(query) = req.query::<ApproveQuery>() {
|
||||
if let Ok(comment_id) = CommentId::from_base64(&query.approve) {
|
||||
helpers::approve_comment(comment_id, &dbs)
|
||||
.map_err(|e| error!("Approving comment: {:?}", e))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
if let Ok(query) = req.query::<ApproveEditQuery>() {
|
||||
if let Ok(comment_id) = CommentId::from_base64(&query.approve_edit) {
|
||||
helpers::approve_edit(comment_id, &dbs)
|
||||
.map_err(|e| error!("Approving edit: {:?}", e))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
if let Ok(query) = req.query::<RemoveQuery>() {
|
||||
if let Ok(comment_id) = CommentId::from_base64(&query.remove) {
|
||||
helpers::remove_comment(comment_id, &dbs)
|
||||
.map_err(|e| error!("Removing comment: {:?}", e))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
if let Ok(query) = req.query::<RemoveEditQuery>() {
|
||||
if let Ok(comment_id) = CommentId::from_base64(&query.remove_edit) {
|
||||
helpers::remove_edit(comment_id, &dbs)
|
||||
.map_err(|e| error!("Removing edit: {:?}", e))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
if let Ok(query) = req.query::<EditQuery>() {
|
||||
if let Ok(comment_id) = CommentId::from_base64(&query.edit) {
|
||||
if let Some((comment, _comment_status)) = 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, addr, comment_status)| {
|
||||
if let CommentStatus::ApprovedEdited(edited_comment) = comment_status {
|
||||
CommentWithId {
|
||||
addr: addr.map(|addr| addr.to_string()),
|
||||
author: edited_comment.author,
|
||||
editable: true,
|
||||
id: comment_id.to_base64(),
|
||||
last_edit_time: edited_comment.last_edit_time,
|
||||
needs_approval: true,
|
||||
original: Some(OriginalComment {
|
||||
author: comment.author,
|
||||
editable: true,
|
||||
last_edit_time: comment.last_edit_time,
|
||||
post_time: comment.post_time,
|
||||
text: comment.text,
|
||||
}),
|
||||
post_time: edited_comment.post_time,
|
||||
text: edited_comment.text,
|
||||
}
|
||||
} else {
|
||||
CommentWithId {
|
||||
addr: addr.map(|addr| addr.to_string()),
|
||||
author: comment.author,
|
||||
editable: true,
|
||||
id: comment_id.to_base64(),
|
||||
last_edit_time: comment.last_edit_time,
|
||||
needs_approval: true,
|
||||
original: None,
|
||||
post_time: comment.post_time,
|
||||
text: comment.text,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<CommentWithId>>(),
|
||||
);
|
||||
}
|
||||
|
||||
context.insert(
|
||||
"comments",
|
||||
&helpers::iter_approved_comments_by_topic(topic_hash, &dbs)
|
||||
.map(|(comment_id, comment, _comment_status)| CommentWithId {
|
||||
addr: None,
|
||||
author: comment.author,
|
||||
editable: admin,
|
||||
id: comment_id.to_base64(),
|
||||
last_edit_time: comment.last_edit_time,
|
||||
needs_approval: false,
|
||||
original: None,
|
||||
post_time: comment.post_time,
|
||||
text: comment.text,
|
||||
})
|
||||
.collect::<Vec<CommentWithId>>(),
|
||||
);
|
||||
|
||||
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>(
|
||||
_req: tide::Request<()>,
|
||||
config: &Config,
|
||||
templates: &Templates,
|
||||
dbs: Dbs,
|
||||
client_langs: &[LanguageIdentifier],
|
||||
) -> tide::Result<tide::Response> {
|
||||
let mut context = Context::new();
|
||||
context.insert("config", &config);
|
||||
context.insert("admin", &true);
|
||||
let time_lang = get_time_lang(client_langs);
|
||||
context.insert(
|
||||
"time_lang",
|
||||
time_lang.as_ref().unwrap_or(&config.default_lang),
|
||||
);
|
||||
context.insert(
|
||||
"l",
|
||||
&client_langs
|
||||
.iter()
|
||||
.map(|lang| lang.language.as_str())
|
||||
.collect::<Vec<&str>>(),
|
||||
);
|
||||
|
||||
context.insert(
|
||||
"comments",
|
||||
&dbs.comment_pending
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
let ((_topic_hash, _time, comment_id), (addr, _is_edit)) = entry
|
||||
.map_err(|e| error!("Reading comment_pending: {:?}", e))
|
||||
.ok()?;
|
||||
let (comment, comment_status) = dbs
|
||||
.comment
|
||||
.get(&comment_id)
|
||||
.map_err(|e| error!("Reading comment: {:?}", e))
|
||||
.ok()?
|
||||
.or_else(|| {
|
||||
error!("Comment not found");
|
||||
None
|
||||
})?;
|
||||
if let CommentStatus::ApprovedEdited(edited_comment) = comment_status {
|
||||
Some(CommentWithId {
|
||||
addr: addr.map(|addr| addr.to_string()),
|
||||
author: edited_comment.author,
|
||||
editable: true,
|
||||
id: comment_id.to_base64(),
|
||||
last_edit_time: edited_comment.last_edit_time,
|
||||
needs_approval: true,
|
||||
original: Some(OriginalComment {
|
||||
author: comment.author,
|
||||
editable: true,
|
||||
last_edit_time: comment.last_edit_time,
|
||||
post_time: comment.post_time,
|
||||
text: comment.text,
|
||||
}),
|
||||
post_time: edited_comment.post_time,
|
||||
text: edited_comment.text,
|
||||
})
|
||||
} else {
|
||||
Some(CommentWithId {
|
||||
addr: addr.map(|addr| addr.to_string()),
|
||||
author: comment.author,
|
||||
editable: true,
|
||||
id: comment_id.to_base64(),
|
||||
last_edit_time: comment.last_edit_time,
|
||||
needs_approval: true,
|
||||
original: None,
|
||||
post_time: comment.post_time,
|
||||
text: comment.text,
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Vec<CommentWithId>>(),
|
||||
);
|
||||
|
||||
Ok(tide::Response::builder(200)
|
||||
.content_type(tide::http::mime::HTML)
|
||||
.body(templates.tera.render("comments.html", &context)?)
|
||||
.build())
|
||||
}
|
||||
|
||||
async fn serve_admin_login(
|
||||
_req: tide::Request<()>,
|
||||
config: &Config,
|
||||
templates: &Templates,
|
||||
client_langs: Vec<LanguageIdentifier>,
|
||||
) -> tide::Result<tide::Response> {
|
||||
let mut context = Context::new();
|
||||
context.insert("config", &config);
|
||||
let time_lang = get_time_lang(&client_langs);
|
||||
context.insert(
|
||||
"time_lang",
|
||||
time_lang.as_ref().unwrap_or(&config.default_lang),
|
||||
);
|
||||
context.insert(
|
||||
"l",
|
||||
&client_langs
|
||||
.iter()
|
||||
.map(|lang| lang.language.as_str())
|
||||
.collect::<Vec<&str>>(),
|
||||
);
|
||||
|
||||
Ok(tide::Response::builder(200)
|
||||
.content_type(tide::http::mime::HTML)
|
||||
.body(templates.tera.render("admin_login.html", &context)?)
|
||||
.build())
|
||||
}
|
||||
|
||||
async fn handle_post_comments(
|
||||
mut req: tide::Request<()>,
|
||||
config: &Config,
|
||||
templates: &Templates,
|
||||
dbs: Dbs,
|
||||
locales: &Locales,
|
||||
notify_send: Sender<Notification>,
|
||||
) -> tide::Result<tide::Response> {
|
||||
let admin = req.cookie("admin").map_or(false, |psw| {
|
||||
check_admin_password_hash(config, &String::from(psw.value()))
|
||||
});
|
||||
|
||||
let client_langs = get_client_langs(&req);
|
||||
|
||||
let client_addr = match helpers::get_client_addr(config, &req) {
|
||||
Some(Ok(addr)) => Some(addr),
|
||||
Some(Err(e)) => {
|
||||
warn!("Unable to parse client addr: {}", e);
|
||||
None
|
||||
}
|
||||
None => {
|
||||
warn!("No client addr");
|
||||
None
|
||||
}
|
||||
};
|
||||
let antispam_enabled = !admin
|
||||
&& config.antispam_enable
|
||||
&& client_addr
|
||||
.as_ref()
|
||||
.map_or(false, |addr| !config.antispam_whitelist.contains(addr));
|
||||
|
||||
let mut errors = Vec::new();
|
||||
let mut context = Context::new();
|
||||
|
||||
match req.body_form::<CommentQuery>().await? {
|
||||
CommentQuery::NewComment(query) => {
|
||||
let Ok(topic) = req.param("topic") else {
|
||||
return Err(tide::Error::from_str(404, "No topic"))
|
||||
};
|
||||
|
||||
helpers::check_comment(config, locales, &client_langs, &query.comment, &mut errors);
|
||||
|
||||
if let Some(client_addr) = &client_addr {
|
||||
if antispam_enabled {
|
||||
if let Some(antispam_timeout) =
|
||||
helpers::antispam_check_client_mutation(client_addr, &dbs, config).unwrap()
|
||||
{
|
||||
errors.push(
|
||||
locales
|
||||
.tr(
|
||||
&client_langs,
|
||||
"error-antispam",
|
||||
Some(&FluentArgs::from_iter([(
|
||||
"antispam_timeout",
|
||||
antispam_timeout,
|
||||
)])),
|
||||
)
|
||||
.unwrap()
|
||||
.into_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
if let Some(client_addr) = &client_addr {
|
||||
if antispam_enabled {
|
||||
helpers::antispam_update_client_mutation(client_addr, &dbs).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let topic_hash = TopicHash::from_topic(topic);
|
||||
|
||||
let time = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
let comment = Comment {
|
||||
topic_hash,
|
||||
author: if query.comment.author.is_empty() {
|
||||
petname::Petnames::large().generate_one(2, " ")
|
||||
} else {
|
||||
query.comment.author
|
||||
},
|
||||
email: if query.comment.email.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(query.comment.email)
|
||||
},
|
||||
last_edit_time: None,
|
||||
mutation_token: MutationToken::new(),
|
||||
post_time: time,
|
||||
text: query.comment.text,
|
||||
};
|
||||
match helpers::new_pending_comment(&comment, client_addr, &dbs) {
|
||||
Ok(comment_id) => {
|
||||
notify_send
|
||||
.send(Notification {
|
||||
topic: topic.to_string(),
|
||||
})
|
||||
.ok();
|
||||
context.insert(
|
||||
"log",
|
||||
&[locales
|
||||
.tr(
|
||||
&client_langs,
|
||||
if config.comment_approve {
|
||||
"new_comment-success_pending"
|
||||
} else {
|
||||
"new_comment-success"
|
||||
},
|
||||
Some(&FluentArgs::from_iter([(
|
||||
"edit_link",
|
||||
format!(
|
||||
"{}t/{}/edit/{}/{}",
|
||||
&config.root_url,
|
||||
topic,
|
||||
comment_id.to_base64(),
|
||||
comment.mutation_token.to_base64(),
|
||||
),
|
||||
)])),
|
||||
)
|
||||
.unwrap()],
|
||||
);
|
||||
}
|
||||
// TODO add message to client log and change http code
|
||||
Err(e) => error!("Adding pending comment: {:?}", e),
|
||||
}
|
||||
} else {
|
||||
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) => {
|
||||
let Ok(topic) = req.param("topic") else {
|
||||
return Err(tide::Error::from_str(404, "No topic"))
|
||||
};
|
||||
|
||||
let Ok(comment_id) = CommentId::from_base64(&query.id) else {
|
||||
return Err(tide::Error::from_str(400, "Invalid comment id"));
|
||||
};
|
||||
|
||||
let Some((old_comment, old_edited_comment)) = dbs.comment.get(&comment_id).unwrap() else {
|
||||
return Err(tide::Error::from_str(404, "Not found"));
|
||||
};
|
||||
|
||||
helpers::check_comment(config, locales, &client_langs, &query.comment, &mut errors);
|
||||
|
||||
let mutation_token = if admin {
|
||||
None
|
||||
} else {
|
||||
'mutation_token: {
|
||||
let Ok(mutation_token_str) = req.param("mutation_token") else {
|
||||
errors.push("no mutation token".into());
|
||||
break 'mutation_token None;
|
||||
};
|
||||
|
||||
let Ok(mutation_token) = MutationToken::from_base64(mutation_token_str) else {
|
||||
errors.push("badly encoded token".into());
|
||||
break 'mutation_token None;
|
||||
};
|
||||
|
||||
if let Err(e) =
|
||||
helpers::check_can_edit_comment(config, &old_comment, &mutation_token)
|
||||
{
|
||||
errors.push(e.to_string());
|
||||
}
|
||||
|
||||
Some(mutation_token)
|
||||
}
|
||||
};
|
||||
|
||||
if !admin {
|
||||
if let Some(client_addr) = &client_addr {
|
||||
if let Some(antispam_timeout) =
|
||||
helpers::antispam_check_client_mutation(client_addr, &dbs, config).unwrap()
|
||||
{
|
||||
let client_langs = get_client_langs(&req);
|
||||
errors.push(
|
||||
locales
|
||||
.tr(
|
||||
&client_langs,
|
||||
"error-antispam",
|
||||
Some(&FluentArgs::from_iter([(
|
||||
"antispam_timeout",
|
||||
antispam_timeout,
|
||||
)])),
|
||||
)
|
||||
.unwrap()
|
||||
.into_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
if !admin {
|
||||
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();
|
||||
|
||||
let mut comment = old_comment.clone();
|
||||
|
||||
comment.author = if query.comment.author.is_empty() {
|
||||
petname::Petnames::large().generate_one(2, " ")
|
||||
} else {
|
||||
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);
|
||||
|
||||
match helpers::edit_comment(
|
||||
comment_id.clone(),
|
||||
old_comment,
|
||||
old_edited_comment,
|
||||
comment.clone(),
|
||||
client_addr,
|
||||
&dbs,
|
||||
) {
|
||||
Ok(()) => {
|
||||
context.insert(
|
||||
"log",
|
||||
&[locales
|
||||
.tr(
|
||||
&client_langs,
|
||||
if config.comment_approve {
|
||||
"edit_comment-success_pending"
|
||||
} else {
|
||||
"edit_comment-success"
|
||||
},
|
||||
Some(&FluentArgs::from_iter([(
|
||||
"edit_link",
|
||||
format!(
|
||||
"{}t/{}/edit/{}/{}",
|
||||
&config.root_url,
|
||||
topic,
|
||||
comment_id.to_base64(),
|
||||
comment.mutation_token.to_base64(),
|
||||
),
|
||||
)])),
|
||||
)
|
||||
.unwrap()],
|
||||
);
|
||||
}
|
||||
// TODO add message to client log and change http code
|
||||
Err(e) => error!("Editing comment: {:?}", e),
|
||||
}
|
||||
} else {
|
||||
context.insert("edit_comment", &comment_id.to_base64());
|
||||
if let Some(mutation_token) = &mutation_token {
|
||||
context.insert("edit_comment_mutation_token", &mutation_token.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);
|
||||
|
||||
return serve_edit_comment(req, config, templates, dbs, client_langs, context, 400)
|
||||
.await;
|
||||
}
|
||||
context.insert("edit_comment_errors", &errors);
|
||||
}
|
||||
}
|
||||
serve_comments(
|
||||
req,
|
||||
config,
|
||||
templates,
|
||||
dbs,
|
||||
client_langs,
|
||||
context,
|
||||
if errors.is_empty() { 200 } else { 400 },
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn handle_post_admin(
|
||||
mut req: tide::Request<()>,
|
||||
config: &Config,
|
||||
templates: &Templates,
|
||||
dbs: Dbs,
|
||||
) -> tide::Result<tide::Response> {
|
||||
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::<AdminQuery>().await? {
|
||||
_ => {
|
||||
let client_langs = get_client_langs(&req);
|
||||
serve_admin(req, config, templates, dbs, &client_langs).await
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let client_langs = get_client_langs(&req);
|
||||
serve_admin_login(req, config, templates, client_langs).await
|
||||
}
|
||||
} else if let AdminQuery::Login(query) = req.body_form::<AdminQuery>().await? {
|
||||
if let Some(password_hash) = check_admin_password(config, &query.psw) {
|
||||
let client_langs = get_client_langs(&req);
|
||||
serve_admin(req, config, templates, dbs, &client_langs)
|
||||
.await
|
||||
.map(|mut r| {
|
||||
let mut cookie = tide::http::Cookie::new("admin", password_hash);
|
||||
cookie.set_http_only(Some(true));
|
||||
cookie.set_path(config.root_url.clone());
|
||||
if let Some(domain) = &config.cookies_domain {
|
||||
cookie.set_domain(domain.clone());
|
||||
}
|
||||
if config.cookies_https_only {
|
||||
cookie.set_secure(Some(true));
|
||||
}
|
||||
r.insert_cookie(cookie);
|
||||
r
|
||||
})
|
||||
} else {
|
||||
let client_langs = get_client_langs(&req);
|
||||
serve_admin_login(req, config, templates, client_langs).await
|
||||
}
|
||||
} else {
|
||||
let client_langs = get_client_langs(&req);
|
||||
serve_admin_login(req, config, templates, client_langs).await
|
||||
}
|
||||
}
|
|
@ -5,10 +5,13 @@ use std::path::Path;
|
|||
use tera::Tera;
|
||||
|
||||
static TEMPLATE_FILES: &[(&str, &str)] = &[
|
||||
("comments.html", include_str!("../templates/comments.html")),
|
||||
(
|
||||
"comments.html",
|
||||
include_str!("../../../templates/comments.html"),
|
||||
),
|
||||
(
|
||||
"admin_login.html",
|
||||
include_str!("../templates/admin_login.html"),
|
||||
include_str!("../../../templates/admin_login.html"),
|
||||
),
|
||||
];
|
||||
|
Loading…
Reference in a new issue