Merge branch 'dev'
This commit is contained in:
		
						commit
						419b50024f
					
				
					 15 changed files with 287 additions and 251 deletions
				
			
		| 
						 | 
				
			
			@ -3,7 +3,10 @@
 | 
			
		|||
 | 
			
		||||
# Server files
 | 
			
		||||
/server/target
 | 
			
		||||
**/target
 | 
			
		||||
 | 
			
		||||
# Client files
 | 
			
		||||
/client/node_modules
 | 
			
		||||
/client/dist
 | 
			
		||||
**/node_modules
 | 
			
		||||
**/dist
 | 
			
		||||
							
								
								
									
										6
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -22,3 +22,9 @@ dist-ssr
 | 
			
		|||
*.njsproj
 | 
			
		||||
*.sln
 | 
			
		||||
*.sw?
 | 
			
		||||
 | 
			
		||||
*.env
 | 
			
		||||
*.db*
 | 
			
		||||
 | 
			
		||||
# Remove this if ever needed
 | 
			
		||||
*.sqlx
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,20 +20,20 @@ export interface Post extends NewPost {
 | 
			
		|||
 | 
			
		||||
export async function getPosts(): Promise<Post[]> {
 | 
			
		||||
//   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<Post> {
 | 
			
		||||
  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<void> {
 | 
			
		||||
//   await fetch(`${API_URL}`, {
 | 
			
		||||
  await fetch("/api/", {
 | 
			
		||||
  await fetch("/api/posts", {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    headers: {
 | 
			
		||||
      "Content-Type": "application/json",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										10
									
								
								server/Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								server/Cargo.lock
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -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]]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,9 @@
 | 
			
		|||
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,
 | 
			
		||||
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
    FOREIGN KEY (user_id) REFERENCES users (id)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										36
									
								
								server/src/db.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								server/src/db.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
use crate::routes::{NewPost, Post};
 | 
			
		||||
use log::warn;
 | 
			
		||||
use sqlx::{Row, SqlitePool};
 | 
			
		||||
 | 
			
		||||
// 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()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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 (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,
 | 
			
		||||
        "SELECT * FROM posts WHERE id = (SELECT MAX(id) FROM posts)"
 | 
			
		||||
    )
 | 
			
		||||
    .fetch_one(pool)
 | 
			
		||||
    .await
 | 
			
		||||
    .ok()?;
 | 
			
		||||
    Some(q)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,15 +1,16 @@
 | 
			
		|||
// #![allow(unused_imports, dead_code, unused_variables, unused_mut)]
 | 
			
		||||
use actix_files::Files;
 | 
			
		||||
use actix_web::middleware;
 | 
			
		||||
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]
 | 
			
		||||
| 
						 | 
				
			
			@ -23,12 +24,11 @@ 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)
 | 
			
		||||
                    .service(new_post)
 | 
			
		||||
                    .service(routes::vote)
 | 
			
		||||
                    .service(test)
 | 
			
		||||
                    .service(login)
 | 
			
		||||
                    .service(register)
 | 
			
		||||
                    .app_data(Data::new(data.clone())),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,189 +0,0 @@
 | 
			
		|||
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("/")]
 | 
			
		||||
pub async fn get_posts(data: Data<ServerState>) -> impl Responder {
 | 
			
		||||
    match data.posts.lock() {
 | 
			
		||||
        Ok(posts) => {
 | 
			
		||||
            let posts: Vec<Post> = 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<NewPost>, data: Data<ServerState>) -> 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<ServerState>) -> impl Responder {
 | 
			
		||||
    match data.posts.lock() {
 | 
			
		||||
        Ok(posts) => {
 | 
			
		||||
            let posts: Vec<Post> = 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<ServerState>) -> 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,
 | 
			
		||||
    password: String,
 | 
			
		||||
    captcha: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[post("/register")]
 | 
			
		||||
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() {
 | 
			
		||||
        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"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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!();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        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"));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(HttpResponse::Ok().json("What happens here???"))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								server/src/routes/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								server/src/routes/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
mod post;
 | 
			
		||||
mod users;
 | 
			
		||||
 | 
			
		||||
pub use post::*;
 | 
			
		||||
pub use users::*;
 | 
			
		||||
							
								
								
									
										56
									
								
								server/src/routes/post.rs
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										56
									
								
								server/src/routes/post.rs
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,56 @@
 | 
			
		|||
use crate::db::db_new_post;
 | 
			
		||||
use crate::ServerState;
 | 
			
		||||
 | 
			
		||||
use actix_web::web::Data;
 | 
			
		||||
use actix_web::{get, post, web::Json, HttpResponse, Responder, Result};
 | 
			
		||||
use log::info;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
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(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>, 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")),
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										116
									
								
								server/src/routes/users.rs
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										116
									
								
								server/src/routes/users.rs
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,116 @@
 | 
			
		|||
use crate::jwt::token_factory;
 | 
			
		||||
use crate::ServerState;
 | 
			
		||||
 | 
			
		||||
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::*;
 | 
			
		||||
use argon2::Argon2;
 | 
			
		||||
use argon2::PasswordHasher;
 | 
			
		||||
use argon2::PasswordVerifier;
 | 
			
		||||
use log::*;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
#[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 {
 | 
			
		||||
    username: String,
 | 
			
		||||
    password: String,
 | 
			
		||||
    captcha: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[post("/register")]
 | 
			
		||||
pub async fn register(
 | 
			
		||||
    data: Json<RegisterData>,
 | 
			
		||||
    state: Data<ServerState>,
 | 
			
		||||
) -> Result<impl Responder> {
 | 
			
		||||
    // 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"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[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();
 | 
			
		||||
 | 
			
		||||
    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"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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"));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,31 +1,58 @@
 | 
			
		|||
use crate::types::Post;
 | 
			
		||||
use sqlx::Pool;
 | 
			
		||||
use sqlx::Sqlite;
 | 
			
		||||
use sqlx::SqlitePool;
 | 
			
		||||
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(":memory:".to_string());
 | 
			
		||||
 | 
			
		||||
        let pool = sqlite::SqlitePoolOptions::new()
 | 
			
		||||
            .max_connections(5)
 | 
			
		||||
            .connect(":memory:")
 | 
			
		||||
            .connect(&db_url)
 | 
			
		||||
            .await
 | 
			
		||||
            .unwrap();
 | 
			
		||||
 | 
			
		||||
        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();
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,40 +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 {
 | 
			
		||||
    content: String,
 | 
			
		||||
    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 votes: VoteCount,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<NewPost> for Post {
 | 
			
		||||
    fn from(post: NewPost) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            uuid: Uuid::new_v4(),
 | 
			
		||||
            content: post.content,
 | 
			
		||||
            votes: VoteCount::new(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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 }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue