summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/auth.rs98
-rw-r--r--src/api/mod.rs4
-rw-r--r--src/api/pages.rs58
-rw-r--r--src/api/posts.rs102
-rw-r--r--src/api/users.rs52
-rw-r--r--src/database/mod.rs16
-rw-r--r--src/database/posts.rs94
-rw-r--r--src/database/sessions.rs42
-rw-r--r--src/database/users.rs79
-rw-r--r--src/main.rs48
-rw-r--r--src/types/extract.rs65
-rw-r--r--src/types/mod.rs5
-rw-r--r--src/types/post.rs83
-rw-r--r--src/types/response.rs60
-rw-r--r--src/types/session.rs38
-rw-r--r--src/types/user.rs79
16 files changed, 923 insertions, 0 deletions
diff --git a/src/api/auth.rs b/src/api/auth.rs
new file mode 100644
index 0000000..d60483f
--- /dev/null
+++ b/src/api/auth.rs
@@ -0,0 +1,98 @@
+use axum::{Router, routing::post, response::Response};
+use serde::Deserialize;
+use time::{OffsetDateTime, Duration};
+use tower_cookies::{Cookies, Cookie};
+
+use crate::types::{user::User, response::ResponseCode, session::Session, extract::{Json, AuthorizedUser}};
+
+#[derive(Deserialize)]
+struct RegistrationRequet {
+ firstname: String,
+ lastname: String,
+ email: String,
+ password: String,
+ gender: String,
+ day: u8,
+ month: u8,
+ year: u32
+}
+
+
+async fn register(cookies: Cookies, Json(body): Json<RegistrationRequet>) -> Response {
+
+ let user = match User::new(body.firstname, body.lastname, body.email, body.password, body.gender, body.day, body.month, body.year) {
+ Ok(user) => user,
+ Err(err) => return err
+ };
+
+ let session = match Session::new(user.user_id) {
+ Ok(session) => session,
+ Err(err) => return err
+ };
+
+ let mut now = OffsetDateTime::now_utc();
+ now += Duration::weeks(52);
+
+ let mut cookie = Cookie::new("auth", session.token);
+ cookie.set_secure(false);
+ cookie.set_http_only(false);
+ cookie.set_expires(now);
+ cookie.set_path("/");
+
+ cookies.add(cookie);
+
+ ResponseCode::Created.msg("Successfully created new user")
+}
+
+#[derive(Deserialize)]
+struct LoginRequest {
+ email: String,
+ password: String,
+}
+
+async fn login(cookies: Cookies, Json(body): Json<LoginRequest>) -> Response {
+
+ let Ok(user) = User::from_email(&body.email) else {
+ return ResponseCode::BadRequest.msg("Email is not registered")
+ };
+
+ if user.password != body.password {
+ return ResponseCode::BadRequest.msg("Password is not correct")
+ }
+
+ let session = match Session::new(user.user_id) {
+ Ok(session) => session,
+ Err(err) => return err
+ };
+
+ let mut now = OffsetDateTime::now_utc();
+ now += Duration::weeks(52);
+
+ let mut cookie = Cookie::new("auth", session.token);
+ cookie.set_secure(false);
+ cookie.set_http_only(false);
+ cookie.set_expires(now);
+ cookie.set_path("/");
+
+ cookies.add(cookie);
+
+ ResponseCode::Success.msg("Successfully logged in")
+}
+
+async fn logout(cookies: Cookies, AuthorizedUser(user): AuthorizedUser) -> Response {
+
+ cookies.remove(Cookie::new("auth", ""));
+
+ if let Err(err) = Session::delete(user.user_id) {
+ return err
+ }
+
+ ResponseCode::Success.msg("Successfully logged out")
+}
+
+pub fn router() -> Router {
+ Router::new()
+ .route("/register", post(register))
+ .route("/login", post(login))
+ .route("/logout", post(logout))
+} \ No newline at end of file
diff --git a/src/api/mod.rs b/src/api/mod.rs
new file mode 100644
index 0000000..ba38aeb
--- /dev/null
+++ b/src/api/mod.rs
@@ -0,0 +1,4 @@
+pub mod auth;
+pub mod pages;
+pub mod posts;
+pub mod users; \ No newline at end of file
diff --git a/src/api/pages.rs b/src/api/pages.rs
new file mode 100644
index 0000000..749a686
--- /dev/null
+++ b/src/api/pages.rs
@@ -0,0 +1,58 @@
+use axum::{Router, response::{Response, Redirect, IntoResponse}, routing::get};
+
+use crate::types::{extract::AuthorizedUser, response::ResponseCode};
+
+async fn root(user: Option<AuthorizedUser>) -> Response {
+ println!("{}", user.is_some());
+ if user.is_some() {
+ return Redirect::to("/home").into_response()
+ } else {
+ return Redirect::to("/login").into_response()
+ }
+}
+
+async fn login(user: Option<AuthorizedUser>) -> Response {
+ if user.is_some() {
+ return Redirect::to("/home").into_response()
+ } else {
+ return ResponseCode::Success.file("/login.html").await.unwrap()
+ }
+}
+
+async fn home(user: Option<AuthorizedUser>) -> Response {
+ if user.is_none() {
+ return Redirect::to("/login").into_response()
+ } else {
+ return ResponseCode::Success.file("/home.html").await.unwrap()
+ }
+}
+
+async fn people(user: Option<AuthorizedUser>) -> Response {
+ if user.is_none() {
+ return Redirect::to("/login").into_response()
+ } else {
+ return ResponseCode::Success.file("/people.html").await.unwrap()
+ }
+}
+
+async fn profile(user: Option<AuthorizedUser>) -> Response {
+ if user.is_none() {
+ return Redirect::to("/login").into_response()
+ } else {
+ return ResponseCode::Success.file("/profile.html").await.unwrap()
+ }
+}
+
+async fn wordpress() -> Response {
+ ResponseCode::ImATeapot.msg("Hello i am a teapot owo")
+}
+
+pub fn router() -> Router {
+ Router::new()
+ .route("/", get(root))
+ .route("/login", get(login))
+ .route("/home", get(home))
+ .route("/people", get(people))
+ .route("/profile", get(profile))
+ .route("/wp-admin", get(wordpress))
+} \ No newline at end of file
diff --git a/src/api/posts.rs b/src/api/posts.rs
new file mode 100644
index 0000000..405dfa6
--- /dev/null
+++ b/src/api/posts.rs
@@ -0,0 +1,102 @@
+use axum::{response::Response, Router, routing::{post, patch}};
+use serde::Deserialize;
+
+use crate::types::{extract::{AuthorizedUser, Json}, post::Post, response::ResponseCode};
+
+
+#[derive(Deserialize)]
+struct PostCreateRequest {
+ content: String
+}
+
+async fn create(AuthorizedUser(user): AuthorizedUser, Json(body): Json<PostCreateRequest>) -> Response {
+
+ let Ok(_post) = Post::new(user.user_id, body.content) else {
+ return ResponseCode::InternalServerError.msg("Failed to create post")
+ };
+
+ ResponseCode::Created.msg("Successfully created new post")
+}
+
+#[derive(Deserialize)]
+struct PostPageRequest {
+ page: u64
+}
+
+async fn page(AuthorizedUser(_user): AuthorizedUser, Json(body): Json<PostPageRequest>) -> Response {
+
+ let Ok(posts) = Post::from_post_page(body.page) else {
+ return ResponseCode::InternalServerError.msg("Failed to fetch posts")
+ };
+
+ let Ok(json) = serde_json::to_string(&posts) else {
+ return ResponseCode::InternalServerError.msg("Failed to fetch posts")
+ };
+
+ ResponseCode::Success.json(&json)
+}
+
+#[derive(Deserialize)]
+struct UsersPostsRequest {
+ user_id: u64
+}
+
+async fn user(AuthorizedUser(_user): AuthorizedUser, Json(body): Json<UsersPostsRequest>) -> Response {
+
+ let Ok(posts) = Post::from_user_id(body.user_id) else {
+ return ResponseCode::InternalServerError.msg("Failed to fetch posts")
+ };
+
+ let Ok(json) = serde_json::to_string(&posts) else {
+ return ResponseCode::InternalServerError.msg("Failed to fetch posts")
+ };
+
+ ResponseCode::Success.json(&json)
+}
+
+#[derive(Deserialize)]
+struct PostCommentRequest {
+ content: String,
+ post_id: u64
+}
+
+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.msg("Failed to fetch posts")
+ };
+
+ if let Err(err) = post.comment(user.user_id, body.content) {
+ return err;
+ }
+
+ ResponseCode::Success.msg("Successfully commented on post")
+}
+
+#[derive(Deserialize)]
+struct PostLikeRequest {
+ state: bool,
+ post_id: u64
+}
+
+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.msg("Failed to fetch posts")
+ };
+
+ if let Err(err) = post.like(user.user_id, body.state) {
+ return err;
+ }
+
+ ResponseCode::Success.msg("Successfully changed like status on post")
+}
+
+pub fn router() -> Router {
+ Router::new()
+ .route("/create", post(create))
+ .route("/page", post(page))
+ .route("/user", post(user))
+ .route("/comment", patch(comment))
+ .route("/like", patch(like))
+} \ No newline at end of file
diff --git a/src/api/users.rs b/src/api/users.rs
new file mode 100644
index 0000000..283ec96
--- /dev/null
+++ b/src/api/users.rs
@@ -0,0 +1,52 @@
+use axum::{Router, response::Response, routing::post};
+use serde::Deserialize;
+use crate::types::{extract::{AuthorizedUser, Json}, response::ResponseCode, user::User};
+
+#[derive(Deserialize)]
+struct UserLoadRequest {
+ ids: Vec<u64>
+}
+
+async fn load_batch(AuthorizedUser(_user): AuthorizedUser, Json(body): Json<UserLoadRequest>) -> Response {
+
+ let users = User::from_user_ids(body.ids);
+ let Ok(json) = serde_json::to_string(&users) else {
+ return ResponseCode::InternalServerError.msg("Failed to fetch users")
+ };
+
+ ResponseCode::Success.json(&json)
+}
+
+#[derive(Deserialize)]
+struct UserPageReqiest {
+ page: u64
+}
+
+async fn load_page(AuthorizedUser(_user): AuthorizedUser, Json(body): Json<UserPageReqiest>) -> Response {
+
+ let Ok(users) = User::from_user_page(body.page) else {
+ return ResponseCode::InternalServerError.msg("Failed to fetch users")
+ };
+
+ let Ok(json) = serde_json::to_string(&users) else {
+ return ResponseCode::InternalServerError.msg("Failed to fetch users")
+ };
+
+ ResponseCode::Success.json(&json)
+}
+
+async fn load_self(AuthorizedUser(user): AuthorizedUser) -> Response {
+
+ let Ok(json) = serde_json::to_string(&user) else {
+ return ResponseCode::InternalServerError.msg("Failed to fetch user")
+ };
+
+ ResponseCode::Success.json(&json)
+}
+
+pub fn router() -> Router {
+ Router::new()
+ .route("/load", post(load_batch))
+ .route("/self", post(load_self))
+ .route("/page", post(load_page))
+} \ No newline at end of file
diff --git a/src/database/mod.rs b/src/database/mod.rs
new file mode 100644
index 0000000..7227074
--- /dev/null
+++ b/src/database/mod.rs
@@ -0,0 +1,16 @@
+use rusqlite::Result;
+
+pub mod posts;
+pub mod users;
+pub mod sessions;
+
+pub fn connect() -> Result<rusqlite::Connection, rusqlite::Error> {
+ return rusqlite::Connection::open("xssbook.db");
+}
+
+pub fn init() -> Result<()> {
+ users::init()?;
+ posts::init()?;
+ sessions::init()?;
+ Ok(())
+} \ No newline at end of file
diff --git a/src/database/posts.rs b/src/database/posts.rs
new file mode 100644
index 0000000..77d2387
--- /dev/null
+++ b/src/database/posts.rs
@@ -0,0 +1,94 @@
+use std::collections::HashSet;
+use std::time::{SystemTime, UNIX_EPOCH};
+
+use rusqlite::{OptionalExtension, Row};
+
+use crate::types::post::Post;
+use crate::database;
+
+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 TEXT NOT NULL,
+ likes TEXT NOT NULL,
+ comments TEXT NOT NULL,
+ date INTEGER NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(user_id)
+ );
+ ";
+ let conn = database::connect()?;
+ conn.execute(sql, ())?;
+ Ok(())
+}
+
+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 Ok(comments) = serde_json::from_str(&comments_json) else {
+ return Err(rusqlite::Error::InvalidQuery)
+ };
+
+ Ok(Post{post_id, user_id, content, likes, comments, date})
+}
+
+pub fn get_post(post_id: u64) -> Result<Option<Post>, rusqlite::Error> {
+ let conn = database::connect()?;
+ let mut stmt = conn.prepare("SELECT * FROM posts WHERE post_id = ?")?;
+ let row = stmt.query_row([post_id], |row| Ok(post_from_row(row)?)).optional()?;
+ Ok(row)
+}
+
+pub fn get_post_page(page: u64) -> Result<Vec<Post>, rusqlite::Error> {
+ let page_size = 10;
+ let conn = database::connect()?;
+ let mut stmt = conn.prepare("SELECT * FROM posts ORDER BY post_id DESC LIMIT ? OFFSET ?")?;
+ let row = stmt.query_map([page_size, page_size * page], |row| Ok(post_from_row(row)?))?;
+ Ok(row.into_iter().flatten().collect())
+}
+
+pub fn get_users_posts(user_id: u64) -> Result<Vec<Post>, rusqlite::Error> {
+ let conn = database::connect()?;
+ let mut stmt = conn.prepare("SELECT * FROM posts WHERE user_id = ? ORDER BY post_id DESC")?;
+ let row = stmt.query_map([user_id], |row| Ok(post_from_row(row)?))?;
+ Ok(row.into_iter().flatten().collect())
+}
+
+pub fn add_post(user_id: u64, content: &str) -> Result<Post, rusqlite::Error> {
+ 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 = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
+ 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| Ok(post_from_row(row)?))?;
+ Ok(post)
+}
+
+pub fn update_post(post_id: u64, likes: &HashSet<u64>, comments: &Vec<(u64, String)>) -> Result<(), rusqlite::Error> {
+ 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(())
+} \ No newline at end of file
diff --git a/src/database/sessions.rs b/src/database/sessions.rs
new file mode 100644
index 0000000..7866d76
--- /dev/null
+++ b/src/database/sessions.rs
@@ -0,0 +1,42 @@
+use rusqlite::OptionalExtension;
+
+use crate::{database, types::session::Session};
+
+pub fn init() -> Result<(), rusqlite::Error> {
+ let sql = "
+ CREATE TABLE IF NOT EXISTS sessions (
+ user_id INTEGER PRIMARY KEY NOT NULL,
+ token TEXT NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(user_id)
+ );
+ ";
+ let conn = database::connect()?;
+ conn.execute(sql, ())?;
+ Ok(())
+}
+
+pub fn get_session(token: &str) -> Result<Option<Session>, rusqlite::Error> {
+ let conn = database::connect()?;
+ let mut stmt = conn.prepare("SELECT * FROM sessions WHERE token = ?")?;
+ let row = stmt.query_row([token], |row| {
+ Ok(Session {
+ user_id: row.get(0)?,
+ token: row.get(1)?,
+ })
+ }).optional()?;
+ Ok(row)
+}
+
+pub fn set_session(user_id: u64, token: &str) -> Result<(), Box<dyn std::error::Error>> {
+ let conn = database::connect()?;
+ let sql = "INSERT OR REPLACE INTO sessions (user_id, token) VALUES (?, ?);";
+ conn.execute(sql, (user_id, token))?;
+ Ok(())
+}
+
+pub fn delete_session(user_id: u64) -> Result<(), Box<dyn std::error::Error>> {
+ let conn = database::connect()?;
+ let sql = "DELETE FROM sessions WHERE user_id = ?;";
+ conn.execute(sql, [user_id])?;
+ Ok(())
+} \ No newline at end of file
diff --git a/src/database/users.rs b/src/database/users.rs
new file mode 100644
index 0000000..2618dce
--- /dev/null
+++ b/src/database/users.rs
@@ -0,0 +1,79 @@
+use std::time::{SystemTime, UNIX_EPOCH};
+use rusqlite::{OptionalExtension, Row};
+
+use crate::{database, types::user::User};
+
+pub fn init() -> Result<(), rusqlite::Error> {
+ let sql = "
+ CREATE TABLE IF NOT EXISTS users (
+ user_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ firstname VARCHAR(20) NOT NULL,
+ lastname VARCHAR(20) NOT NULL,
+ email VARCHAR(50) NOT NULL,
+ password VARCHAR(50) NOT NULL,
+ gender VARCHAR(100) NOT NULL,
+ date BIGINT NOT NULL,
+ day TINYINT NOT NULL,
+ month TINYINT NOT NULL,
+ year INTEGER NOT NULL
+ );
+ ";
+ let conn = database::connect()?;
+ conn.execute(sql, ())?;
+ Ok(())
+}
+
+fn user_from_row(row: &Row, hide_password: bool) -> Result<User, rusqlite::Error> {
+ let user_id = row.get(0)?;
+ let firstname = row.get(1)?;
+ let lastname = row.get(2)?;
+ let email = row.get(3)?;
+ let password = row.get(4)?;
+ let gender = row.get(5)?;
+ let date = row.get(6)?;
+ let day = row.get(7)?;
+ let month = row.get(8)?;
+ let year = row.get(9)?;
+
+ let password = if hide_password { "".to_string() } else { password };
+
+ Ok(User{user_id, firstname, lastname, email, password, gender,date, day, month, year})
+}
+
+pub fn get_user_by_id(user_id: u64, hide_password: bool) -> Result<Option<User>, rusqlite::Error> {
+ let conn = database::connect()?;
+ let mut stmt = conn.prepare("SELECT * FROM users WHERE user_id = ?")?;
+ let row = stmt.query_row([user_id], |row| Ok(user_from_row(row, hide_password)?)).optional()?;
+ Ok(row)
+}
+
+pub fn get_user_by_email(email: &str, hide_password: bool) -> Result<Option<User>, rusqlite::Error> {
+ let conn = database::connect()?;
+ let mut stmt = conn.prepare("SELECT * FROM users WHERE email = ?")?;
+ let row = stmt.query_row([email], |row| Ok(user_from_row(row, hide_password)?)).optional()?;
+ Ok(row)
+}
+
+pub fn get_user_by_password(password: &str, hide_password: bool) -> Result<Option<User>, rusqlite::Error> {
+ let conn = database::connect()?;
+ let mut stmt = conn.prepare("SELECT * FROM users WHERE password = ?")?;
+ let row = stmt.query_row([password], |row| Ok(user_from_row(row, hide_password)?)).optional()?;
+ Ok(row)
+}
+
+pub fn get_user_page(page: u64, hide_password: bool) -> Result<Vec<User>, rusqlite::Error> {
+ let page_size = 5;
+ let conn = database::connect()?;
+ let mut stmt = conn.prepare("SELECT * FROM users ORDER BY user_id DESC LIMIT ? OFFSET ?")?;
+ let row = stmt.query_map([page_size, page_size * page], |row| Ok(user_from_row(row, hide_password)?))?;
+ Ok(row.into_iter().flatten().collect())
+}
+
+pub fn add_user(firstname: &str, lastname: &str, email: &str, password: &str, gender: &str, day: u8, month: u8, year: u32) -> Result<User, rusqlite::Error> {
+ let date = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
+
+ let conn = database::connect()?;
+ let mut stmt = conn.prepare("INSERT INTO users (firstname, lastname, email, password, gender, date, day, month, year) VALUES(?,?,?,?,?,?,?,?,?) RETURNING *;")?;
+ let user = stmt.query_row((firstname, lastname, email, password, gender, date, day, month, year), |row| Ok(user_from_row(row, false)?))?;
+ Ok(user)
+} \ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..9ad772d
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,48 @@
+use std::net::SocketAddr;
+use axum::{Router, response::Response, http::Request, middleware::{Next, self}};
+use tower_cookies::CookieManagerLayer;
+use types::response::ResponseCode;
+
+use crate::api::{pages, auth, users, posts};
+
+mod api;
+mod database;
+mod types;
+
+async fn serve<B>(req: Request<B>, next: Next<B>) -> Response {
+ let Ok(file) = ResponseCode::Success.file(&req.uri().to_string()).await else {
+ return next.run(req).await
+ };
+ file
+}
+
+async fn not_found() -> Response {
+ match ResponseCode::NotFound.file("/404.html").await {
+ Ok(file) => file,
+ Err(err) => err
+ }
+}
+
+#[tokio::main]
+async fn main() {
+
+ database::init().unwrap();
+
+ let app = Router::new()
+ .fallback(not_found)
+ .layer(middleware::from_fn(serve))
+ .nest("/", pages::router())
+ .nest("/api/auth", auth::router())
+ .nest("/api/users", users::router())
+ .nest("/api/posts", posts::router())
+ .layer(CookieManagerLayer::new());
+
+ let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
+ println!("Listening on {}", addr);
+
+ axum::Server::bind(&addr)
+ .serve(app.into_make_service())
+ .await
+ .unwrap();
+
+}
diff --git a/src/types/extract.rs b/src/types/extract.rs
new file mode 100644
index 0000000..6518ca1
--- /dev/null
+++ b/src/types/extract.rs
@@ -0,0 +1,65 @@
+use std::io::Read;
+
+use axum::{extract::{FromRequestParts, FromRequest}, async_trait, response::Response, http::{request::Parts, Request}, TypedHeader, headers::Cookie, body::HttpBody, BoxError};
+use bytes::Bytes;
+use serde::de::DeserializeOwned;
+
+use crate::types::{user::User, response::{ResponseCode, Result}, session::Session};
+
+pub struct AuthorizedUser(pub User);
+
+#[async_trait]
+impl<S> FromRequestParts<S> for AuthorizedUser where S: Send + Sync {
+ type Rejection = Response;
+
+ async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self> {
+
+ let Ok(Some(cookies)) = Option::<TypedHeader<Cookie>>::from_request_parts(parts, state).await else {
+ return Err(ResponseCode::Forbidden.msg("No cookies provided"))
+ };
+
+ let Some(token) = cookies.get("auth") else {
+ return Err(ResponseCode::Forbidden.msg("No auth token provided"))
+ };
+
+ let Ok(session) = Session::from_token(&token) else {
+ return Err(ResponseCode::Unauthorized.msg("Auth token invalid"))
+ };
+
+ let Ok(user) = User::from_user_id(session.user_id, true) else {
+ return Err(ResponseCode::InternalServerError.msg("Valid token but no valid user"))
+ };
+
+ Ok(AuthorizedUser(user))
+ }
+}
+
+pub struct Json<T>(pub T);
+
+#[async_trait]
+impl<T, S, B> FromRequest<S, B> for Json<T> where
+ T: DeserializeOwned,
+ B: HttpBody + Send + 'static,
+ B::Data: Send,
+ B::Error: Into<BoxError>,
+ S: Send + Sync,
+{
+ type Rejection = Response;
+
+ async fn from_request(req: Request<B>, state: &S) -> Result<Self> {
+
+ let Ok(bytes) = Bytes::from_request(req, state).await else {
+ return Err(ResponseCode::InternalServerError.msg("Failed to read request body"));
+ };
+
+ let Ok(string) = String::from_utf8(bytes.bytes().flatten().collect()) else {
+ return Err(ResponseCode::BadRequest.msg("Invalid utf8 body"))
+ };
+
+ let Ok(value) = serde_json::from_str(&string) else {
+ return Err(ResponseCode::BadRequest.msg("Invalid request body"))
+ };
+
+ Ok(Json(value))
+ }
+} \ No newline at end of file
diff --git a/src/types/mod.rs b/src/types/mod.rs
new file mode 100644
index 0000000..089885e
--- /dev/null
+++ b/src/types/mod.rs
@@ -0,0 +1,5 @@
+pub mod user;
+pub mod post;
+pub mod session;
+pub mod extract;
+pub mod response; \ No newline at end of file
diff --git a/src/types/post.rs b/src/types/post.rs
new file mode 100644
index 0000000..94f0a9e
--- /dev/null
+++ b/src/types/post.rs
@@ -0,0 +1,83 @@
+use std::collections::HashSet;
+use serde::Serialize;
+
+use crate::database;
+use crate::types::response::{Result, ResponseCode};
+
+#[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
+}
+
+impl Post {
+
+ pub fn from_post_id(post_id: u64) -> Result<Self> {
+ let Ok(Some(post)) = database::posts::get_post(post_id) else {
+ return Err(ResponseCode::BadRequest.msg("Post does not exist"))
+ };
+
+ Ok(post)
+ }
+
+ // pub fn from_post_ids(post_ids: Vec<u64>) -> Vec<Self> {
+ // post_ids.iter().map(|id| {
+ // let Ok(post) = Post::from_post_id(*id) else {
+ // return None;
+ // };
+ // Some(post)
+ // }).flatten().collect()
+ // }
+
+ pub fn from_post_page(page: u64) -> Result<Vec<Self>> {
+ let Ok(posts) = database::posts::get_post_page(page) else {
+ return Err(ResponseCode::BadRequest.msg("Failed to fetch posts"))
+ };
+ Ok(posts)
+ }
+
+ pub fn from_user_id(user_id: u64) -> Result<Vec<Self>> {
+ let Ok(posts) = database::posts::get_users_posts(user_id) else {
+ return Err(ResponseCode::BadRequest.msg("Failed to fetch posts"))
+ };
+ Ok(posts)
+ }
+
+ pub fn new(user_id: u64, content: String) -> Result<Self> {
+ let Ok(post) = database::posts::add_post(user_id, &content) else {
+ return Err(ResponseCode::InternalServerError.msg("Failed to create post"))
+ };
+
+ Ok(post)
+ }
+
+ 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() {
+ return Err(ResponseCode::InternalServerError.msg("Failed to comment on post"))
+ }
+
+ Ok(())
+ }
+
+ 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() {
+ return Err(ResponseCode::InternalServerError.msg("Failed to comment on post"))
+ }
+
+ Ok(())
+ }
+
+} \ No newline at end of file
diff --git a/src/types/response.rs b/src/types/response.rs
new file mode 100644
index 0000000..bea3406
--- /dev/null
+++ b/src/types/response.rs
@@ -0,0 +1,60 @@
+use axum::{response::{IntoResponse, Response}, http::{StatusCode, Request, HeaderValue}, body::Body, headers::HeaderName};
+use tower::ServiceExt;
+use tower_http::services::ServeFile;
+
+#[derive(Debug)]
+pub enum ResponseCode {
+ Success,
+ Created,
+ BadRequest,
+ Unauthorized,
+ Forbidden,
+ NotFound,
+ ImATeapot,
+ InternalServerError
+}
+
+impl ResponseCode {
+ pub fn code(self) -> StatusCode {
+ match self {
+ Self::Success => StatusCode::OK,
+ Self::Created => StatusCode::CREATED,
+ Self::BadRequest => StatusCode::BAD_REQUEST,
+ Self::Unauthorized => StatusCode::UNAUTHORIZED,
+ Self::Forbidden => StatusCode::FORBIDDEN,
+ Self::NotFound => StatusCode::NOT_FOUND,
+ Self::ImATeapot => StatusCode::IM_A_TEAPOT,
+ Self::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR
+ }
+ }
+
+ pub fn msg(self, msg: &str) -> Response {
+ (self.code(), msg.to_owned()).into_response()
+ }
+
+ pub fn json(self, json: &str) -> Response {
+ let mut res = (self.code(), json.to_owned()).into_response();
+ res.headers_mut().insert(
+ HeaderName::from_static("content-type"), HeaderValue::from_static("application/json"),
+ );
+ res
+ }
+
+ pub async fn file(self, path: &str) -> Result<Response> {
+ if path.chars().position(|c| c == '.' ).is_none() {
+ return Err(ResponseCode::BadRequest.msg("Folders cannot be served"));
+ }
+ let path = format!("public{}", path);
+ let svc = ServeFile::new(path);
+ let Ok(mut res) = svc.oneshot(Request::new(Body::empty())).await else {
+ return Err(ResponseCode::InternalServerError.msg("Error wile fetching file"));
+ };
+ if res.status() != StatusCode::OK {
+ return Err(ResponseCode::NotFound.msg("File not found"));
+ }
+ *res.status_mut() = self.code();
+ Ok(res.into_response())
+ }
+}
+
+pub type Result<T> = std::result::Result<T, Response>; \ No newline at end of file
diff --git a/src/types/session.rs b/src/types/session.rs
new file mode 100644
index 0000000..8064fb1
--- /dev/null
+++ b/src/types/session.rs
@@ -0,0 +1,38 @@
+use rand::{distributions::Alphanumeric, Rng};
+use serde::Serialize;
+
+use crate::database;
+use crate::types::response::{Result, ResponseCode};
+
+#[derive(Serialize)]
+pub struct Session {
+ pub user_id: u64,
+ pub token: String
+}
+
+impl Session {
+
+ pub fn from_token(token: &str) -> Result<Self> {
+ let Ok(Some(session)) = database::sessions::get_session(token) else {
+ return Err(ResponseCode::BadRequest.msg("Invalid auth token"));
+ };
+
+ Ok(session)
+ }
+
+ pub fn new(user_id: u64) -> Result<Self> {
+ let token: String = rand::thread_rng().sample_iter(&Alphanumeric).take(32).map(char::from).collect();
+ match database::sessions::set_session(user_id, &token) {
+ Err(_) => return Err(ResponseCode::BadRequest.msg("Failed to create session")),
+ Ok(_) => return Ok(Session {user_id, token})
+ };
+ }
+
+ pub fn delete(user_id: u64) -> Result<()> {
+ if let Err(_) = database::sessions::delete_session(user_id) {
+ return Err(ResponseCode::InternalServerError.msg("Failed to logout"));
+ };
+ Ok(())
+ }
+
+} \ No newline at end of file
diff --git a/src/types/user.rs b/src/types/user.rs
new file mode 100644
index 0000000..1213a75
--- /dev/null
+++ b/src/types/user.rs
@@ -0,0 +1,79 @@
+use serde::{Serialize, Deserialize};
+
+use crate::database;
+use crate::types::response::{Result, ResponseCode};
+
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct User {
+ pub user_id: u64,
+ pub firstname: String,
+ pub lastname: String,
+ pub email: String,
+ pub password: String,
+ pub gender: String,
+ pub date: u64,
+ pub day: u8,
+ pub month: u8,
+ pub year: u32,
+}
+
+impl User {
+
+ pub fn from_user_id(user_id: u64, hide_password: bool) -> Result<Self> {
+ let Ok(Some(user)) = database::users::get_user_by_id(user_id, hide_password) else {
+ return Err(ResponseCode::BadRequest.msg("User does not exist"))
+ };
+
+ Ok(user)
+ }
+
+ pub fn from_user_ids(user_ids: Vec<u64>) -> Vec<Self> {
+ user_ids.iter().map(|user_id| {
+ let Ok(Some(user)) = database::users::get_user_by_id(*user_id, true) else {
+ return None;
+ };
+ Some(user)
+ }).flatten().collect()
+ }
+
+ pub fn from_user_page(page: u64) -> Result<Vec<Self>> {
+ let Ok(users) = database::users::get_user_page(page, true) else {
+ return Err(ResponseCode::BadRequest.msg("Failed to fetch users"))
+ };
+ Ok(users)
+ }
+
+ pub fn from_email(email: &str) -> Result<Self> {
+ let Ok(Some(user)) = database::users::get_user_by_email(email, false) else {
+ return Err(ResponseCode::BadRequest.msg("User does not exist"))
+ };
+
+ Ok(user)
+ }
+
+ pub fn from_password(password: &str) -> Result<Self> {
+ let Ok(Some(user)) = database::users::get_user_by_password(password, true) else {
+ return Err(ResponseCode::BadRequest.msg("User does not exist"))
+ };
+
+ Ok(user)
+ }
+
+ pub fn new(firstname: String, lastname: String, email: String, password: String, gender: String, day: u8, month: u8, year: u32) -> Result<Self> {
+ if let Ok(_) = User::from_email(&email) {
+ return Err(ResponseCode::BadRequest.msg(&format!("Email is already in use by {}", &email)))
+ }
+
+ if let Ok(user) = User::from_password(&password) {
+ return Err(ResponseCode::BadRequest.msg(&format!("Password is already in use by {}", user.email)))
+ }
+
+ let Ok(user) = database::users::add_user(&firstname, &lastname, &email, &password, &gender, day, month, year) else {
+ return Err(ResponseCode::InternalServerError.msg("Failed to create new uesr"))
+ };
+
+ Ok(user)
+ }
+
+} \ No newline at end of file