From 88209d88236c3d865a9f5174a0dced31920859bf Mon Sep 17 00:00:00 2001 From: Tyler Murphy Date: Thu, 26 Jan 2023 17:29:16 -0500 Subject: i did things --- src/api/auth.rs | 98 +++++++++++++++++++++++++++++++++++++++++++++ src/api/mod.rs | 4 ++ src/api/pages.rs | 58 +++++++++++++++++++++++++++ src/api/posts.rs | 102 +++++++++++++++++++++++++++++++++++++++++++++++ src/api/users.rs | 52 ++++++++++++++++++++++++ src/database/mod.rs | 16 ++++++++ src/database/posts.rs | 94 +++++++++++++++++++++++++++++++++++++++++++ src/database/sessions.rs | 42 +++++++++++++++++++ src/database/users.rs | 79 ++++++++++++++++++++++++++++++++++++ src/main.rs | 48 ++++++++++++++++++++++ src/types/extract.rs | 65 ++++++++++++++++++++++++++++++ src/types/mod.rs | 5 +++ src/types/post.rs | 83 ++++++++++++++++++++++++++++++++++++++ src/types/response.rs | 60 ++++++++++++++++++++++++++++ src/types/session.rs | 38 ++++++++++++++++++ src/types/user.rs | 79 ++++++++++++++++++++++++++++++++++++ 16 files changed, 923 insertions(+) create mode 100644 src/api/auth.rs create mode 100644 src/api/mod.rs create mode 100644 src/api/pages.rs create mode 100644 src/api/posts.rs create mode 100644 src/api/users.rs create mode 100644 src/database/mod.rs create mode 100644 src/database/posts.rs create mode 100644 src/database/sessions.rs create mode 100644 src/database/users.rs create mode 100644 src/main.rs create mode 100644 src/types/extract.rs create mode 100644 src/types/mod.rs create mode 100644 src/types/post.rs create mode 100644 src/types/response.rs create mode 100644 src/types/session.rs create mode 100644 src/types/user.rs (limited to 'src') 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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 +} + +async fn load_batch(AuthorizedUser(_user): AuthorizedUser, Json(body): Json) -> 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) -> 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 { + 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 { + 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, 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, 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, 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 { + let likes: HashSet = 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, 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, 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> { + 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> { + 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 { + 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, 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, 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, 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, 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 { + 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(req: Request, next: Next) -> 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 FromRequestParts for AuthorizedUser where S: Send + Sync { + type Rejection = Response; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + + let Ok(Some(cookies)) = Option::>::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(pub T); + +#[async_trait] +impl FromRequest for Json where + T: DeserializeOwned, + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into, + S: Send + Sync, +{ + type Rejection = Response; + + async fn from_request(req: Request, state: &S) -> Result { + + 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, + pub comments: Vec<(u64, String)>, + pub date: u64 +} + +impl Post { + + pub fn from_post_id(post_id: u64) -> Result { + 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) -> Vec { + // 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> { + 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> { + 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 { + 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 { + 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 = std::result::Result; \ 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 { + 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 { + 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 { + 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) -> Vec { + 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> { + 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 { + 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 { + 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 { + 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 -- cgit v1.2.3-freya