Lots of untested changes

This commit is contained in:
Imbus 2023-10-20 20:57:58 +02:00
parent 27386d5d18
commit f69823c08e
13 changed files with 169 additions and 159 deletions

10
server/Cargo.lock generated
View file

@ -1782,6 +1782,7 @@ dependencies = [
"argon2", "argon2",
"chrono", "chrono",
"clap", "clap",
"dotenvy",
"env_logger", "env_logger",
"jsonwebtoken", "jsonwebtoken",
"log", "log",
@ -1945,6 +1946,7 @@ dependencies = [
"atoi", "atoi",
"byteorder", "byteorder",
"bytes", "bytes",
"chrono",
"crc", "crc",
"crossbeam-queue", "crossbeam-queue",
"dotenvy", "dotenvy",
@ -1973,6 +1975,7 @@ dependencies = [
"tokio-stream", "tokio-stream",
"tracing", "tracing",
"url", "url",
"uuid",
] ]
[[package]] [[package]]
@ -2006,6 +2009,7 @@ dependencies = [
"sha2", "sha2",
"sqlx-core", "sqlx-core",
"sqlx-mysql", "sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite", "sqlx-sqlite",
"syn 1.0.109", "syn 1.0.109",
"tempfile", "tempfile",
@ -2024,6 +2028,7 @@ dependencies = [
"bitflags 2.4.0", "bitflags 2.4.0",
"byteorder", "byteorder",
"bytes", "bytes",
"chrono",
"crc", "crc",
"digest", "digest",
"dotenvy", "dotenvy",
@ -2052,6 +2057,7 @@ dependencies = [
"stringprep", "stringprep",
"thiserror", "thiserror",
"tracing", "tracing",
"uuid",
"whoami", "whoami",
] ]
@ -2065,6 +2071,7 @@ dependencies = [
"base64 0.21.4", "base64 0.21.4",
"bitflags 2.4.0", "bitflags 2.4.0",
"byteorder", "byteorder",
"chrono",
"crc", "crc",
"dotenvy", "dotenvy",
"etcetera", "etcetera",
@ -2091,6 +2098,7 @@ dependencies = [
"stringprep", "stringprep",
"thiserror", "thiserror",
"tracing", "tracing",
"uuid",
"whoami", "whoami",
] ]
@ -2101,6 +2109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f"
dependencies = [ dependencies = [
"atoi", "atoi",
"chrono",
"flume", "flume",
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@ -2114,6 +2123,7 @@ dependencies = [
"sqlx-core", "sqlx-core",
"tracing", "tracing",
"url", "url",
"uuid",
] ]
[[package]] [[package]]

View file

@ -11,11 +11,15 @@ actix-web = "4.4.0"
argon2 = { version = "0.5.2", features = ["zeroize"] } argon2 = { version = "0.5.2", features = ["zeroize"] }
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
clap = { version = "4.4.5", features = ["derive"] } clap = { version = "4.4.5", features = ["derive"] }
dotenvy = "0.15.7"
env_logger = "0.10.0" env_logger = "0.10.0"
jsonwebtoken = "8.3.0" jsonwebtoken = "8.3.0"
log = "0.4.20" log = "0.4.20"
serde = { version = "1.0.188", features = ["derive"] } serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.107" serde_json = "1.0.107"
sled = { version = "0.34.7" } 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"] } uuid = { version = "1.4.1", features = ["serde", "v4"] }
[profile.dev.package.sqlx-macros]
opt-level = 3

View file

@ -1,5 +1,5 @@
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY, id INTEGER PRIMARY KEY,
username TEXT NOT NULL, username TEXT NOT NULL,
password TEXT NOT NULL, password TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

View file

@ -1,6 +1,6 @@
CREATE TABLE IF NOT EXISTS posts ( CREATE TABLE IF NOT EXISTS posts (
id SERIAL PRIMARY KEY, id INTEGER PRIMARY KEY NOT NULL,
user_id SERIAL NOT NULL, user_id INTEGER NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
upvotes INT NOT NULL DEFAULT 0, upvotes INT NOT NULL DEFAULT 0,
downvotes INT NOT NULL DEFAULT 0, downvotes INT NOT NULL DEFAULT 0,

View file

@ -1,25 +1,26 @@
use crate::types::{NewPost, Post}; use crate::routes::{NewPost, Post};
use sqlx::{Any, AnyPool, Row}; use sqlx::{Row, SqlitePool};
fn db_get_user() -> String { // Gets all posts from the database
unimplemented!(); pub async fn db_get_posts(pool: &SqlitePool) -> Vec<Post> {
sqlx::query_as!(Post, "SELECT * FROM posts")
.fetch_all(pool)
.await
.unwrap()
} }
fn db_get_posts() -> Vec<Post> { // Inserts a new post to the database
unimplemented!(); pub async fn db_new_post(post: NewPost, pool: &SqlitePool) -> Option<Post> {
} 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 = sqlx::query_as!(
let q = "INSERT INTO posts (content) VALUES (?)"; Post,
let query = sqlx::query(q).bind(post.content); "SELECT * FROM posts WHERE id = (SELECT MAX(id) FROM posts)"
let result = query.fetch_one(pool).await.ok(); )
if let Some(row) = result { .fetch_one(pool)
row.get::<i32, _>("id") .await
} else { .ok()?;
panic!("Failed to insert post into database"); Some(q)
}
}
fn db_vote(post_id: i32, user_id: i32, upvote: bool) {
unimplemented!();
} }

View file

@ -1,3 +1,4 @@
// #![allow(unused_imports, dead_code, unused_variables, unused_mut)]
use actix_files::Files; use actix_files::Files;
use actix_web::middleware; use actix_web::middleware;
use actix_web::web::Data; use actix_web::web::Data;
@ -8,7 +9,6 @@ mod db;
mod jwt; mod jwt;
mod routes; mod routes;
mod state; mod state;
mod types;
use routes::{get_posts, login, new_post, register}; use routes::{get_posts, login, new_post, register};
use state::ServerState; use state::ServerState;
@ -28,7 +28,6 @@ async fn main() -> std::io::Result<()> {
scope("/api") scope("/api")
.service(get_posts) .service(get_posts)
.service(new_post) .service(new_post)
.service(routes::vote)
.service(login) .service(login)
.service(register) .service(register)
.app_data(Data::new(data.clone())), .app_data(Data::new(data.clone())),

View file

@ -1,7 +1,5 @@
mod post; mod post;
mod users; mod users;
mod vote;
pub use post::*; pub use post::*;
pub use users::*; pub use users::*;
pub use vote::*;

View file

@ -1,27 +1,56 @@
use crate::jwt::token_factory; use crate::db::db_new_post;
use crate::types::{NewPost, Post};
use crate::ServerState; 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 actix_web::{get, post, web::Json, HttpResponse, Responder, Result};
use argon2::password_hash::rand_core::OsRng; use log::info;
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 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")] #[get("/posts")]
pub async fn get_posts(data: Data<ServerState>) -> impl Responder { pub async fn get_posts(state: Data<ServerState>) -> Result<impl Responder> {
HttpResponse::InternalServerError().body("Unimplemented") 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")] #[post("/posts")]
pub async fn new_post(new_post: Json<NewPost>, data: Data<ServerState>) -> impl Responder { pub async fn new_post(new_post: Json<NewPost>, state: Data<ServerState>) -> Result<impl Responder> {
let post = Post::from(new_post.into_inner()); return match db_new_post(new_post.into_inner(), &state.pool).await {
info!("Created post {:?}", post.uuid); Some(post) => {
HttpResponse::Ok().json("Post added!") info!("Created post {:?}", post.id);
Ok(HttpResponse::Ok().json(post))
}
None => Ok(HttpResponse::InternalServerError().json("Error")),
};
} }

View file

@ -1,9 +1,8 @@
use crate::jwt::token_factory; use crate::jwt::token_factory;
use crate::types::{NewPost, Post};
use crate::ServerState; 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 actix_web::{post, web::Json, HttpResponse, Responder, Result};
use argon2::password_hash::rand_core::OsRng; use argon2::password_hash::rand_core::OsRng;
use argon2::password_hash::SaltString; use argon2::password_hash::SaltString;
use argon2::password_hash::*; use argon2::password_hash::*;
@ -12,7 +11,18 @@ use argon2::PasswordHasher;
use argon2::PasswordVerifier; use argon2::PasswordVerifier;
use log::*; use log::*;
use serde::{Deserialize, Serialize}; 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)] #[derive(Debug, Serialize, Deserialize)]
pub struct RegisterData { pub struct RegisterData {
@ -26,52 +36,61 @@ pub async fn register(
data: Json<RegisterData>, data: Json<RegisterData>,
state: Data<ServerState>, state: Data<ServerState>,
) -> Result<impl Responder> { ) -> Result<impl Responder> {
let q = "SELECT password FROM users WHERE username = ?"; // First check if the user already exists
let query = sqlx::query(q).bind(&data.username); let exists = sqlx::query!(
let result = query.fetch_one(&state.pool).await.ok(); "SELECT username FROM users WHERE username = ?",
if result.is_some() { 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); info!("User \"{}\" already exists", data.username);
return Ok(HttpResponse::BadRequest().json("Error")); return Ok(HttpResponse::BadRequest().json("Error"));
} }
let password = data.password.clone(); // Unwrapping here because if this fails, we have a serious problem
let salt = SaltString::generate(&mut OsRng); let phc_hash = Argon2::default()
let phc_hash = Argon2::default().hash_password(password.as_bytes(), &salt); .hash_password(data.password.as_bytes(), &SaltString::generate(&mut OsRng))
if let Ok(phc_hash) = phc_hash { .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); 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"));
}
Ok(HttpResponse::Ok().json("User registered")) 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")] #[post("/login")]
pub async fn login(data: Json<LoginData>, state: Data<ServerState>) -> Result<impl Responder> { pub async fn login(data: Json<LoginData>, state: Data<ServerState>) -> Result<impl Responder> {
let q = "SELECT password FROM users WHERE username = ?"; // let q = "SELECT password FROM users WHERE username = ?";
let query = sqlx::query(q).bind(&data.username); // let query = sqlx::query(q).bind(&data.username);
let result = query.fetch_one(&state.pool).await.ok(); // let result = query.fetch_one(&state.pool).await.ok();
if let Some(row) = result {
let phc_from_db = row.get::<String, _>("password"); let uname = data.username.clone();
let pwhash = PasswordHash::new(&phc_from_db).unwrap_or_else(|_| { 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"));
}
let phc_password = q.unwrap().password;
let phc_password = PasswordHash::new(&phc_password).unwrap_or_else(|_| {
warn!( warn!(
"Invalid hash for user {} fetched from database (not a valid PHC string)", "Invalid hash for user {} fetched from database (not a valid PHC string)",
data.username data.username
@ -79,7 +98,7 @@ pub async fn login(data: Json<LoginData>, state: Data<ServerState>) -> Result<im
panic!(); panic!();
}); });
match Argon2::default().verify_password(data.password.as_bytes(), &pwhash) { match Argon2::default().verify_password(data.password.as_bytes(), &phc_password) {
Ok(_) => { Ok(_) => {
info!("User {} logged in", data.username); info!("User {} logged in", data.username);
let token = token_factory(&data.username).unwrap(); let token = token_factory(&data.username).unwrap();
@ -94,7 +113,4 @@ pub async fn login(data: Json<LoginData>, state: Data<ServerState>) -> Result<im
return Ok(HttpResponse::BadRequest().json("Error")); return Ok(HttpResponse::BadRequest().json("Error"));
} }
} }
}
Ok(HttpResponse::Ok().json("What happens here???"))
} }

View file

@ -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<ServerState>) -> impl Responder {
HttpResponse::InternalServerError().body("Unimplemented")
}

View file

@ -1,23 +1,21 @@
use crate::types::Post;
use sqlx::Pool; use sqlx::Pool;
use sqlx::Sqlite; use sqlx::Sqlite;
use sqlx::{self, sqlite}; use sqlx::{self, sqlite};
use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::Mutex;
use uuid::Uuid;
#[derive(Clone)] #[derive(Clone)]
pub struct ServerState { pub struct ServerState {
// pub posts: Arc<Mutex<BTreeMap<Uuid, Post>>>,
pub pool: Pool<Sqlite>, pub pool: Pool<Sqlite>,
} }
impl ServerState { impl ServerState {
pub async fn new() -> Self { 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() let pool = sqlite::SqlitePoolOptions::new()
.max_connections(5) .max_connections(5)
.connect(":memory:") .connect(&db_url)
.await .await
.unwrap(); .unwrap();

View file

@ -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<NewPost> for Post {
fn from(post: NewPost) -> Self {
Self {
uuid: Uuid::new_v4(),
content: post.content,
upvotes: 0,
downvotes: 0,
}
}
}