From 27386d5d18ef6ac921a9ffcca63f1950ea00dc94 Mon Sep 17 00:00:00 2001 From: Imbus Date: Fri, 20 Oct 2023 06:06:55 +0200 Subject: [PATCH 1/5] Major restructure --- server/migrations/0002_posts_table.sql | 2 + server/src/db.rs | 25 +++++++ server/src/main.rs | 4 +- server/src/routes/mod.rs | 7 ++ server/src/routes/post.rs | 27 +++++++ server/src/{routes.rs => routes/users.rs} | 89 ----------------------- server/src/routes/vote.rs | 16 ++++ server/src/state.rs | 4 +- server/src/types.rs | 23 ++---- 9 files changed, 87 insertions(+), 110 deletions(-) create mode 100644 server/src/db.rs create mode 100644 server/src/routes/mod.rs create mode 100755 server/src/routes/post.rs rename server/src/{routes.rs => routes/users.rs} (54%) create mode 100644 server/src/routes/vote.rs diff --git a/server/migrations/0002_posts_table.sql b/server/migrations/0002_posts_table.sql index 35c0660..97d6a83 100644 --- a/server/migrations/0002_posts_table.sql +++ b/server/migrations/0002_posts_table.sql @@ -2,6 +2,8 @@ CREATE TABLE IF NOT EXISTS posts ( id SERIAL PRIMARY KEY, user_id SERIAL NOT NULL, content TEXT NOT NULL, + upvotes INT NOT NULL DEFAULT 0, + downvotes INT NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) diff --git a/server/src/db.rs b/server/src/db.rs new file mode 100644 index 0000000..2afb3a4 --- /dev/null +++ b/server/src/db.rs @@ -0,0 +1,25 @@ +use crate::types::{NewPost, Post}; +use sqlx::{Any, AnyPool, Row}; + +fn db_get_user() -> String { + unimplemented!(); +} + +fn db_get_posts() -> Vec { + unimplemented!(); +} + +async fn db_new_post(post: NewPost, pool: &AnyPool) -> i32 { + let q = "INSERT INTO posts (content) VALUES (?)"; + let query = sqlx::query(q).bind(post.content); + let result = query.fetch_one(pool).await.ok(); + if let Some(row) = result { + row.get::("id") + } else { + panic!("Failed to insert post into database"); + } +} + +fn db_vote(post_id: i32, user_id: i32, upvote: bool) { + unimplemented!(); +} diff --git a/server/src/main.rs b/server/src/main.rs index ab9f334..d1a377d 100755 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -4,12 +4,13 @@ use actix_web::web::Data; use actix_web::{web::scope, App, HttpServer}; use log::info; +mod db; mod jwt; mod routes; mod state; mod types; -use routes::{get_posts, login, new_post, register, test}; +use routes::{get_posts, login, new_post, register}; use state::ServerState; #[actix_web::main] @@ -28,7 +29,6 @@ async fn main() -> std::io::Result<()> { .service(get_posts) .service(new_post) .service(routes::vote) - .service(test) .service(login) .service(register) .app_data(Data::new(data.clone())), diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs new file mode 100644 index 0000000..8413972 --- /dev/null +++ b/server/src/routes/mod.rs @@ -0,0 +1,7 @@ +mod post; +mod users; +mod vote; + +pub use post::*; +pub use users::*; +pub use vote::*; diff --git a/server/src/routes/post.rs b/server/src/routes/post.rs new file mode 100755 index 0000000..e2c465b --- /dev/null +++ b/server/src/routes/post.rs @@ -0,0 +1,27 @@ +use crate::jwt::token_factory; +use crate::types::{NewPost, Post}; +use crate::ServerState; + +use actix_web::web::{Data, Path}; +use actix_web::{get, post, web::Json, HttpResponse, Responder, Result}; +use argon2::password_hash::rand_core::OsRng; +use argon2::password_hash::SaltString; +use argon2::password_hash::*; +use argon2::Argon2; +use argon2::PasswordHasher; +use argon2::PasswordVerifier; +use log::*; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[get("/posts")] +pub async fn get_posts(data: Data) -> impl Responder { + HttpResponse::InternalServerError().body("Unimplemented") +} + +#[post("/posts")] +pub async fn new_post(new_post: Json, data: Data) -> impl Responder { + let post = Post::from(new_post.into_inner()); + info!("Created post {:?}", post.uuid); + HttpResponse::Ok().json("Post added!") +} diff --git a/server/src/routes.rs b/server/src/routes/users.rs similarity index 54% rename from server/src/routes.rs rename to server/src/routes/users.rs index ef19c75..e5b568b 100755 --- a/server/src/routes.rs +++ b/server/src/routes/users.rs @@ -14,95 +14,6 @@ use log::*; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[get("/")] -pub async fn get_posts(data: Data) -> impl Responder { - match data.posts.lock() { - Ok(posts) => { - let posts: Vec = posts.values().cloned().collect(); - HttpResponse::Ok().json(posts) - } - Err(e) => { - warn!("Error: {:?}", e); - HttpResponse::InternalServerError().body("Error") - } - } -} - -#[post("/")] -pub async fn new_post(new_post: Json, data: Data) -> impl Responder { - let post = Post::from(new_post.into_inner()); - info!("Created post {:?}", post.uuid); - - // let q = "INSERT INTO posts (uuid, content, upvotes, downvotes) VALUES (?, ?, ?, ?)"; - // let query = sqlx::query(q) - // .bind(post.uuid) - // .bind(post.content) - // .bind(post.votes.up) - // .bind(post.votes.down); - - match data.posts.lock() { - Ok(mut posts) => { - posts.insert(post.uuid, post); - } - Err(e) => { - warn!("Error: {:?}", e); - } - }; - - HttpResponse::Ok().json("Post added!") -} - -// This is a test route, returns "Hello, world!" -#[get("/test")] -pub async fn test(data: Data) -> impl Responder { - match data.posts.lock() { - Ok(posts) => { - let posts: Vec = posts.values().cloned().collect(); - HttpResponse::Ok().body(format!("Hello, world! {:?}", posts)) - } - Err(e) => { - warn!("Error: {:?}", e); - HttpResponse::InternalServerError().body("Error") - } - } -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum VoteDirection { - Up, - Down, - Unupvote, - Undownvote, -} - -#[post("vote/{uuid}/{direction}")] -pub async fn vote(params: Path<(Uuid, VoteDirection)>, data: Data) -> impl Responder { - let (uuid, direction) = params.into_inner(); - println!("Voting {:?} on post {:?}", direction, uuid); - - match data.posts.lock() { - Ok(mut posts) => { - let uuid = uuid; - if let Some(post) = posts.get_mut(&uuid) { - match direction { - VoteDirection::Up => post.votes.up += 1, - VoteDirection::Unupvote => post.votes.up -= 1, - VoteDirection::Down => post.votes.down += 1, - VoteDirection::Undownvote => post.votes.down -= 1, - } - HttpResponse::Ok().body("Downvoted!") - } else { - HttpResponse::NotFound().body("Post not found!") - } - } - Err(e) => { - warn!("Error: {:?}", e); - HttpResponse::InternalServerError().body("Error") - } - } -} - #[derive(Debug, Serialize, Deserialize)] pub struct RegisterData { username: String, diff --git a/server/src/routes/vote.rs b/server/src/routes/vote.rs new file mode 100644 index 0000000..a2dc987 --- /dev/null +++ b/server/src/routes/vote.rs @@ -0,0 +1,16 @@ +use crate::state::ServerState; +use actix_web::web::{Data, Path}; +use actix_web::{get, post, web::Json, HttpResponse, Responder, Result}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Serialize, Deserialize)] +enum VoteDirection { + Up, + Down, +} + +#[post("vote/{uuid}")] +pub async fn vote(params: Path<(Uuid, VoteDirection)>, data: Data) -> impl Responder { + HttpResponse::InternalServerError().body("Unimplemented") +} diff --git a/server/src/state.rs b/server/src/state.rs index 8699d93..39c66a0 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -9,7 +9,7 @@ use uuid::Uuid; #[derive(Clone)] pub struct ServerState { - pub posts: Arc>>, + // pub posts: Arc>>, pub pool: Pool, } @@ -24,7 +24,7 @@ impl ServerState { sqlx::migrate!("./migrations").run(&pool).await.unwrap(); Self { - posts: Arc::new(Mutex::new(BTreeMap::new())), + // posts: Arc::new(Mutex::new(BTreeMap::new())), pool: pool, } } diff --git a/server/src/types.rs b/server/src/types.rs index 267be22..e59b086 100755 --- a/server/src/types.rs +++ b/server/src/types.rs @@ -4,8 +4,8 @@ use uuid::Uuid; // The post as it is received from the client #[derive(Debug, Serialize, Deserialize)] pub struct NewPost { - content: String, - token: String, + pub content: String, + pub token: String, } // The post as it is stored in the database @@ -13,7 +13,8 @@ pub struct NewPost { pub struct Post { pub uuid: Uuid, pub content: String, - pub votes: VoteCount, + pub upvotes: i32, + pub downvotes: i32, } impl From for Post { @@ -21,20 +22,8 @@ impl From for Post { Self { uuid: Uuid::new_v4(), content: post.content, - votes: VoteCount::new(), + upvotes: 0, + downvotes: 0, } } } - -// Part of the post struct -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct VoteCount { - pub up: u32, - pub down: u32, -} - -impl VoteCount { - fn new() -> Self { - Self { up: 0, down: 0 } - } -} From f69823c08e510d43bc285620123061a106f5e330 Mon Sep 17 00:00:00 2001 From: Imbus Date: Fri, 20 Oct 2023 20:57:58 +0200 Subject: [PATCH 2/5] Lots of untested changes --- .../.containerignore | 0 server/Cargo.lock | 10 ++ server/Cargo.toml | 6 +- server/migrations/0001_users_table.sql | 2 +- server/migrations/0002_posts_table.sql | 4 +- server/src/db.rs | 41 ++--- server/src/main.rs | 3 +- server/src/routes/mod.rs | 2 - server/src/routes/post.rs | 63 +++++--- server/src/routes/users.rs | 140 ++++++++++-------- server/src/routes/vote.rs | 16 -- server/src/state.rs | 12 +- server/src/types.rs | 29 ---- 13 files changed, 169 insertions(+), 159 deletions(-) rename .containerignore => container/.containerignore (100%) delete mode 100644 server/src/routes/vote.rs delete mode 100755 server/src/types.rs diff --git a/.containerignore b/container/.containerignore similarity index 100% rename from .containerignore rename to container/.containerignore diff --git a/server/Cargo.lock b/server/Cargo.lock index 033f1e9..93d4286 100755 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -1782,6 +1782,7 @@ dependencies = [ "argon2", "chrono", "clap", + "dotenvy", "env_logger", "jsonwebtoken", "log", @@ -1945,6 +1946,7 @@ dependencies = [ "atoi", "byteorder", "bytes", + "chrono", "crc", "crossbeam-queue", "dotenvy", @@ -1973,6 +1975,7 @@ dependencies = [ "tokio-stream", "tracing", "url", + "uuid", ] [[package]] @@ -2006,6 +2009,7 @@ dependencies = [ "sha2", "sqlx-core", "sqlx-mysql", + "sqlx-postgres", "sqlx-sqlite", "syn 1.0.109", "tempfile", @@ -2024,6 +2028,7 @@ dependencies = [ "bitflags 2.4.0", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -2052,6 +2057,7 @@ dependencies = [ "stringprep", "thiserror", "tracing", + "uuid", "whoami", ] @@ -2065,6 +2071,7 @@ dependencies = [ "base64 0.21.4", "bitflags 2.4.0", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -2091,6 +2098,7 @@ dependencies = [ "stringprep", "thiserror", "tracing", + "uuid", "whoami", ] @@ -2101,6 +2109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", @@ -2114,6 +2123,7 @@ dependencies = [ "sqlx-core", "tracing", "url", + "uuid", ] [[package]] diff --git a/server/Cargo.toml b/server/Cargo.toml index 49e8521..8afda9d 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -11,11 +11,15 @@ actix-web = "4.4.0" argon2 = { version = "0.5.2", features = ["zeroize"] } chrono = { version = "0.4.31", features = ["serde"] } clap = { version = "4.4.5", features = ["derive"] } +dotenvy = "0.15.7" env_logger = "0.10.0" jsonwebtoken = "8.3.0" log = "0.4.20" serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.107" sled = { version = "0.34.7" } -sqlx = { version = "0.7.2", features = ["sqlite", "runtime-tokio"] } +sqlx = { version = "0.7.2", features = ["sqlite", "runtime-tokio", "chrono", "uuid"] } uuid = { version = "1.4.1", features = ["serde", "v4"] } + +[profile.dev.package.sqlx-macros] +opt-level = 3 diff --git a/server/migrations/0001_users_table.sql b/server/migrations/0001_users_table.sql index ae0b2b6..76976d2 100644 --- a/server/migrations/0001_users_table.sql +++ b/server/migrations/0001_users_table.sql @@ -1,5 +1,5 @@ CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, + id INTEGER PRIMARY KEY, username TEXT NOT NULL, password TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/server/migrations/0002_posts_table.sql b/server/migrations/0002_posts_table.sql index 97d6a83..c5d05e5 100644 --- a/server/migrations/0002_posts_table.sql +++ b/server/migrations/0002_posts_table.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS posts ( - id SERIAL PRIMARY KEY, - user_id SERIAL NOT NULL, + id INTEGER PRIMARY KEY NOT NULL, + user_id INTEGER NOT NULL, content TEXT NOT NULL, upvotes INT NOT NULL DEFAULT 0, downvotes INT NOT NULL DEFAULT 0, diff --git a/server/src/db.rs b/server/src/db.rs index 2afb3a4..69b90e6 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -1,25 +1,26 @@ -use crate::types::{NewPost, Post}; -use sqlx::{Any, AnyPool, Row}; +use crate::routes::{NewPost, Post}; +use sqlx::{Row, SqlitePool}; -fn db_get_user() -> String { - unimplemented!(); +// Gets all posts from the database +pub async fn db_get_posts(pool: &SqlitePool) -> Vec { + sqlx::query_as!(Post, "SELECT * FROM posts") + .fetch_all(pool) + .await + .unwrap() } -fn db_get_posts() -> Vec { - unimplemented!(); -} +// Inserts a new post to the database +pub async fn db_new_post(post: NewPost, pool: &SqlitePool) -> Option { + let q2 = sqlx::query!("INSERT INTO posts (content) VALUES (?)", post.content) + .execute(pool) + .await; -async fn db_new_post(post: NewPost, pool: &AnyPool) -> i32 { - let q = "INSERT INTO posts (content) VALUES (?)"; - let query = sqlx::query(q).bind(post.content); - let result = query.fetch_one(pool).await.ok(); - if let Some(row) = result { - row.get::("id") - } else { - panic!("Failed to insert post into database"); - } -} - -fn db_vote(post_id: i32, user_id: i32, upvote: bool) { - unimplemented!(); + let q = sqlx::query_as!( + Post, + "SELECT * FROM posts WHERE id = (SELECT MAX(id) FROM posts)" + ) + .fetch_one(pool) + .await + .ok()?; + Some(q) } diff --git a/server/src/main.rs b/server/src/main.rs index d1a377d..9192906 100755 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,3 +1,4 @@ +// #![allow(unused_imports, dead_code, unused_variables, unused_mut)] use actix_files::Files; use actix_web::middleware; use actix_web::web::Data; @@ -8,7 +9,6 @@ mod db; mod jwt; mod routes; mod state; -mod types; use routes::{get_posts, login, new_post, register}; use state::ServerState; @@ -28,7 +28,6 @@ async fn main() -> std::io::Result<()> { scope("/api") .service(get_posts) .service(new_post) - .service(routes::vote) .service(login) .service(register) .app_data(Data::new(data.clone())), diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 8413972..c02003b 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -1,7 +1,5 @@ mod post; mod users; -mod vote; pub use post::*; pub use users::*; -pub use vote::*; diff --git a/server/src/routes/post.rs b/server/src/routes/post.rs index e2c465b..cc7970c 100755 --- a/server/src/routes/post.rs +++ b/server/src/routes/post.rs @@ -1,27 +1,56 @@ -use crate::jwt::token_factory; -use crate::types::{NewPost, Post}; +use crate::db::db_new_post; use crate::ServerState; -use actix_web::web::{Data, Path}; +use actix_web::web::Data; use actix_web::{get, post, web::Json, HttpResponse, Responder, Result}; -use argon2::password_hash::rand_core::OsRng; -use argon2::password_hash::SaltString; -use argon2::password_hash::*; -use argon2::Argon2; -use argon2::PasswordHasher; -use argon2::PasswordVerifier; -use log::*; +use log::info; use serde::{Deserialize, Serialize}; -use uuid::Uuid; +use sqlx::FromRow; + +// The post as it is received from the client +// The token is used to identify the user +#[derive(Debug, Serialize, Deserialize)] +pub struct NewPost { + pub content: String, + pub token: String, +} + +// The post as it is stored in the database, with all the related metadata +#[derive(Debug, Serialize, Deserialize, Clone, FromRow)] +pub struct Post { + pub id: i64, + pub user_id: i64, + pub content: String, + pub upvotes: i64, + pub downvotes: i64, + pub created_at: chrono::NaiveDateTime, + pub updated_at: chrono::NaiveDateTime, +} + +#[derive(Debug, Serialize, Deserialize, Clone, FromRow)] +pub struct User { + pub id: i64, + pub username: String, + pub password: String, + pub created_at: chrono::NaiveDateTime, + pub updated_at: chrono::NaiveDateTime, +} #[get("/posts")] -pub async fn get_posts(data: Data) -> impl Responder { - HttpResponse::InternalServerError().body("Unimplemented") +pub async fn get_posts(state: Data) -> Result { + let stream = sqlx::query_as!(Post, "SELECT * FROM posts"); + + let posts = stream.fetch_all(&state.pool).await.unwrap(); + Ok(HttpResponse::Ok().json(posts)) } #[post("/posts")] -pub async fn new_post(new_post: Json, data: Data) -> impl Responder { - let post = Post::from(new_post.into_inner()); - info!("Created post {:?}", post.uuid); - HttpResponse::Ok().json("Post added!") +pub async fn new_post(new_post: Json, state: Data) -> Result { + return match db_new_post(new_post.into_inner(), &state.pool).await { + Some(post) => { + info!("Created post {:?}", post.id); + Ok(HttpResponse::Ok().json(post)) + } + None => Ok(HttpResponse::InternalServerError().json("Error")), + }; } diff --git a/server/src/routes/users.rs b/server/src/routes/users.rs index e5b568b..e5cdec8 100755 --- a/server/src/routes/users.rs +++ b/server/src/routes/users.rs @@ -1,9 +1,8 @@ use crate::jwt::token_factory; -use crate::types::{NewPost, Post}; use crate::ServerState; -use actix_web::web::{Data, Path}; -use actix_web::{get, post, web::Json, HttpResponse, Responder, Result}; +use actix_web::web::Data; +use actix_web::{post, web::Json, HttpResponse, Responder, Result}; use argon2::password_hash::rand_core::OsRng; use argon2::password_hash::SaltString; use argon2::password_hash::*; @@ -12,7 +11,18 @@ use argon2::PasswordHasher; use argon2::PasswordVerifier; use log::*; use serde::{Deserialize, Serialize}; -use uuid::Uuid; + +#[derive(Debug, Serialize, Deserialize)] +pub struct LoginData { + username: String, + password: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct LoginResponse { + username: String, + token: String, +} #[derive(Debug, Serialize, Deserialize)] pub struct RegisterData { @@ -26,75 +36,81 @@ pub async fn register( data: Json, state: Data, ) -> Result { - let q = "SELECT password FROM users WHERE username = ?"; - let query = sqlx::query(q).bind(&data.username); - let result = query.fetch_one(&state.pool).await.ok(); - if result.is_some() { + // First check if the user already exists + let exists = sqlx::query!( + "SELECT username FROM users WHERE username = ?", + data.username + ) + .fetch_one(&state.pool) + .await + .ok() + .map(|row| row.username); + + // Bail out if the user already exists + if exists.is_some() { info!("User \"{}\" already exists", data.username); return Ok(HttpResponse::BadRequest().json("Error")); } - let password = data.password.clone(); - let salt = SaltString::generate(&mut OsRng); - let phc_hash = Argon2::default().hash_password(password.as_bytes(), &salt); - if let Ok(phc_hash) = phc_hash { - info!("User: {} registered", data.username); - let phc_hash = phc_hash.to_string(); - let q = "INSERT INTO users (username, password) VALUES (?, ?)"; - let query = sqlx::query(q).bind(&data.username).bind(&phc_hash); - query.execute(&state.pool).await.unwrap(); - } else { - return Ok(HttpResponse::BadRequest().json("Error")); - } + // Unwrapping here because if this fails, we have a serious problem + let phc_hash = Argon2::default() + .hash_password(data.password.as_bytes(), &SaltString::generate(&mut OsRng)) + .unwrap() + .to_string(); + // Insert our new user into the database + sqlx::query!( + "INSERT INTO users (username, password) VALUES (?, ?)", + data.username, + phc_hash + ) + .execute(&state.pool) + .await + .unwrap(); + + info!("User: {} registered", data.username); Ok(HttpResponse::Ok().json("User registered")) } -#[derive(Debug, Serialize, Deserialize)] -pub struct LoginData { - username: String, - password: String, -} - -use sqlx::Row; - -#[derive(Debug, Serialize, Deserialize)] -struct LoginResponse { - username: String, - token: String, -} - #[post("/login")] pub async fn login(data: Json, state: Data) -> Result { - let q = "SELECT password FROM users WHERE username = ?"; - let query = sqlx::query(q).bind(&data.username); - let result = query.fetch_one(&state.pool).await.ok(); - if let Some(row) = result { - let phc_from_db = row.get::("password"); - let pwhash = PasswordHash::new(&phc_from_db).unwrap_or_else(|_| { - warn!( - "Invalid hash for user {} fetched from database (not a valid PHC string)", - data.username - ); - panic!(); - }); + // let q = "SELECT password FROM users WHERE username = ?"; + // let query = sqlx::query(q).bind(&data.username); + // let result = query.fetch_one(&state.pool).await.ok(); - match Argon2::default().verify_password(data.password.as_bytes(), &pwhash) { - Ok(_) => { - info!("User {} logged in", data.username); - let token = token_factory(&data.username).unwrap(); - println!("{:?}", token); - return Ok(HttpResponse::Ok().json(LoginResponse { - username: data.username.clone(), - token: token, - })); - } - Err(_) => { - info!("User \"{}\" failed to log in", data.username); - return Ok(HttpResponse::BadRequest().json("Error")); - } - } + let uname = data.username.clone(); + let q = sqlx::query!("SELECT password FROM users WHERE username = ?", uname) + .fetch_one(&state.pool) + .await + .ok(); + + if q.is_none() { + info!("User \"{}\" failed to log in", data.username); + return Ok(HttpResponse::BadRequest().json("Error")); } - Ok(HttpResponse::Ok().json("What happens here???")) + let phc_password = q.unwrap().password; + let phc_password = PasswordHash::new(&phc_password).unwrap_or_else(|_| { + warn!( + "Invalid hash for user {} fetched from database (not a valid PHC string)", + data.username + ); + panic!(); + }); + + match Argon2::default().verify_password(data.password.as_bytes(), &phc_password) { + Ok(_) => { + info!("User {} logged in", data.username); + let token = token_factory(&data.username).unwrap(); + println!("{:?}", token); + return Ok(HttpResponse::Ok().json(LoginResponse { + username: data.username.clone(), + token: token, + })); + } + Err(_) => { + info!("User \"{}\" failed to log in", data.username); + return Ok(HttpResponse::BadRequest().json("Error")); + } + } } diff --git a/server/src/routes/vote.rs b/server/src/routes/vote.rs deleted file mode 100644 index a2dc987..0000000 --- a/server/src/routes/vote.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::state::ServerState; -use actix_web::web::{Data, Path}; -use actix_web::{get, post, web::Json, HttpResponse, Responder, Result}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -#[derive(Debug, Serialize, Deserialize)] -enum VoteDirection { - Up, - Down, -} - -#[post("vote/{uuid}")] -pub async fn vote(params: Path<(Uuid, VoteDirection)>, data: Data) -> impl Responder { - HttpResponse::InternalServerError().body("Unimplemented") -} diff --git a/server/src/state.rs b/server/src/state.rs index 39c66a0..c1e0339 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -1,23 +1,21 @@ -use crate::types::Post; use sqlx::Pool; use sqlx::Sqlite; use sqlx::{self, sqlite}; -use std::collections::BTreeMap; -use std::sync::Arc; -use std::sync::Mutex; -use uuid::Uuid; #[derive(Clone)] pub struct ServerState { - // pub posts: Arc>>, pub pool: Pool, } impl ServerState { pub async fn new() -> Self { + // This is almost certainly bad practice for more reasons than I can count + dotenvy::dotenv().ok(); + let db_url = dotenvy::var("DATABASE_URL").unwrap_or("sqlite:./db.sqlite".to_string()); + let pool = sqlite::SqlitePoolOptions::new() .max_connections(5) - .connect(":memory:") + .connect(&db_url) .await .unwrap(); diff --git a/server/src/types.rs b/server/src/types.rs deleted file mode 100755 index e59b086..0000000 --- a/server/src/types.rs +++ /dev/null @@ -1,29 +0,0 @@ -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -// The post as it is received from the client -#[derive(Debug, Serialize, Deserialize)] -pub struct NewPost { - pub content: String, - pub token: String, -} - -// The post as it is stored in the database -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Post { - pub uuid: Uuid, - pub content: String, - pub upvotes: i32, - pub downvotes: i32, -} - -impl From for Post { - fn from(post: NewPost) -> Self { - Self { - uuid: Uuid::new_v4(), - content: post.content, - upvotes: 0, - downvotes: 0, - } - } -} From 6c2f7dd891f0b5a15e184b1b4a7fbba37a16cbb3 Mon Sep 17 00:00:00 2001 From: Imbus Date: Fri, 20 Oct 2023 22:47:25 +0200 Subject: [PATCH 3/5] Moved and updated .containerignore --- container/.containerignore => .containerignore | 3 +++ 1 file changed, 3 insertions(+) rename container/.containerignore => .containerignore (89%) diff --git a/container/.containerignore b/.containerignore similarity index 89% rename from container/.containerignore rename to .containerignore index 56c261b..2a485ca 100644 --- a/container/.containerignore +++ b/.containerignore @@ -3,7 +3,10 @@ # Server files /server/target +**/target # Client files /client/node_modules /client/dist +**/node_modules +**/dist \ No newline at end of file From 8cfbfd17b607214aa23a788aef46d60018ee5f7b Mon Sep 17 00:00:00 2001 From: Imbus Date: Fri, 20 Oct 2023 22:48:42 +0200 Subject: [PATCH 4/5] .gitignore bump --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index a547bf3..135ea6f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,9 @@ dist-ssr *.njsproj *.sln *.sw? + +*.env +*.db* + +# Remove this if ever needed +*.sqlx From 0ea21cfaf3d35131b95ab25ad3c181c1befd016b Mon Sep 17 00:00:00 2001 From: Imbus Date: Fri, 20 Oct 2023 22:49:09 +0200 Subject: [PATCH 5/5] Changes related to api in both client and server --- client-solid/src/api.ts | 6 +++--- server/src/db.rs | 16 +++++++++++++--- server/src/main.rs | 1 + server/src/state.rs | 39 ++++++++++++++++++++++++++++++++++----- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/client-solid/src/api.ts b/client-solid/src/api.ts index 70b9a6f..b1ed3a6 100644 --- a/client-solid/src/api.ts +++ b/client-solid/src/api.ts @@ -20,20 +20,20 @@ export interface Post extends NewPost { export async function getPosts(): Promise { // const res = await fetch(`${API_URL}/posts`); - const res = await fetch("/api/"); + const res = await fetch("/api/posts"); const data = await res.json(); return data; } export async function getPost(id: string): Promise { - const res = await fetch(`/api/${id}`); + const res = await fetch(`/api/posts/${id}`); const data = await res.json(); return data; } export async function createPost(post: NewPost): Promise { // await fetch(`${API_URL}`, { - await fetch("/api/", { + await fetch("/api/posts", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/server/src/db.rs b/server/src/db.rs index 69b90e6..c2f0f39 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -1,4 +1,5 @@ use crate::routes::{NewPost, Post}; +use log::warn; use sqlx::{Row, SqlitePool}; // Gets all posts from the database @@ -11,9 +12,18 @@ pub async fn db_get_posts(pool: &SqlitePool) -> Vec { // Inserts a new post to the database pub async fn db_new_post(post: NewPost, pool: &SqlitePool) -> Option { - let q2 = sqlx::query!("INSERT INTO posts (content) VALUES (?)", post.content) - .execute(pool) - .await; + let q2 = sqlx::query!( + "INSERT INTO posts (user_id, content) VALUES (1, ?)", + post.content + ) + .execute(pool) + .await; + + if q2.is_err() { + let s = q2.err().unwrap(); + warn!("Error inserting post into database: {}", s); + return None; + } let q = sqlx::query_as!( Post, diff --git a/server/src/main.rs b/server/src/main.rs index 9192906..18e07d2 100755 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -24,6 +24,7 @@ async fn main() -> std::io::Result<()> { App::new() .wrap(middleware::Compress::default()) .wrap(middleware::Logger::default()) + .wrap(middleware::NormalizePath::trim()) .service( scope("/api") .service(get_posts) diff --git a/server/src/state.rs b/server/src/state.rs index c1e0339..783b08d 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -1,5 +1,6 @@ use sqlx::Pool; use sqlx::Sqlite; +use sqlx::SqlitePool; use sqlx::{self, sqlite}; #[derive(Clone)] @@ -11,7 +12,7 @@ impl ServerState { pub async fn new() -> Self { // This is almost certainly bad practice for more reasons than I can count dotenvy::dotenv().ok(); - let db_url = dotenvy::var("DATABASE_URL").unwrap_or("sqlite:./db.sqlite".to_string()); + let db_url = dotenvy::var("DATABASE_URL").unwrap_or(":memory:".to_string()); let pool = sqlite::SqlitePoolOptions::new() .max_connections(5) @@ -21,9 +22,37 @@ impl ServerState { sqlx::migrate!("./migrations").run(&pool).await.unwrap(); - Self { - // posts: Arc::new(Mutex::new(BTreeMap::new())), - pool: pool, - } + #[cfg(debug_assertions)] + debug_setup(&pool).await; + + Self { pool } } } + +// Inserts a bunch of dummy data into the database +// Mostly useful for debugging new posts, as we need to satisfy foreign key constraints. +#[cfg(debug_assertions)] +async fn debug_setup(pool: &SqlitePool) { + use chrono::NaiveDateTime; + use sqlx::query; + + let now = NaiveDateTime::from_timestamp(0, 0); + + query!( + "INSERT INTO users (username, password, created_at, updated_at) VALUES ('test', 'test', ?, ?)", + now, + now + ) + .execute(pool) + .await + .unwrap(); + + query!( + "INSERT INTO posts (user_id, content, created_at, updated_at) VALUES (1, 'Hello world!', ?, ?)", + now, + now + ) + .execute(pool) + .await + .unwrap(); +}