diff --git a/site/src/api.rs b/site/src/api.rs new file mode 100644 index 0000000..546faa7 --- /dev/null +++ b/site/src/api.rs @@ -0,0 +1,97 @@ +use crate::db::*; +use crate::routes::{authorized, id_valid, replace_newlines}; +use actix_web::{get, http::StatusCode, post, web, web::Form, HttpResponse, Responder}; +use serde::Deserialize; + +#[derive(Deserialize)] +struct NewPostForm { + title: String, + body: String, + token: String, +} + +#[derive(Deserialize)] +struct BlogActionForm { + token: String, +} + +#[post("/api/blog/create")] +async fn blog_create_post(form: Form) -> impl Responder { + if authorized(&form.token) { + create_post(&form.title.as_str(), replace_newlines(&form.body).as_str()); + println!("New blog post created."); + } else { + println!("Unauthorized new blog post"); + } + + HttpResponse::MovedPermanently() + .set_header("LOCATION", "/blog") + .finish() +} + +#[post("/api/blog/posts/edit/{post_id}")] +async fn blog_edit_post( + web::Path(post_id): web::Path, + form: Form, +) -> impl Responder { + let (valid, id) = id_valid(post_id); + if valid && authorized(&form.token) { + edit_post_by_id( + id as i32, + &form.title.as_str(), + replace_newlines(&form.body).as_str(), + ); + println!("Edited post: {}", id); + } else { + println!("Unauthorized blog post edit."); + return HttpResponse::new(StatusCode::UNAUTHORIZED); + } + + return HttpResponse::MovedPermanently() + .set_header("LOCATION", "/blog") + .finish(); +} + +#[post("/api/blog/posts/delete/{post_id}")] +async fn blog_delete_post( + web::Path(post_id): web::Path, + form: Form, +) -> impl Responder { + let (valid, id) = id_valid(post_id); + if valid && authorized(&form.token) { + println!("Deleted post: {}", id); + delete_post_by_id(id as i32); + } else { + println!("Unauthorized blog post deletion."); + return HttpResponse::new(StatusCode::UNAUTHORIZED); + } + + return HttpResponse::MovedPermanently() + .set_header("LOCATION", "/blog") + .finish(); +} + +#[post("/api/blog/posts/hide/{post_id}")] +async fn blog_hide_post( + web::Path(post_id): web::Path, + form: Form, +) -> impl Responder { + let (valid, id) = id_valid(post_id); + if valid && authorized(&form.token) { + println!("Hid post: {}", id); + hide_post_by_id(id as i32); + } else { + println!("Unauthorized blog post hiding."); + return HttpResponse::new(StatusCode::UNAUTHORIZED); + } + + return HttpResponse::MovedPermanently() + .set_header("LOCATION", "/blog") + .finish(); +} + +#[get("/api/blog/posts")] +async fn blog_get_posts_json() -> impl Responder { + let posts = get_posts(); + HttpResponse::Ok().json(posts) +} diff --git a/site/src/db.rs b/site/src/db.rs index 87dfb7a..f12d702 100644 --- a/site/src/db.rs +++ b/site/src/db.rs @@ -14,7 +14,7 @@ fn establish_connection() -> SqliteConnection { } pub fn get_posts() -> std::vec::Vec { - use crate::db::schema::posts::dsl::*; + use schema::posts::dsl::*; let connection = establish_connection(); posts .filter(published.eq(true)) @@ -24,16 +24,16 @@ pub fn get_posts() -> std::vec::Vec { .expect("Error, couldn't load posts.") } -pub fn get_post_by_id(_id: i32) -> Post { - use crate::db::schema::posts::dsl::*; +pub fn get_post_by_id(post_id: i32) -> Post { + use schema::posts::dsl::*; let connection = establish_connection(); posts - .find(_id) + .find(post_id) .get_result(&connection) - .expect("Error, couldn't find post") + .expect("Error, couldn't find post.") } -pub fn add_post(title: &str, body: &str) { +pub fn create_post(title: &str, body: &str) { use chrono::prelude::*; use schema::posts; @@ -51,3 +51,34 @@ pub fn add_post(title: &str, body: &str) { .execute(&connection) .unwrap_or_else(|_| panic!("Error, couldn't insert new Post.")); } + +pub fn edit_post_by_id(post_id: i32, new_title: &str, new_body: &str) { + use schema::posts::dsl::*; + let connection = establish_connection(); + + diesel::update(posts) + .filter(id.eq(post_id)) + .set((title.eq(new_title), body.eq(new_body))) + .execute(&connection) + .expect("Error, couldn't update post."); +} + +pub fn delete_post_by_id(post_id: i32) { + use schema::posts::dsl::*; + let connection = establish_connection(); + + diesel::delete(posts.filter(id.eq(post_id))) + .execute(&connection) + .expect("Error, couldn't update post."); +} + +pub fn hide_post_by_id(post_id: i32) { + use schema::posts::dsl::*; + let connection = establish_connection(); + + diesel::update(posts) + .filter(id.eq(post_id)) + .set(published.eq(false)) + .execute(&connection) + .expect("Error, couldn't update post."); +} diff --git a/site/src/main.rs b/site/src/main.rs index 05e917e..0caf822 100644 --- a/site/src/main.rs +++ b/site/src/main.rs @@ -1,3 +1,4 @@ +mod api; mod config; mod db; mod routes; @@ -18,12 +19,17 @@ async fn main() -> std::io::Result<()> { let root_path = get_from_env("ROOT_PATH", true); App::new() - //.wrap(middleware::NormalizePath::default()) .service(routes::root) .service(routes::blog) - .service(routes::blog_permalink) + .service(routes::blog_by_id) .service(routes::blog_submit) - .service(routes::blog_new_post) + .service(routes::blog_edit) + .service(routes::blog_edit_by_id) + .service(api::blog_get_posts_json) + .service(api::blog_create_post) + .service(api::blog_edit_post) + .service(api::blog_hide_post) + .service(api::blog_delete_post) .service(fs::Files::new("/static", root_path + "/static")) }) .bind(String::from("localhost:") + &get_from_env("BIND_PORT", true))? diff --git a/site/src/routes.rs b/site/src/routes.rs index 5c1fbe5..9e434c5 100644 --- a/site/src/routes.rs +++ b/site/src/routes.rs @@ -2,10 +2,38 @@ use crate::config; use crate::db::*; use actix_files as fs; -use actix_web::{get, http::StatusCode, post, web, web::Form, HttpResponse, Responder}; -use serde::Deserialize; +use actix_web::{get, http::StatusCode, web, HttpResponse, Responder}; use tera::{Context, Tera}; +pub fn authorized(form_token: &str) -> bool { + let token = config::get_from_env("SUBMIT_TOKEN", true); + if token == form_token { + return true; + } + false +} + +pub fn id_valid(post_id: String) -> (bool, i32) { + match post_id.parse::() { + Err(_) => (false, 0), + Ok(id) => { + if id < 1 { + (false, id) + } else { + (true, id) + } + } + } +} + +pub fn replace_newlines(x: &String) -> String { + x.replace("\n", "
") +} + +pub fn replace_br_tags(x: &String) -> String { + x.replace("
", "\n") +} + #[get("/")] async fn root() -> impl Responder { let root_path = config::get_from_env("ROOT_PATH", true); @@ -36,62 +64,82 @@ async fn blog() -> impl Responder { #[get("/blog/submit")] async fn blog_submit() -> impl Responder { let root_path = config::get_from_env("ROOT_PATH", true); - HttpResponse::Ok() - .content_type("text/html") - .set_header("SameSite", "secure") - .body( - std::fs::read_to_string(root_path + "/html/submit.html") - .unwrap_or_else(|e| panic!("Error, couldn't load submit html file.\n{}", e)), - ) + + let mut context = Context::new(); + context.insert("title", ""); + context.insert("body", ""); + + // one-off render blog template with context + let result = Tera::one_off( + &(std::fs::read_to_string(root_path + "/templates/post-submit.html") + .unwrap_or_else(|e| panic!("Error, couldn't load blog template.\n{}", e)) + .as_str()), + &context, + false, + ) + .unwrap_or_else(|e| panic!("Error, couldn't render blog template.\n{}", e)); + HttpResponse::Ok().content_type("text/html").body(result) } #[get("/blog/id/{post_id}")] -async fn blog_permalink(web::Path(post_id): web::Path) -> impl Responder { - match post_id.parse::() { - Err(_) => HttpResponse::new(StatusCode::NOT_FOUND), - Ok(i) => { - let root_path = config::get_from_env("ROOT_PATH", true); +async fn blog_by_id(web::Path(post_id): web::Path) -> impl Responder { + let (valid, id) = id_valid(post_id); + if valid { + let root_path = config::get_from_env("ROOT_PATH", true); - let post = get_post_by_id(i as i32); + let post = get_post_by_id(id as i32); - let mut context = Context::new(); - context.insert("posts", &[post]); + let mut context = Context::new(); + context.insert("posts", &[post]); - // one-off render blog template with context - let result = Tera::one_off( - &(std::fs::read_to_string(root_path + "/templates/blog.html") - .unwrap_or_else(|e| panic!("Error, couldn't load blog template.\n{}", e)) - .as_str()), - &context, - false, - ) - .unwrap_or_else(|e| panic!("Error, couldn't render blog template.\n{}", e)); - HttpResponse::Ok().content_type("text/html").body(result) - } - } -} - -#[derive(Deserialize)] -struct NewPostForm { - title: String, - body: String, - token: String, -} - -#[post("/blog/posts/new")] -async fn blog_new_post(form: Form) -> impl Responder { - let token = config::get_from_env("SUBMIT_TOKEN", true); - - let replace_newlines = |x: &String| x.replace("\n", "
"); - - if form.token == token { - add_post(&form.title.as_str(), replace_newlines(&form.body).as_str()); - println!("New blog post created."); + // one-off render blog template with context + let result = Tera::one_off( + &(std::fs::read_to_string(root_path + "/templates/blog.html") + .unwrap_or_else(|e| panic!("Error, couldn't load blog template.\n{}", e)) + .as_str()), + &context, + false, + ) + .unwrap_or_else(|e| panic!("Error, couldn't render blog template.\n{}", e)); + return HttpResponse::Ok().content_type("text/html").body(result); } else { - println!("Unauthorized new blog post"); + return HttpResponse::new(StatusCode::NOT_FOUND); + } +} + +#[get("/blog/edit")] +async fn blog_edit() -> impl Responder { + "edit" +} + +#[get("/blog/edit/{post_id}")] +async fn blog_edit_by_id(web::Path(post_id): web::Path) -> impl Responder { + let (valid, id) = id_valid(post_id); + if valid { + let root_path = config::get_from_env("ROOT_PATH", true); + + let mut post = get_post_by_id(id as i32); + + post.title = replace_br_tags(&post.title); + post.body = replace_br_tags(&post.body); + + let mut context = Context::new(); + context.insert("title", &post.title); + context.insert("body", &post.body); + context.insert("id", &id); + + // one-off render blog template with context + let result = Tera::one_off( + &(std::fs::read_to_string(root_path + "/templates/post-edit.html") + .unwrap_or_else(|e| panic!("Error, couldn't load blog template.\n{}", e)) + .as_str()), + &context, + false, + ) + .unwrap_or_else(|e| panic!("Error, couldn't render submit template.\n{}", e)); + + return HttpResponse::Ok().content_type("text/html").body(result); + } else { + return HttpResponse::new(StatusCode::UNAUTHORIZED); } - - HttpResponse::MovedPermanently() - .set_header("LOCATION", "/blog") - .finish() }