From 89ebbf9269f161047c5545e8cf1d19e0f094aeda Mon Sep 17 00:00:00 2001 From: Imbus Date: Tue, 28 Nov 2023 01:26:19 +0100 Subject: [PATCH] Added routes and database logic to handle comments --- ...f8609a54f8ba99bc55f208fb01cda5dd219f7.json | 72 ++++++++++++++ ...6e89a9851b372b39d68a10c16961acd968eef.json | 17 ++++ server/src/db.rs | 46 ++++++++- server/src/main.rs | 4 +- server/src/routes/comment.rs | 97 +++++++++++++++++++ server/src/routes/mod.rs | 2 + 6 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 server/.sqlx/query-345472dbe81319923bf40fc39a1f8609a54f8ba99bc55f208fb01cda5dd219f7.json create mode 100644 server/.sqlx/query-fe72509852c87463cea9775d9606e89a9851b372b39d68a10c16961acd968eef.json create mode 100644 server/src/routes/comment.rs diff --git a/server/.sqlx/query-345472dbe81319923bf40fc39a1f8609a54f8ba99bc55f208fb01cda5dd219f7.json b/server/.sqlx/query-345472dbe81319923bf40fc39a1f8609a54f8ba99bc55f208fb01cda5dd219f7.json new file mode 100644 index 0000000..80b1d33 --- /dev/null +++ b/server/.sqlx/query-345472dbe81319923bf40fc39a1f8609a54f8ba99bc55f208fb01cda5dd219f7.json @@ -0,0 +1,72 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM comments WHERE parent_post_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "parent_post_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "parent_comment_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "author_user_id", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "content", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "upvotes", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "downvotes", + "type_info": "Int4" + }, + { + "ordinal": 7, + "name": "created_at", + "type_info": "Timestamp" + }, + { + "ordinal": 8, + "name": "updated_at", + "type_info": "Timestamp" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "345472dbe81319923bf40fc39a1f8609a54f8ba99bc55f208fb01cda5dd219f7" +} diff --git a/server/.sqlx/query-fe72509852c87463cea9775d9606e89a9851b372b39d68a10c16961acd968eef.json b/server/.sqlx/query-fe72509852c87463cea9775d9606e89a9851b372b39d68a10c16961acd968eef.json new file mode 100644 index 0000000..c95db9c --- /dev/null +++ b/server/.sqlx/query-fe72509852c87463cea9775d9606e89a9851b372b39d68a10c16961acd968eef.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO comments (parent_post_id, parent_comment_id, author_user_id, content) VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int8", + "Text" + ] + }, + "nullable": [] + }, + "hash": "fe72509852c87463cea9775d9606e89a9851b372b39d68a10c16961acd968eef" +} diff --git a/server/src/db.rs b/server/src/db.rs index 3c52b93..1baed3e 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -1,4 +1,4 @@ -use crate::routes::{Post, User}; +use crate::routes::{Comment, Post, User}; use argon2::{ password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHasher, PasswordVerifier, @@ -6,6 +6,50 @@ use argon2::{ use log::{info, warn}; use sqlx::PgPool; +pub async fn db_new_comment( + pool: &PgPool, + parent_post_id: i64, + parent_comment_id: Option, + user_id: i64, + content: &str, +) -> bool { + let insert_query = sqlx::query!( + "INSERT INTO comments (parent_post_id, parent_comment_id, author_user_id, content) VALUES ($1, $2, $3, $4)", + parent_post_id, + parent_comment_id.unwrap_or(-1), // This is a bit of a hack + user_id, + content + ) + .execute(pool) + .await; + + if insert_query.is_err() { + let s = insert_query.err().unwrap(); + warn!("Error inserting comment into database: {}", s); + return false; + } + + true +} + +pub async fn db_get_comments( + pool: &PgPool, + parent_post_id: i64, + limit: i64, + offset: i64, +) -> Vec { + sqlx::query_as!( + Comment, + "SELECT * FROM comments WHERE parent_post_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", + parent_post_id, + limit, + offset + ) + .fetch_all(pool) + .await + .unwrap() +} + /// Gets the latest posts from the database, ordered by created_at pub async fn db_get_latest_posts(pool: &PgPool, limit: i64, offset: i64) -> Vec { sqlx::query_as!( diff --git a/server/src/main.rs b/server/src/main.rs index beba099..e57aaf0 100755 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -10,7 +10,7 @@ mod routes; mod state; mod util; -use routes::{get_posts, login, new_post, post_by_id, register}; +use routes::{get_comments, get_posts, login, new_comment, new_post, post_by_id, register}; use state::CaptchaState; use state::ServerState; use util::hex_string; @@ -41,6 +41,8 @@ async fn main() -> std::io::Result<()> { scope("/api") .service(get_posts) .service(new_post) + .service(new_comment) + .service(get_comments) .service(post_by_id) .service(login) .service(register) diff --git a/server/src/routes/comment.rs b/server/src/routes/comment.rs new file mode 100644 index 0000000..e67a2a2 --- /dev/null +++ b/server/src/routes/comment.rs @@ -0,0 +1,97 @@ +use crate::db::{db_get_comments, db_new_comment}; +use crate::jwt::validate_token; +use crate::ServerState; + +use actix_web::get; +use actix_web::web::{Data, Query}; +use actix_web::{post, web::Json, HttpResponse, Responder, Result}; +use log::info; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct NewComment { + pub parent_post_id: i64, + pub parent_comment_id: Option, + pub user_token: String, + pub content: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] +pub struct Comment { + pub id: i64, + pub parent_post_id: i64, + pub parent_comment_id: Option, + pub author_user_id: i64, + pub upvotes: i64, + pub downvotes: i64, + pub content: String, + pub created_at: chrono::NaiveDateTime, + pub updated_at: chrono::NaiveDateTime, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CommentQueryParams { + post_id: i64, + limit: Option, + offset: Option, +} + +#[get("/comments")] +pub async fn get_comments( + commentiflter: Query, + state: Data, +) -> Result { + let post_id = commentiflter.post_id; + let limit = commentiflter.limit.unwrap_or(10); + let offset = commentiflter.offset.unwrap_or(0); + + info!( + "Getting comments for post {} with limit {} and offset {}", + post_id, limit, offset + ); + + let comments = db_get_comments(&state.pool, post_id, limit, offset).await; + + Ok(HttpResponse::Ok().json(comments)) +} + +#[post("/comments")] +pub async fn new_comment( + data: Json, + state: Data, +) -> Result { + let user_claims = validate_token(&data.user_token); + + // Bail if the token is invalid + if let Err(e) = user_claims { + info!("Error validating token: {}", e); + return Ok(HttpResponse::BadRequest().json("Error")); + } + + let claims = user_claims.unwrap(); + info!("User {:?} created a new comment", &claims.sub); + + let content = data.content.clone(); + let username = claims.sub.clone(); + + // This one is avoidable if we just store the user id in the token + let userid = sqlx::query!("SELECT id FROM users WHERE username = $1", username) + .fetch_one(&state.pool) + .await + .unwrap() + .id; + + let success = db_new_comment( + &state.pool, + data.parent_post_id, + data.parent_comment_id, + userid, + &content, + ) + .await; + + match success { + true => Ok(HttpResponse::Ok().json("Success")), + false => Ok(HttpResponse::BadRequest().json("Error")), + } +} diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index c02003b..b16b7fb 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -1,5 +1,7 @@ +mod comment; mod post; mod users; +pub use comment::*; pub use post::*; pub use users::*;