summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/api/admin.rs63
-rw-r--r--src/api/auth.rs34
-rw-r--r--src/api/posts.rs120
-rw-r--r--src/api/users.rs35
-rw-r--r--src/database/comments.rs91
-rw-r--r--src/database/likes.rs77
-rw-r--r--src/database/mod.rs4
-rw-r--r--src/database/posts.rs54
-rw-r--r--src/database/users.rs7
-rw-r--r--src/public/admin.rs64
-rw-r--r--src/public/docs.rs96
-rw-r--r--src/types/comment.rs44
-rw-r--r--src/types/like.rs48
-rw-r--r--src/types/mod.rs2
-rw-r--r--src/types/post.rs38
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(())
- }
}