diff options
author | Tyler Murphy <tylermurphy534@gmail.com> | 2023-02-12 14:11:50 -0500 |
---|---|---|
committer | Tyler Murphy <tylermurphy534@gmail.com> | 2023-02-12 14:11:50 -0500 |
commit | 3d71da490947aacc52a3b77efdc13d5f0458c57f (patch) | |
tree | 8047eb6966655cffc772cbde4d73982fb7064a28 /src | |
parent | docs is ssr'd (diff) | |
download | xssbook-3d71da490947aacc52a3b77efdc13d5f0458c57f.tar.gz xssbook-3d71da490947aacc52a3b77efdc13d5f0458c57f.tar.bz2 xssbook-3d71da490947aacc52a3b77efdc13d5f0458c57f.zip |
refactor
Diffstat (limited to 'src')
-rw-r--r-- | src/api/admin.rs | 63 | ||||
-rw-r--r-- | src/api/auth.rs | 34 | ||||
-rw-r--r-- | src/api/posts.rs | 120 | ||||
-rw-r--r-- | src/api/users.rs | 35 | ||||
-rw-r--r-- | src/database/comments.rs | 91 | ||||
-rw-r--r-- | src/database/likes.rs | 77 | ||||
-rw-r--r-- | src/database/mod.rs | 4 | ||||
-rw-r--r-- | src/database/posts.rs | 54 | ||||
-rw-r--r-- | src/database/users.rs | 7 | ||||
-rw-r--r-- | src/public/admin.rs | 64 | ||||
-rw-r--r-- | src/public/docs.rs | 96 | ||||
-rw-r--r-- | src/types/comment.rs | 44 | ||||
-rw-r--r-- | src/types/like.rs | 48 | ||||
-rw-r--r-- | src/types/mod.rs | 2 | ||||
-rw-r--r-- | src/types/post.rs | 38 |
15 files changed, 580 insertions, 197 deletions
diff --git a/src/api/admin.rs b/src/api/admin.rs index 8db3032..6030315 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -6,7 +6,10 @@ use tower_cookies::{Cookie, Cookies}; use crate::{ database, - public::{admin, docs::{EndpointDocumentation, EndpointMethod}}, + public::{ + admin, + docs::{EndpointDocumentation, EndpointMethod}, + }, types::{ extract::{AdminUser, Check, CheckResult, Json}, http::ResponseCode, @@ -17,14 +20,16 @@ pub const ADMIN_AUTH: EndpointDocumentation = EndpointDocumentation { uri: "/api/admin/auth", method: EndpointMethod::Post, description: "Authenticates on the admin panel", - body: Some(r#" + body: Some( + r#" { "secret" : "admin" } - "#), + "#, + ), responses: &[ (200, "Successfully executed SQL query"), - (400, " Successfully authed, admin cookie returned") + (400, " Successfully authed, admin cookie returned"), ], cookie: None, }; @@ -60,16 +65,18 @@ pub const ADMIN_QUERY: EndpointDocumentation = EndpointDocumentation { uri: "/api/admin/query", method: EndpointMethod::Post, description: "Run a SQL query on the database", - body: Some(r#" + body: Some( + r#" { "query" : "DROP TABLE users;" } - "#), + "#, + ), responses: &[ (200, "Successfully executed SQL query"), (400, "Body does not match parameters"), (401, "Unauthorized"), - (500, "SQL query ran into an error") + (500, "SQL query ran into an error"), ], cookie: Some("admin"), }; @@ -102,7 +109,7 @@ pub const ADMIN_POSTS: EndpointDocumentation = EndpointDocumentation { responses: &[ (200, "Returns sql table in <span>text/html</span>"), (401, "Unauthorized"), - (500, "Failed to fetch data") + (500, "Failed to fetch data"), ], cookie: Some("admin"), }; @@ -119,7 +126,7 @@ pub const ADMIN_USERS: EndpointDocumentation = EndpointDocumentation { responses: &[ (200, "Returns sql table in <span>text/html</span>"), (401, "Unauthorized"), - (500, "Failed to fetch data") + (500, "Failed to fetch data"), ], cookie: Some("admin"), }; @@ -136,7 +143,7 @@ pub const ADMIN_SESSIONS: EndpointDocumentation = EndpointDocumentation { responses: &[ (200, "Returns sql table in <span>text/html</span>"), (401, "Unauthorized"), - (500, "Failed to fetch data") + (500, "Failed to fetch data"), ], cookie: Some("admin"), }; @@ -145,6 +152,40 @@ async fn sessions(_: AdminUser) -> Response { admin::generate_sessions() } +pub const ADMIN_COMMENTS: EndpointDocumentation = EndpointDocumentation { + uri: "/api/admin/comments", + method: EndpointMethod::Post, + description: "Returns the entire comments table", + body: None, + responses: &[ + (200, "Returns sql table in <span>text/html</span>"), + (401, "Unauthorized"), + (500, "Failed to fetch data"), + ], + cookie: Some("admin"), +}; + +async fn comments(_: AdminUser) -> Response { + admin::generate_comments() +} + +pub const ADMIN_LIKES: EndpointDocumentation = EndpointDocumentation { + uri: "/api/admin/likes", + method: EndpointMethod::Post, + description: "Returns the entire likes table", + body: None, + responses: &[ + (200, "Returns sql table in <span>text/html</span>"), + (401, "Unauthorized"), + (500, "Failed to fetch data"), + ], + cookie: Some("admin"), +}; + +async fn likes(_: AdminUser) -> Response { + admin::generate_likes() +} + async fn check(check: Option<AdminUser>) -> Response { if check.is_none() { ResponseCode::Success.text("false") @@ -160,5 +201,7 @@ pub fn router() -> Router { .route("/posts", post(posts)) .route("/users", post(users)) .route("/sessions", post(sessions)) + .route("/comments", post(comments)) + .route("/likes", post(likes)) .route("/check", post(check)) } diff --git a/src/api/auth.rs b/src/api/auth.rs index 0ff180e..60ddc80 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -3,18 +3,22 @@ use serde::Deserialize; use time::{Duration, OffsetDateTime}; use tower_cookies::{Cookie, Cookies}; -use crate::{types::{ - extract::{AuthorizedUser, Check, CheckResult, Json, Log}, - http::ResponseCode, - session::Session, - user::User, -}, public::docs::{EndpointDocumentation, EndpointMethod}}; +use crate::{ + public::docs::{EndpointDocumentation, EndpointMethod}, + types::{ + extract::{AuthorizedUser, Check, CheckResult, Json, Log}, + http::ResponseCode, + session::Session, + user::User, + }, +}; pub const AUTH_REGISTER: EndpointDocumentation = EndpointDocumentation { uri: "/api/auth/register", method: EndpointMethod::Post, description: "Registeres a new account", - body: Some(r#" + body: Some( + r#" { "firstname": "[Object]", "lastname": "object]", @@ -25,7 +29,8 @@ pub const AUTH_REGISTER: EndpointDocumentation = EndpointDocumentation { "month": 1, "year": 1970 } - "#), + "#, + ), responses: &[ (201, "Successfully registered new user"), (400, "Body does not match parameters"), @@ -123,15 +128,20 @@ pub const AUTH_LOGIN: EndpointDocumentation = EndpointDocumentation { uri: "/api/auth/login", method: EndpointMethod::Post, description: "Logs into an existing account", - body: Some(r#" + body: Some( + r#" { "email": "object@object.object", "password": "i love js" } - "#), + "#, + ), responses: &[ (200, "Successfully logged in, auth cookie is returned"), - (400, "Body does not match parameters, or invalid email password combination"), + ( + 400, + "Body does not match parameters, or invalid email password combination", + ), ], cookie: None, }; @@ -184,7 +194,7 @@ pub const AUTH_LOGOUT: EndpointDocumentation = EndpointDocumentation { responses: &[ (200, "Successfully logged out"), (401, "Unauthorized"), - (500, "Failed to log out user") + (500, "Failed to log out user"), ], cookie: None, }; diff --git a/src/api/posts.rs b/src/api/posts.rs index f1cdab3..ca459cd 100644 --- a/src/api/posts.rs +++ b/src/api/posts.rs @@ -5,26 +5,33 @@ use axum::{ }; use serde::Deserialize; -use crate::{types::{ - extract::{AuthorizedUser, Check, CheckResult, Json}, - http::ResponseCode, - post::Post, -}, public::docs::{EndpointDocumentation, EndpointMethod}}; +use crate::{ + public::docs::{EndpointDocumentation, EndpointMethod}, + types::{ + comment::Comment, + extract::{AuthorizedUser, Check, CheckResult, Json}, + http::ResponseCode, + like::Like, + post::Post, + }, +}; pub const POSTS_CREATE: EndpointDocumentation = EndpointDocumentation { uri: "/api/posts/create", method: EndpointMethod::Post, description: "Creates a new post", - body: Some(r#" + body: Some( + r#" { "content" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." } - "#), + "#, + ), responses: &[ (201, "Successfully created post"), (400, "Body does not match parameters"), (401, "Unauthorized"), - (500, "Failed to create post") + (500, "Failed to create post"), ], cookie: Some("auth"), }; @@ -65,16 +72,18 @@ pub const POSTS_PAGE: EndpointDocumentation = EndpointDocumentation { uri: "/api/posts/page", method: EndpointMethod::Post, description: "Load a section of posts from newest to oldest", - body: Some(r#" + body: Some( + r#" { "page": 0 } - "#), + "#, + ), responses: &[ (200, "Returns posts in <span>application/json<span>"), (400, "Body does not match parameters"), (401, "Unauthorized"), - (500, "Failed to fetch posts") + (500, "Failed to fetch posts"), ], cookie: Some("auth"), }; @@ -105,21 +114,71 @@ async fn page( ResponseCode::Success.json(&json) } +pub const COMMENTS_PAGE: EndpointDocumentation = EndpointDocumentation { + uri: "/api/posts/comments", + method: EndpointMethod::Post, + description: "Load a section of comments from newest to oldest", + body: Some( + r#" + { + "page": 1, + "post_id": 13 + } + "#, + ), + responses: &[ + (200, "Returns comments in <span>application/json<span>"), + (400, "Body does not match parameters"), + (401, "Unauthorized"), + (500, "Failed to fetch comments"), + ], + cookie: Some("auth"), +}; + +#[derive(Deserialize)] +struct CommentsPageRequest { + page: u64, + post_id: u64 +} + +impl Check for CommentsPageRequest { + fn check(&self) -> CheckResult { + Ok(()) + } +} + +async fn comments( + AuthorizedUser(_user): AuthorizedUser, + Json(body): Json<CommentsPageRequest>, +) -> Response { + let Ok(comments) = Comment::from_comment_page(body.page, body.post_id) else { + return ResponseCode::InternalServerError.text("Failed to fetch comments") + }; + + let Ok(json) = serde_json::to_string(&comments) else { + return ResponseCode::InternalServerError.text("Failed to fetch comments") + }; + + ResponseCode::Success.json(&json) +} + pub const POSTS_USER: EndpointDocumentation = EndpointDocumentation { uri: "/api/posts/user", method: EndpointMethod::Post, description: "Load a section of posts from newest to oldest from a specific user", - body: Some(r#" + body: Some( + r#" { "user_id": 3, "page": 0 } - "#), + "#, + ), responses: &[ (200, "Returns posts in <span>application/json<span>"), (400, "Body does not match parameters"), (401, "Unauthorized"), - (500, "Failed to fetch posts") + (500, "Failed to fetch posts"), ], cookie: Some("auth"), }; @@ -155,17 +214,19 @@ pub const POSTS_COMMENT: EndpointDocumentation = EndpointDocumentation { uri: "/api/posts/comment", method: EndpointMethod::Patch, description: "Add a comment to a post", - body: Some(r#" + body: Some( + r#" { "content": "This is a very cool comment", "post_id": 0 } - "#), + "#, + ), responses: &[ (200, "Successfully added comment"), (400, "Body does not match parameters"), (401, "Unauthorized"), - (500, "Failed to add comment") + (500, "Failed to add comment"), ], cookie: Some("auth"), }; @@ -192,11 +253,7 @@ async fn comment( AuthorizedUser(user): AuthorizedUser, Json(body): Json<PostCommentRequest>, ) -> Response { - let Ok(mut post) = Post::from_post_id(body.post_id) else { - return ResponseCode::InternalServerError.text("Failed to add comment") - }; - - if let Err(err) = post.comment(user.user_id, body.content) { + if let Err(err) = Comment::new(user.user_id, body.post_id, &body.content) { return err; } @@ -207,17 +264,19 @@ pub const POSTS_LIKE: EndpointDocumentation = EndpointDocumentation { uri: "/api/posts/like", method: EndpointMethod::Patch, description: "Set like status on a post", - body: Some(r#" + body: Some( + r#" { "post_id" : 0, "status" : true } - "#), + "#, + ), responses: &[ (200, "Successfully set like status"), (400, "Body does not match parameters"), (401, "Unauthorized"), - (500, "Failed to set like status") + (500, "Failed to set like status"), ], cookie: Some("auth"), }; @@ -235,11 +294,11 @@ impl Check for PostLikeRequest { } async fn like(AuthorizedUser(user): AuthorizedUser, Json(body): Json<PostLikeRequest>) -> Response { - let Ok(mut post) = Post::from_post_id(body.post_id) else { - return ResponseCode::InternalServerError.text("Failed to fetch posts") - }; - - if let Err(err) = post.like(user.user_id, body.state) { + if body.state { + if let Err(err) = Like::add_liked(user.user_id, body.post_id) { + return err; + } + } else if let Err(err) = Like::remove_liked(user.user_id, body.post_id) { return err; } @@ -250,6 +309,7 @@ pub fn router() -> Router { Router::new() .route("/create", post(create)) .route("/page", post(page)) + .route("/comments", post(comments)) .route("/user", post(user)) .route("/comment", patch(comment)) .route("/like", patch(like)) diff --git a/src/api/users.rs b/src/api/users.rs index 7d1f006..0ce9988 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -1,8 +1,11 @@ -use crate::{types::{ - extract::{AuthorizedUser, Check, CheckResult, Json, Png}, - http::ResponseCode, - user::User, -}, public::docs::{EndpointDocumentation, EndpointMethod}}; +use crate::{ + public::docs::{EndpointDocumentation, EndpointMethod}, + types::{ + extract::{AuthorizedUser, Check, CheckResult, Json, Png}, + http::ResponseCode, + user::User, + }, +}; use axum::{ response::Response, routing::{post, put}, @@ -14,16 +17,18 @@ pub const USERS_LOAD: EndpointDocumentation = EndpointDocumentation { uri: "/api/users/load", method: EndpointMethod::Post, description: "Loads a requested set of users", - body: Some(r#" + body: Some( + r#" { "ids": [0, 3, 7] } - "#), + "#, + ), responses: &[ (200, "Returns users in <span>application/json</span>"), (400, "Body does not match parameters"), (401, "Unauthorized"), - (500, "Failed to fetch users") + (500, "Failed to fetch users"), ], cookie: Some("auth"), }; @@ -55,17 +60,19 @@ pub const USERS_PAGE: EndpointDocumentation = EndpointDocumentation { uri: "/api/users/page", method: EndpointMethod::Post, description: "Load a section of users from newest to oldest", - body: Some(r#" + body: Some( + r#" { "user_id": 3, "page": 0 } - "#), + "#, + ), responses: &[ (200, "Returns users in <span>application/json</span>"), (400, "Body does not match parameters"), (401, "Unauthorized"), - (500, "Failed to fetch users") + (500, "Failed to fetch users"), ], cookie: Some("auth"), }; @@ -104,7 +111,7 @@ pub const USERS_SELF: EndpointDocumentation = EndpointDocumentation { responses: &[ (200, "Successfully executed SQL query"), (401, "Unauthorized"), - (500, "Failed to fetch user") + (500, "Failed to fetch user"), ], cookie: Some("auth"), }; @@ -126,7 +133,7 @@ pub const USERS_AVATAR: EndpointDocumentation = EndpointDocumentation { (200, "Successfully updated avatar"), (400, "Invalid PNG or disallowed size"), (401, "Unauthorized"), - (500, "Failed to update avatar") + (500, "Failed to update avatar"), ], cookie: Some("auth"), }; @@ -150,7 +157,7 @@ pub const USERS_BANNER: EndpointDocumentation = EndpointDocumentation { (200, "Successfully updated banner"), (400, "Invalid PNG or disallowed size"), (401, "Unauthorized"), - (500, "Failed to update banner") + (500, "Failed to update banner"), ], cookie: Some("auth"), }; diff --git a/src/database/comments.rs b/src/database/comments.rs new file mode 100644 index 0000000..9e0eaf9 --- /dev/null +++ b/src/database/comments.rs @@ -0,0 +1,91 @@ +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use rusqlite::Row; +use tracing::instrument; + +use crate::{database, types::comment::Comment}; + +pub fn init() -> Result<(), rusqlite::Error> { + let sql = " + CREATE TABLE IF NOT EXISTS comments ( + comment_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + post_id INTEGER NOT NULL, + date INTEGER NOT NULL, + content VARCHAR(255) NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(user_id), + FOREIGN KEY(post_id) REFERENCES posts(post_id) + ); + "; + let conn = database::connect()?; + conn.execute(sql, ())?; + + let sql2 = "CREATE INDEX IF NOT EXISTS post_ids on comments (post_id);"; + conn.execute(sql2, ())?; + + Ok(()) +} + +fn comment_from_row(row: &Row) -> Result<Comment, rusqlite::Error> { + let comment_id = row.get(0)?; + let user_id = row.get(1)?; + let post_id = row.get(2)?; + let date = row.get(3)?; + let content = row.get(4)?; + + Ok(Comment { + comment_id, + user_id, + post_id, + date, + content, + }) +} + +#[instrument()] +pub fn get_comments_page(page: u64, post_id: u64) -> Result<Vec<Comment>, rusqlite::Error> { + tracing::trace!("Retrieving comments page"); + let page_size = 5; + let conn = database::connect()?; + let mut stmt = conn.prepare( + "SELECT * FROM comments WHERE post_id = ? ORDER BY comment_id ASC LIMIT ? OFFSET ?", + )?; + let row = stmt.query_map([post_id, page_size, page_size * page], |row| { + let row = comment_from_row(row)?; + Ok(row) + })?; + Ok(row.into_iter().flatten().collect()) +} + +#[instrument()] +pub fn get_all_comments() -> Result<Vec<Comment>, rusqlite::Error> { + tracing::trace!("Retrieving comments page"); + let conn = database::connect()?; + let mut stmt = conn.prepare("SELECT * FROM comments ORDER BY comment_id DESC")?; + let row = stmt.query_map([], |row| { + let row = comment_from_row(row)?; + Ok(row) + })?; + Ok(row.into_iter().flatten().collect()) +} + +#[instrument()] +pub fn add_comment(user_id: u64, post_id: u64, content: &str) -> Result<Comment, rusqlite::Error> { + tracing::trace!("Adding comment"); + let date = u64::try_from( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::ZERO) + .as_millis(), + ) + .unwrap_or(0); + let conn = database::connect()?; + let mut stmt = conn.prepare( + "INSERT INTO comments (user_id, post_id, date, content) VALUES(?,?,?,?) RETURNING *;", + )?; + let post = stmt.query_row((user_id, post_id, date, content), |row| { + let row = comment_from_row(row)?; + Ok(row) + })?; + Ok(post) +} diff --git a/src/database/likes.rs b/src/database/likes.rs new file mode 100644 index 0000000..6f6939e --- /dev/null +++ b/src/database/likes.rs @@ -0,0 +1,77 @@ +use rusqlite::OptionalExtension; +use tracing::instrument; + +use crate::{database, types::like::Like}; + +pub fn init() -> Result<(), rusqlite::Error> { + let sql = " + CREATE TABLE IF NOT EXISTS likes ( + user_id INTEGER NOT NULL, + post_id INTEGER NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(user_id), + FOREIGN KEY(post_id) REFERENCES posts(post_id), + PRIMARY KEY (user_id, post_id) + ); + "; + let conn = database::connect()?; + conn.execute(sql, ())?; + Ok(()) +} + +#[instrument()] +pub fn get_like_count(post_id: u64) -> Result<Option<u64>, rusqlite::Error> { + tracing::trace!("Retrieving like count"); + let conn = database::connect()?; + let mut stmt = conn.prepare("SELECT COUNT(post_id) FROM likes WHERE post_id = ?")?; + let row = stmt + .query_row([post_id], |row| { + let row = row.get(0)?; + Ok(row) + }) + .optional()?; + Ok(row) +} + +#[instrument()] +pub fn get_liked(user_id: u64, post_id: u64) -> Result<bool, rusqlite::Error> { + tracing::trace!("Retrieving if liked"); + let conn = database::connect()?; + let mut stmt = conn.prepare("SELECT * FROM likes WHERE user_id = ? AND post_id = ?")?; + let liked = stmt.query_row([user_id, post_id], |_| { + Ok(()) + }).optional()?; + Ok(liked.is_some()) +} + +#[instrument()] +pub fn add_liked(user_id: u64, post_id: u64) -> Result<bool, rusqlite::Error> { + tracing::trace!("Adding like"); + let conn = database::connect()?; + let mut stmt = conn.prepare("INSERT OR REPLACE INTO likes (user_id, post_id) VALUES (?,?)")?; + let changes = stmt.execute([user_id, post_id])?; + Ok(changes == 1) +} + +#[instrument()] +pub fn remove_liked(user_id: u64, post_id: u64) -> Result<bool, rusqlite::Error> { + tracing::trace!("Removing like"); + let conn = database::connect()?; + let mut stmt = conn.prepare("DELETE FROM likes WHERE user_id = ? AND post_id = ?;")?; + let changes = stmt.execute((user_id, post_id))?; + Ok(changes == 1) +} + +#[instrument()] +pub fn get_all_likes() -> Result<Vec<Like>, rusqlite::Error> { + tracing::trace!("Retrieving comments page"); + let conn = database::connect()?; + let mut stmt = conn.prepare("SELECT * FROM likes")?; + let row = stmt.query_map([], |row| { + let like = Like { + user_id: row.get(0)?, + post_id: row.get(1)? + }; + Ok(like) + })?; + Ok(row.into_iter().flatten().collect()) +} diff --git a/src/database/mod.rs b/src/database/mod.rs index 55cbe4f..6d4853a 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,5 +1,7 @@ use tracing::instrument; +pub mod comments; +pub mod likes; pub mod posts; pub mod sessions; pub mod users; @@ -12,6 +14,8 @@ pub fn init() -> Result<(), rusqlite::Error> { users::init()?; posts::init()?; sessions::init()?; + likes::init()?; + comments::init()?; Ok(()) } diff --git a/src/database/posts.rs b/src/database/posts.rs index 8ca9b2d..7da3bf0 100644 --- a/src/database/posts.rs +++ b/src/database/posts.rs @@ -1,4 +1,3 @@ -use std::collections::HashSet; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use rusqlite::{OptionalExtension, Row}; @@ -7,14 +6,14 @@ use tracing::instrument; use crate::database; use crate::types::post::Post; +use super::{comments, likes}; + pub fn init() -> Result<(), rusqlite::Error> { let sql = " CREATE TABLE IF NOT EXISTS posts ( post_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, content VARCHAR(500) NOT NULL, - likes TEXT NOT NULL, - comments TEXT NOT NULL, date INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(user_id) ); @@ -28,25 +27,20 @@ fn post_from_row(row: &Row) -> Result<Post, rusqlite::Error> { let post_id = row.get(0)?; let user_id = row.get(1)?; let content = row.get(2)?; - let likes_json: String = row.get(3)?; - let comments_json: String = row.get(4)?; - let date = row.get(5)?; - - let Ok(likes) = serde_json::from_str(&likes_json) else { - return Err(rusqlite::Error::InvalidQuery) - }; + let date = row.get(3)?; - let Ok(comments) = serde_json::from_str(&comments_json) else { - return Err(rusqlite::Error::InvalidQuery) - }; + let comments = comments::get_comments_page(0, post_id).unwrap_or_else(|_| Vec::new()); + let likes = likes::get_like_count(post_id).unwrap_or(None).unwrap_or(0); + let liked = likes::get_liked(user_id, post_id).unwrap_or(false); Ok(Post { post_id, user_id, content, + date, likes, + liked, comments, - date, }) } @@ -106,14 +100,6 @@ pub fn get_users_post_page(user_id: u64, page: u64) -> Result<Vec<Post>, rusqlit #[instrument()] pub fn add_post(user_id: u64, content: &str) -> Result<Post, rusqlite::Error> { tracing::trace!("Adding post"); - let likes: HashSet<u64> = HashSet::new(); - let comments: Vec<(u64, String)> = Vec::new(); - let Ok(likes_json) = serde_json::to_string(&likes) else { - return Err(rusqlite::Error::InvalidQuery) - }; - let Ok(comments_json) = serde_json::to_string(&comments) else { - return Err(rusqlite::Error::InvalidQuery) - }; let date = u64::try_from( SystemTime::now() .duration_since(UNIX_EPOCH) @@ -122,29 +108,11 @@ pub fn add_post(user_id: u64, content: &str) -> Result<Post, rusqlite::Error> { ) .unwrap_or(0); let conn = database::connect()?; - let mut stmt = conn.prepare("INSERT INTO posts (user_id, content, likes, comments, date) VALUES(?,?,?,?,?) RETURNING *;")?; - let post = stmt.query_row((user_id, content, likes_json, comments_json, date), |row| { + let mut stmt = + conn.prepare("INSERT INTO posts (user_id, content, date) VALUES(?,?,?) RETURNING *;")?; + let post = stmt.query_row((user_id, content, date), |row| { let row = post_from_row(row)?; Ok(row) })?; Ok(post) } - -#[instrument()] -pub fn update_post( - post_id: u64, - likes: &HashSet<u64>, - comments: &Vec<(u64, String)>, -) -> Result<(), rusqlite::Error> { - tracing::trace!("Updating post"); - let Ok(likes_json) = serde_json::to_string(&likes) else { - return Err(rusqlite::Error::InvalidQuery) - }; - let Ok(comments_json) = serde_json::to_string(&comments) else { - return Err(rusqlite::Error::InvalidQuery) - }; - let conn = database::connect()?; - let sql = "UPDATE posts SET likes = ?, comments = ? WHERE post_id = ?"; - conn.execute(sql, (likes_json, comments_json, post_id))?; - Ok(()) -} diff --git a/src/database/users.rs b/src/database/users.rs index 8045bc4..15565f1 100644 --- a/src/database/users.rs +++ b/src/database/users.rs @@ -21,6 +21,13 @@ pub fn init() -> Result<(), rusqlite::Error> { "; let conn = database::connect()?; conn.execute(sql, ())?; + + let sql2 = "CREATE UNIQUE INDEX IF NOT EXISTS emails on users (email);"; + conn.execute(sql2, ())?; + + let sql3 = "CREATE UNIQUE INDEX IF NOT EXISTS passwords on users (password);"; + conn.execute(sql3, ())?; + Ok(()) } diff --git a/src/public/admin.rs b/src/public/admin.rs index 1da2f1e..25941f1 100644 --- a/src/public/admin.rs +++ b/src/public/admin.rs @@ -4,8 +4,8 @@ use rand::{distributions::Alphanumeric, Rng}; use tokio::sync::Mutex; use crate::{ - console::{self, sanatize}, - types::{http::ResponseCode, post::Post, session::Session, user::User}, + console::sanatize, + types::{http::ResponseCode, post::Post, session::Session, user::User, comment::Comment, like::Like}, }; lazy_static! { @@ -79,24 +79,17 @@ pub fn generate_posts() -> Response { <th>Post ID</th> <th>User ID</th> <th>Content</th> - <th>Likes</th> - <th>Comments</th> <th>Date</th> </tr> "# .to_string(); for post in posts { - let Ok(likes) = serde_json::to_string(&post.likes) else { continue }; - let Ok(comments) = serde_json::to_string(&post.comments) else { continue }; - html.push_str(&format!( - "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>", + "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>", post.post_id, post.user_id, sanatize(&post.content), - console::beautify(&likes), - console::beautify(&comments), post.date )); } @@ -127,3 +120,54 @@ pub fn generate_sessions() -> Response { ResponseCode::Success.text(&html) } + +pub fn generate_comments() -> Response { + let comments = match Comment::reterieve_all() { + Ok(comments) => comments, + Err(err) => return err, + }; + + let mut html = r#" + <tr> + <th>Comment ID</th> + <th>User ID</th> + <th>Post ID</th> + <th>Content</th> + <th>Date</th> + </tr> + "# + .to_string(); + + for comment in comments { + html.push_str(&format!( + "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>", + comment.comment_id, comment.user_id, comment.post_id, sanatize(&comment.content), comment.date + )); + } + + ResponseCode::Success.text(&html) +} + +pub fn generate_likes() -> Response { + let likes = match Like::reterieve_all() { + Ok(likes) => likes, + Err(err) => return err, + }; + + let mut html = r#" + <tr> + <th>User ID</th> + <th>Post ID</th> + </tr> + "# + .to_string(); + + for like in likes { + html.push_str(&format!( + "<tr><td>{}</td><td>{}</td></tr>", + like.user_id, like.post_id + )); + } + + ResponseCode::Success.text(&html) +} diff --git a/src/public/docs.rs b/src/public/docs.rs index 1f1448b..f4e26be 100644 --- a/src/public/docs.rs +++ b/src/public/docs.rs @@ -2,7 +2,10 @@ use axum::response::Response; use lazy_static::lazy_static; use tokio::sync::Mutex; -use crate::{api::{admin, users, posts, auth}, types::http::ResponseCode}; +use crate::{ + api::{admin, auth, posts, users}, + types::http::ResponseCode, +}; use super::console::beautify; @@ -40,10 +43,10 @@ fn generate_body(body: Option<&'static str>) -> String { return String::new() }; let html = r#" - <h2>Body</h2> - <div class="body"> - $body - </div> + <h2>Body</h2> + <div class="body"> + $body + </div> "# .to_string(); let body = body.trim(); @@ -60,20 +63,20 @@ fn generate_body(body: Option<&'static str>) -> String { fn generate_responses(responses: &[(u16, &'static str)]) -> String { let mut html = r#" - <h2>Responses</h2> - $responses + <h2>Responses</h2> + $responses "# .to_string(); for response in responses { let res = format!( r#" - <div> - <span class="ptype">{}</span> - <span class="pdesc">{}</span> - </div> - $responses - "#, + <div> + <span class="ptype">{}</span> + <span class="pdesc">{}</span> + </div> + $responses + "#, response.0, response.1 ); html = html.replace("$responses", &res); @@ -93,18 +96,18 @@ fn generate_cookie(cookie: Option<&'static str>) -> String { fn generate_endpoint(doc: &EndpointDocumentation) -> String { let html = r#" - <div> - <div class="endpoint"> - <span class="method $method_class">$method</span> - <span class="uri">$uri</span> - <span class="desc">$description</span> - $cookie - </div> - <div class="info"> - $body - $responses - </div> + <div> + <div class="endpoint"> + <span class="method $method_class">$method</span> + <span class="uri">$uri</span> + <span class="desc">$description</span> + $cookie </div> + <div class="info"> + $body + $responses + </div> + </div> "#; html.replace("$method_class", &doc.method.to_string().to_lowercase()) @@ -123,6 +126,7 @@ pub async fn init() { auth::AUTH_LOGOUT, posts::POSTS_CREATE, posts::POSTS_PAGE, + posts::COMMENTS_PAGE, posts::POSTS_USER, posts::POSTS_COMMENT, posts::POSTS_LIKE, @@ -136,6 +140,8 @@ pub async fn init() { admin::ADMIN_POSTS, admin::ADMIN_USERS, admin::ADMIN_SESSIONS, + admin::ADMIN_COMMENTS, + admin::ADMIN_LIKES ]; let mut endpoints = ENDPOINTS.lock().await; for doc in docs { @@ -154,27 +160,27 @@ pub async fn generate() -> Response { let html = format!( r#" - <!DOCTYPE html> - <html lang="en"> - <head> - <meta charset="UTF-8"> - <link rel="stylesheet" href="/css/main.css"> - <link rel="stylesheet" href="/css/header.css"> - <link rel="stylesheet" href="/css/console.css"> - <link rel="stylesheet" href="/css/api.css"> - <title>XSSBook - API Documentation</title> - </head> - <body> - <div id="header"> - <span class="logo"><a href="/">xssbook</a></span> - <span class="gtext desc" style="margin-left: 6em; font-size: 2em; color: #606770">API Documentation</span> - </div> - <div id="docs"> - {data} - </div> - </body> - </html> - "# + <!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <link rel="stylesheet" href="/css/main.css"> + <link rel="stylesheet" href="/css/header.css"> + <link rel="stylesheet" href="/css/console.css"> + <link rel="stylesheet" href="/css/api.css"> + <title>XSSBook - API Documentation</title> + </head> + <body> + <div id="header"> + <span class="logo"><a href="/">xssbook</a></span> + <span class="gtext desc" style="margin-left: 6em; font-size: 2em; color: #606770">API Documentation</span> + </div> + <div id="docs"> + {data} + </div> + </body> + </html> + "# ); ResponseCode::Success.html(&html) diff --git a/src/types/comment.rs b/src/types/comment.rs new file mode 100644 index 0000000..cf94bd3 --- /dev/null +++ b/src/types/comment.rs @@ -0,0 +1,44 @@ +use serde::Serialize; +use tracing::instrument; + +use crate::{ + database::{self, comments}, + types::http::{ResponseCode, Result}, +}; + +#[derive(Serialize)] +pub struct Comment { + pub comment_id: u64, + pub user_id: u64, + pub post_id: u64, + pub date: u64, + pub content: String, +} + +impl Comment { + #[instrument()] + pub fn new(user_id: u64, post_id: u64, content: &str) -> Result<Self> { + let Ok(comment) = comments::add_comment(user_id, post_id, content) else { + tracing::error!("Failed to create comment"); + return Err(ResponseCode::InternalServerError.text("Failed to create post")) + }; + + Ok(comment) + } + + #[instrument()] + pub fn from_comment_page(page: u64, post_id: u64) -> Result<Vec<Self>> { + let Ok(posts) = database::comments::get_comments_page(page, post_id) else { + return Err(ResponseCode::BadRequest.text("Failed to fetch comments")) + }; + Ok(posts) + } + + #[instrument()] + pub fn reterieve_all() -> Result<Vec<Self>> { + let Ok(posts) = database::comments::get_all_comments() else { + return Err(ResponseCode::InternalServerError.text("Failed to fetch comments")) + }; + Ok(posts) + } +} diff --git a/src/types/like.rs b/src/types/like.rs new file mode 100644 index 0000000..bf10b2d --- /dev/null +++ b/src/types/like.rs @@ -0,0 +1,48 @@ +use serde::Serialize; +use tracing::instrument; + +use crate::database; +use crate::types::http::{ResponseCode, Result}; + +#[derive(Serialize)] +pub struct Like { + pub user_id: u64, + pub post_id: u64 +} + +impl Like { + #[instrument()] + pub fn add_liked(user_id: u64, post_id: u64) -> Result<()> { + + let Ok(liked) = database::likes::add_liked(user_id, post_id) else { + return Err(ResponseCode::BadRequest.text("Failed to add like status")) + }; + + if !liked { + return Err(ResponseCode::InternalServerError.text("Failed to add like status")); + } + + Ok(()) + } + + #[instrument()] + pub fn remove_liked(user_id: u64, post_id: u64) -> Result<()> { + let Ok(liked) = database::likes::remove_liked(user_id, post_id) else { + return Err(ResponseCode::BadRequest.text("Failed to remove like status")) + }; + + if !liked { + return Err(ResponseCode::InternalServerError.text("Failed to remove like status")); + } + + Ok(()) + } + + #[instrument()] + pub fn reterieve_all() -> Result<Vec<Self>> { + let Ok(likes) = database::likes::get_all_likes() else { + return Err(ResponseCode::InternalServerError.text("Failed to fetch likes")) + }; + Ok(likes) + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 3449d5c..1ee2d08 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,5 +1,7 @@ +pub mod comment; pub mod extract; pub mod http; +pub mod like; pub mod post; pub mod session; pub mod user; diff --git a/src/types/post.rs b/src/types/post.rs index ea7cbbf..a067512 100644 --- a/src/types/post.rs +++ b/src/types/post.rs @@ -1,19 +1,21 @@ use core::fmt; use serde::Serialize; -use std::collections::HashSet; use tracing::instrument; use crate::database; use crate::types::http::{ResponseCode, Result}; +use super::comment::Comment; + #[derive(Serialize)] pub struct Post { pub post_id: u64, pub user_id: u64, pub content: String, - pub likes: HashSet<u64>, - pub comments: Vec<(u64, String)>, pub date: u64, + pub likes: u64, + pub liked: bool, + pub comments: Vec<Comment>, } impl fmt::Debug for Post { @@ -67,34 +69,4 @@ impl Post { Ok(post) } - - #[instrument()] - pub fn comment(&mut self, user_id: u64, content: String) -> Result<()> { - self.comments.push((user_id, content)); - - if database::posts::update_post(self.post_id, &self.likes, &self.comments).is_err() { - tracing::error!("Failed to comment on post"); - return Err(ResponseCode::InternalServerError.text("Failed to comment on post")); - } - - Ok(()) - } - - #[instrument()] - pub fn like(&mut self, user_id: u64, state: bool) -> Result<()> { - if state { - self.likes.insert(user_id); - } else { - self.likes.remove(&user_id); - } - - if database::posts::update_post(self.post_id, &self.likes, &self.comments).is_err() { - tracing::error!("Failed to change like state on post"); - return Err( - ResponseCode::InternalServerError.text("Failed to change like state on post") - ); - } - - Ok(()) - } } |