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",
"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]]

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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<Post> {
sqlx::query_as!(Post, "SELECT * FROM posts")
.fetch_all(pool)
.await
.unwrap()
}
fn db_get_posts() -> Vec<Post> {
unimplemented!();
}
// Inserts a new post to the database
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 = "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::<i32, _>("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)
}

View file

@ -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())),

View file

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

View file

@ -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<ServerState>) -> impl Responder {
HttpResponse::InternalServerError().body("Unimplemented")
pub async fn get_posts(state: Data<ServerState>) -> Result<impl Responder> {
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<NewPost>, data: Data<ServerState>) -> 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<NewPost>, state: Data<ServerState>) -> Result<impl Responder> {
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")),
};
}

View file

@ -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<RegisterData>,
state: Data<ServerState>,
) -> Result<impl Responder> {
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<LoginData>, state: Data<ServerState>) -> Result<impl Responder> {
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::<String, _>("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"));
}
}
}

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::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<Mutex<BTreeMap<Uuid, Post>>>,
pub pool: Pool<Sqlite>,
}
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();

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,
}
}
}