diff --git a/justfile b/justfile index 7c73356..e0e6a37 100644 --- a/justfile +++ b/justfile @@ -4,14 +4,15 @@ runtime := "podman" dev: start-debug @echo "Cd into client and run 'npm run dev' to start the client in dev mode." -# Builds the client with npm (result in client/dist +[private] npm-install directory: cd {{directory}} && npm install # Builds the client with npm (result in client/dist) [private] -npm-build: (npm-install "client-solid") - cd client && npm run build +npm-build directory: (npm-install directory) + cd {{directory}} && npm run build + @echo "Built client at {{directory}}/dist" # Builds a debug container [private] diff --git a/server/Cargo.lock b/server/Cargo.lock index 93d4286..3dfcfc7 100755 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -391,6 +391,18 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "biosvg" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51c50785b88aca88dc4417a3aede395dac83b5031286f02523ddf4839c59e7f8" +dependencies = [ + "once_cell", + "rand", + "regex", + "thiserror", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -1780,6 +1792,7 @@ dependencies = [ "actix-files", "actix-web", "argon2", + "biosvg", "chrono", "clap", "dotenvy", diff --git a/server/Cargo.toml b/server/Cargo.toml index 8afda9d..f770f6e 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" actix-files = "0.6.2" actix-web = "4.4.0" argon2 = { version = "0.5.2", features = ["zeroize"] } +biosvg = "0.1.3" chrono = { version = "0.4.31", features = ["serde"] } clap = { version = "4.4.5", features = ["derive"] } dotenvy = "0.15.7" diff --git a/server/src/db.rs b/server/src/db.rs index 710fb97..5d01141 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -17,25 +17,27 @@ pub async fn db_get_latest_posts(pool: &SqlitePool, limit: i64, offset: i64) -> // Inserts a new post to the database pub async fn db_new_post(post: NewPost, pool: &SqlitePool) -> Option { - let q2 = sqlx::query!( + let insert_query = sqlx::query!( "INSERT INTO posts (user_id, content) VALUES (1, ?)", post.content ) .execute(pool) .await; - if q2.is_err() { - let s = q2.err().unwrap(); + if insert_query.is_err() { + let s = insert_query.err().unwrap(); warn!("Error inserting post into database: {}", s); return None; } - let q = sqlx::query_as!( + // Dips into the database to get the post we just inserted + let post = sqlx::query_as!( Post, "SELECT * FROM posts WHERE id = (SELECT MAX(id) FROM posts)" ) .fetch_one(pool) .await .ok()?; - Some(q) + + Some(post) } diff --git a/server/src/main.rs b/server/src/main.rs index 18e07d2..719a040 100755 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -11,6 +11,7 @@ mod routes; mod state; use routes::{get_posts, login, new_post, register}; +use state::CaptchaState; use state::ServerState; #[actix_web::main] @@ -18,6 +19,7 @@ async fn main() -> std::io::Result<()> { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init(); let data = ServerState::new().await; + let capt_db = CaptchaState::new(); info!("Spinning up server on http://localhost:8080"); HttpServer::new(move || { @@ -31,7 +33,8 @@ async fn main() -> std::io::Result<()> { .service(new_post) .service(login) .service(register) - .app_data(Data::new(data.clone())), + .app_data(Data::new(data.clone())) + .app_data(Data::new(capt_db.clone())), ) .service(Files::new("/", "./public").index_file("index.html")) }) diff --git a/server/src/routes/post.rs b/server/src/routes/post.rs index 2b71ca1..d0e5538 100755 --- a/server/src/routes/post.rs +++ b/server/src/routes/post.rs @@ -41,7 +41,7 @@ pub struct User { // Note that these are optional /// Query parameters for the /posts endpoint #[derive(Debug, Serialize, Deserialize)] -struct QueryParams { +pub struct QueryParams { limit: Option, offset: Option, } diff --git a/server/src/routes/users.rs b/server/src/routes/users.rs index 62f504b..ca47319 100755 --- a/server/src/routes/users.rs +++ b/server/src/routes/users.rs @@ -1,14 +1,16 @@ use crate::jwt::token_factory; +use crate::state::CaptchaState; 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::rand_core::{OsRng, RngCore}; use argon2::password_hash::SaltString; use argon2::password_hash::*; use argon2::Argon2; use argon2::PasswordHasher; use argon2::PasswordVerifier; +use biosvg::BiosvgBuilder; use log::*; use serde::{Deserialize, Serialize}; @@ -101,7 +103,7 @@ pub async fn login(data: Json, state: Data) -> Result { @@ -110,3 +112,59 @@ pub async fn login(data: Json, state: Data) -> Result) -> Result { + // This might block the thread a bit too long + let (answer, svg) = get_captcha(); + + let id = rand_core::OsRng.next_u32() as i32; + + let cresponse = CaptchaResponse { + captcha_svg: svg.clone(), + captcha_id: id, + }; + + // This is bad in about every way i can think of + // It might just be better to hit the database every time, and let the database + // handle rng and maybe set a trigger to delete old captchas + match cstate.capthca_db.lock() { + Ok(mut db) => { + if (db.len() as i32) > 100 { + // To prevent the database from growing too large + // Replace with a proper LRU cache or circular buffer + db.remove(&(id % 100)); // This is terrible + } + db.insert(id, answer.clone()); // We do not care about collisions + return Ok(HttpResponse::Ok().json(cresponse)); + } + Err(_) => { + // This shouldnt happen + error!("Failed to lock captcha database"); + return Ok(HttpResponse::InternalServerError().json("Error")); + } + } +} + +/// Returns a new captcha in the form of a tuple (answer, svg) +fn get_captcha() -> (String, String) { + BiosvgBuilder::new() + .length(4) + .difficulty(6) + .colors(vec![ + "#0078D6".to_string(), + "#aa3333".to_string(), + "#f08012".to_string(), + "#33aa00".to_string(), + "#aa33aa".to_string(), + ]) + .build() + .unwrap() +} diff --git a/server/src/state.rs b/server/src/state.rs index f43f91b..3e4796b 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -1,8 +1,25 @@ +use std::collections::BTreeMap; +use std::sync::Arc; +use std::sync::Mutex; + use sqlx::Pool; use sqlx::Sqlite; use sqlx::SqlitePool; use sqlx::{self, sqlite}; +#[derive(Clone)] +pub struct CaptchaState { + pub capthca_db: Arc>>, +} + +impl CaptchaState { + pub fn new() -> Self { + Self { + capthca_db: Arc::new(Mutex::new(BTreeMap::new())), + } + } +} + #[derive(Clone)] pub struct ServerState { pub pool: Pool,