2022-10-15 15:32:57 +02:00
|
|
|
use crate::{config::*, db::*, helpers, queries::*, templates::*};
|
|
|
|
|
2022-10-15 17:32:35 +02:00
|
|
|
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
2022-10-15 23:48:06 +02:00
|
|
|
use crossbeam_channel::Sender;
|
2022-10-15 15:32:57 +02:00
|
|
|
use log::error;
|
|
|
|
use std::sync::Arc;
|
|
|
|
use tera::Context;
|
|
|
|
|
|
|
|
pub async fn start_server(config: Config, dbs: Dbs, templates: Templates) {
|
|
|
|
tide::log::start();
|
|
|
|
|
|
|
|
let templates = Arc::new(templates);
|
|
|
|
let config = Arc::new(config);
|
|
|
|
|
2022-10-15 23:48:06 +02:00
|
|
|
let (notify_send, notify_recv) = crossbeam_channel::bounded(10);
|
|
|
|
tokio::spawn(crate::notify::run_notifier(config.clone(), notify_recv));
|
|
|
|
|
2022-10-15 15:32:57 +02:00
|
|
|
let mut app = tide::new();
|
|
|
|
app.at(&format!("{}t/:topic", config.root_url)).get({
|
|
|
|
let config = config.clone();
|
|
|
|
let templates = templates.clone();
|
|
|
|
let dbs = dbs.clone();
|
|
|
|
move |req: tide::Request<()>| {
|
2022-10-20 20:17:49 +02:00
|
|
|
serve_comments(req, config.clone(), templates.clone(), dbs.clone(), &[])
|
2022-10-15 15:32:57 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
app.at(&format!("{}t/:topic", config.root_url)).post({
|
|
|
|
let config = config.clone();
|
|
|
|
let templates = templates.clone();
|
|
|
|
let dbs = dbs.clone();
|
|
|
|
move |req: tide::Request<()>| {
|
2022-10-15 23:48:06 +02:00
|
|
|
handle_post_comments(
|
|
|
|
req,
|
|
|
|
config.clone(),
|
|
|
|
templates.clone(),
|
|
|
|
dbs.clone(),
|
|
|
|
notify_send.clone(),
|
|
|
|
)
|
2022-10-15 15:32:57 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
app.at(&format!("{}admin", config.root_url)).get({
|
|
|
|
let config = config.clone();
|
|
|
|
let templates = templates.clone();
|
|
|
|
move |req: tide::Request<()>| serve_admin_login(req, config.clone(), templates.clone())
|
|
|
|
});
|
|
|
|
app.at(&format!("{}admin", config.root_url)).post({
|
|
|
|
let config = config.clone();
|
|
|
|
let templates = templates.clone();
|
|
|
|
let dbs = dbs.clone();
|
|
|
|
move |req: tide::Request<()>| {
|
|
|
|
handle_post_admin(req, config.clone(), templates.clone(), dbs.clone())
|
|
|
|
}
|
|
|
|
});
|
|
|
|
app.listen(config.listen).await.unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn serve_comments<'a>(
|
|
|
|
req: tide::Request<()>,
|
|
|
|
config: Arc<Config>,
|
|
|
|
templates: Arc<Templates>,
|
|
|
|
dbs: Dbs,
|
2022-10-20 20:17:49 +02:00
|
|
|
errors: &[String],
|
2022-10-15 15:32:57 +02:00
|
|
|
) -> 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| {
|
2022-10-15 17:32:35 +02:00
|
|
|
check_admin_password_hash(&config, &String::from(psw.value()))
|
2022-10-15 15:32:57 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
let topic_hash = TopicHash::from_topic(topic);
|
|
|
|
|
|
|
|
let mut context = Context::new();
|
|
|
|
context.insert("config", &config);
|
|
|
|
context.insert("admin", &admin);
|
2022-10-20 20:17:49 +02:00
|
|
|
context.insert("new_comment_errors", errors);
|
2022-10-15 15:32:57 +02:00
|
|
|
|
|
|
|
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::<RemoveQuery>() {
|
|
|
|
if let Ok(comment_id) = CommentId::from_base64(&query.remove) {
|
|
|
|
helpers::remove_pending_comment(comment_id, &dbs)
|
|
|
|
.map_err(|e| error!("Removing comment: {:?}", e))
|
|
|
|
.ok();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
context.insert(
|
|
|
|
"comments_pending",
|
|
|
|
&helpers::iter_pending_comments_by_topic(topic_hash.clone(), &dbs)
|
|
|
|
.map(|(comment_id, comment)| CommentWithId {
|
|
|
|
author: comment.author,
|
|
|
|
id: comment_id.to_base64(),
|
|
|
|
needs_approval: true,
|
|
|
|
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)| CommentWithId {
|
|
|
|
author: comment.author,
|
|
|
|
id: comment_id.to_base64(),
|
|
|
|
needs_approval: false,
|
|
|
|
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<'a>(
|
|
|
|
_req: tide::Request<()>,
|
|
|
|
config: Arc<Config>,
|
|
|
|
templates: Arc<Templates>,
|
|
|
|
dbs: Dbs,
|
|
|
|
) -> tide::Result<tide::Response> {
|
|
|
|
let mut context = Context::new();
|
|
|
|
context.insert("config", &config);
|
|
|
|
context.insert("admin", &true);
|
|
|
|
|
|
|
|
context.insert(
|
|
|
|
"comments",
|
|
|
|
&dbs.comment_pending
|
|
|
|
.iter()
|
|
|
|
.filter_map(|entry| {
|
2022-10-15 17:32:35 +02:00
|
|
|
let ((_topic_hash, _time, comment_id), ()) = entry
|
2022-10-15 15:32:57 +02:00
|
|
|
.map_err(|e| error!("Reading comment_pending: {:?}", e))
|
2022-10-15 17:32:35 +02:00
|
|
|
.ok()?;
|
2022-10-15 15:32:57 +02:00
|
|
|
let comment = dbs
|
|
|
|
.comment
|
|
|
|
.get(&comment_id)
|
|
|
|
.map_err(|e| error!("Reading comment: {:?}", e))
|
|
|
|
.ok()?
|
|
|
|
.or_else(|| {
|
|
|
|
error!("Comment not found");
|
|
|
|
None
|
|
|
|
})?;
|
|
|
|
Some(CommentWithId {
|
|
|
|
author: comment.author,
|
|
|
|
id: comment_id.to_base64(),
|
|
|
|
needs_approval: true,
|
|
|
|
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: Arc<Config>,
|
|
|
|
templates: Arc<Templates>,
|
|
|
|
) -> tide::Result<tide::Response> {
|
|
|
|
let mut context = Context::new();
|
|
|
|
context.insert("config", &config);
|
|
|
|
|
|
|
|
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: Arc<Config>,
|
|
|
|
templates: Arc<Templates>,
|
|
|
|
dbs: Dbs,
|
2022-10-15 23:48:06 +02:00
|
|
|
notify_send: Sender<()>,
|
2022-10-15 15:32:57 +02:00
|
|
|
) -> tide::Result<tide::Response> {
|
2022-10-20 20:17:49 +02:00
|
|
|
let mut errors = Vec::new();
|
|
|
|
|
2022-10-15 15:32:57 +02:00
|
|
|
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"))
|
|
|
|
};
|
|
|
|
|
|
|
|
if query.author.len() > config.comment_author_max_len {
|
2022-10-20 20:17:49 +02:00
|
|
|
errors.push(format!(
|
|
|
|
"Author name length is {} but maximum is {}.",
|
|
|
|
query.author.len(),
|
|
|
|
config.comment_author_max_len
|
|
|
|
));
|
2022-10-15 15:32:57 +02:00
|
|
|
}
|
|
|
|
if query.email.len() > config.comment_email_max_len {
|
2022-10-20 20:17:49 +02:00
|
|
|
errors.push(format!(
|
|
|
|
"E-mail length is {} but maximum is {}.",
|
|
|
|
query.email.len(),
|
|
|
|
config.comment_email_max_len
|
|
|
|
));
|
2022-10-15 15:32:57 +02:00
|
|
|
}
|
|
|
|
if query.text.len() > config.comment_text_max_len {
|
2022-10-20 20:17:49 +02:00
|
|
|
errors.push(format!(
|
|
|
|
"Comment length is {} but maximum is {}.",
|
|
|
|
query.text.len(),
|
|
|
|
config.comment_text_max_len
|
|
|
|
));
|
2022-10-15 15:32:57 +02:00
|
|
|
}
|
|
|
|
|
2022-10-20 20:17:49 +02:00
|
|
|
if errors.is_empty() {
|
|
|
|
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: query.author,
|
|
|
|
email: if query.email.is_empty() {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
Some(query.email)
|
|
|
|
},
|
|
|
|
last_edit_time: None,
|
|
|
|
post_time: time,
|
|
|
|
text: query.text,
|
|
|
|
};
|
|
|
|
helpers::new_pending_comment(&comment, &dbs)
|
|
|
|
.map_err(|e| error!("Adding pending comment: {:?}", e))
|
|
|
|
.ok();
|
|
|
|
notify_send.send(()).ok();
|
|
|
|
}
|
2022-10-15 15:32:57 +02:00
|
|
|
}
|
2022-10-20 20:17:49 +02:00
|
|
|
_ => {}
|
2022-10-15 15:32:57 +02:00
|
|
|
}
|
2022-10-20 20:17:49 +02:00
|
|
|
serve_comments(req, config, templates, dbs, &errors).await
|
2022-10-15 15:32:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async fn handle_post_admin(
|
|
|
|
mut req: tide::Request<()>,
|
|
|
|
config: Arc<Config>,
|
|
|
|
templates: Arc<Templates>,
|
|
|
|
dbs: Dbs,
|
|
|
|
) -> tide::Result<tide::Response> {
|
|
|
|
if let Some(psw) = req.cookie("admin") {
|
2022-10-15 17:32:35 +02:00
|
|
|
if check_admin_password(&config, &String::from(psw.value())).is_some() {
|
2022-10-15 15:32:57 +02:00
|
|
|
match req.body_form::<AdminQuery>().await? {
|
|
|
|
_ => serve_admin(req, config, templates, dbs).await,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
serve_admin_login(req, config, templates).await
|
|
|
|
}
|
|
|
|
} else if let AdminQuery::Login(query) = req.body_form::<AdminQuery>().await? {
|
2022-10-15 17:32:35 +02:00
|
|
|
if let Some(password_hash) = check_admin_password(&config, &query.psw) {
|
2022-10-15 15:32:57 +02:00
|
|
|
serve_admin(req, config.clone(), templates, dbs)
|
|
|
|
.await
|
|
|
|
.map(|mut r| {
|
2022-10-15 17:32:35 +02:00
|
|
|
let mut cookie = tide::http::Cookie::new("admin", password_hash);
|
2022-10-15 15:32:57 +02:00
|
|
|
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 {
|
|
|
|
serve_admin_login(req, config, templates).await
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
serve_admin_login(req, config, templates).await
|
|
|
|
}
|
|
|
|
}
|
2022-10-15 17:32:35 +02:00
|
|
|
|
|
|
|
fn check_admin_password(config: &Config, password: &str) -> Option<String> {
|
|
|
|
let argon2 = Argon2::default();
|
|
|
|
config
|
|
|
|
.admin_passwords
|
|
|
|
.iter()
|
|
|
|
.filter_map(|admin_password| PasswordHash::new(admin_password).ok())
|
|
|
|
.find(|admin_password| {
|
|
|
|
argon2
|
|
|
|
.verify_password(password.as_bytes(), admin_password)
|
|
|
|
.is_ok()
|
|
|
|
})
|
|
|
|
.map(|password_hash| password_hash.to_string())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn check_admin_password_hash(config: &Config, password_hash: &str) -> bool {
|
|
|
|
config.admin_passwords.iter().any(|h| h == password_hash)
|
|
|
|
}
|