Working but untested captcha api
This commit is contained in:
parent
4c398a40f6
commit
fa9b2b6fc1
8 changed files with 107 additions and 12 deletions
7
justfile
7
justfile
|
@ -4,14 +4,15 @@ runtime := "podman"
|
||||||
dev: start-debug
|
dev: start-debug
|
||||||
@echo "Cd into client and run 'npm run dev' to start the client in dev mode."
|
@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:
|
npm-install directory:
|
||||||
cd {{directory}} && npm install
|
cd {{directory}} && npm install
|
||||||
|
|
||||||
# Builds the client with npm (result in client/dist)
|
# Builds the client with npm (result in client/dist)
|
||||||
[private]
|
[private]
|
||||||
npm-build: (npm-install "client-solid")
|
npm-build directory: (npm-install directory)
|
||||||
cd client && npm run build
|
cd {{directory}} && npm run build
|
||||||
|
@echo "Built client at {{directory}}/dist"
|
||||||
|
|
||||||
# Builds a debug container
|
# Builds a debug container
|
||||||
[private]
|
[private]
|
||||||
|
|
13
server/Cargo.lock
generated
13
server/Cargo.lock
generated
|
@ -391,6 +391,18 @@ version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
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]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
|
@ -1780,6 +1792,7 @@ dependencies = [
|
||||||
"actix-files",
|
"actix-files",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
"biosvg",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
|
|
@ -9,6 +9,7 @@ edition = "2021"
|
||||||
actix-files = "0.6.2"
|
actix-files = "0.6.2"
|
||||||
actix-web = "4.4.0"
|
actix-web = "4.4.0"
|
||||||
argon2 = { version = "0.5.2", features = ["zeroize"] }
|
argon2 = { version = "0.5.2", features = ["zeroize"] }
|
||||||
|
biosvg = "0.1.3"
|
||||||
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"
|
dotenvy = "0.15.7"
|
||||||
|
|
|
@ -17,25 +17,27 @@ pub async fn db_get_latest_posts(pool: &SqlitePool, limit: i64, offset: i64) ->
|
||||||
|
|
||||||
// Inserts a new post to the database
|
// Inserts a new post to the database
|
||||||
pub async fn db_new_post(post: NewPost, pool: &SqlitePool) -> Option<Post> {
|
pub async fn db_new_post(post: NewPost, pool: &SqlitePool) -> Option<Post> {
|
||||||
let q2 = sqlx::query!(
|
let insert_query = sqlx::query!(
|
||||||
"INSERT INTO posts (user_id, content) VALUES (1, ?)",
|
"INSERT INTO posts (user_id, content) VALUES (1, ?)",
|
||||||
post.content
|
post.content
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if q2.is_err() {
|
if insert_query.is_err() {
|
||||||
let s = q2.err().unwrap();
|
let s = insert_query.err().unwrap();
|
||||||
warn!("Error inserting post into database: {}", s);
|
warn!("Error inserting post into database: {}", s);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let q = sqlx::query_as!(
|
// Dips into the database to get the post we just inserted
|
||||||
|
let post = sqlx::query_as!(
|
||||||
Post,
|
Post,
|
||||||
"SELECT * FROM posts WHERE id = (SELECT MAX(id) FROM posts)"
|
"SELECT * FROM posts WHERE id = (SELECT MAX(id) FROM posts)"
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await
|
.await
|
||||||
.ok()?;
|
.ok()?;
|
||||||
Some(q)
|
|
||||||
|
Some(post)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ mod routes;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
use routes::{get_posts, login, new_post, register};
|
use routes::{get_posts, login, new_post, register};
|
||||||
|
use state::CaptchaState;
|
||||||
use state::ServerState;
|
use state::ServerState;
|
||||||
|
|
||||||
#[actix_web::main]
|
#[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();
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init();
|
||||||
|
|
||||||
let data = ServerState::new().await;
|
let data = ServerState::new().await;
|
||||||
|
let capt_db = CaptchaState::new();
|
||||||
|
|
||||||
info!("Spinning up server on http://localhost:8080");
|
info!("Spinning up server on http://localhost:8080");
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
|
@ -31,7 +33,8 @@ async fn main() -> std::io::Result<()> {
|
||||||
.service(new_post)
|
.service(new_post)
|
||||||
.service(login)
|
.service(login)
|
||||||
.service(register)
|
.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"))
|
.service(Files::new("/", "./public").index_file("index.html"))
|
||||||
})
|
})
|
||||||
|
|
|
@ -41,7 +41,7 @@ pub struct User {
|
||||||
// Note that these are optional
|
// Note that these are optional
|
||||||
/// Query parameters for the /posts endpoint
|
/// Query parameters for the /posts endpoint
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct QueryParams {
|
pub struct QueryParams {
|
||||||
limit: Option<i64>,
|
limit: Option<i64>,
|
||||||
offset: Option<i64>,
|
offset: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
use crate::jwt::token_factory;
|
use crate::jwt::token_factory;
|
||||||
|
use crate::state::CaptchaState;
|
||||||
use crate::ServerState;
|
use crate::ServerState;
|
||||||
|
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use actix_web::{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, RngCore};
|
||||||
use argon2::password_hash::SaltString;
|
use argon2::password_hash::SaltString;
|
||||||
use argon2::password_hash::*;
|
use argon2::password_hash::*;
|
||||||
use argon2::Argon2;
|
use argon2::Argon2;
|
||||||
use argon2::PasswordHasher;
|
use argon2::PasswordHasher;
|
||||||
use argon2::PasswordVerifier;
|
use argon2::PasswordVerifier;
|
||||||
|
use biosvg::BiosvgBuilder;
|
||||||
use log::*;
|
use log::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -101,7 +103,7 @@ pub async fn login(data: Json<LoginData>, state: Data<ServerState>) -> Result<im
|
||||||
println!("{:?}", token);
|
println!("{:?}", token);
|
||||||
return Ok(HttpResponse::Ok().json(LoginResponse {
|
return Ok(HttpResponse::Ok().json(LoginResponse {
|
||||||
username: data.username.clone(),
|
username: data.username.clone(),
|
||||||
token: token,
|
token,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
@ -110,3 +112,59 @@ pub async fn login(data: Json<LoginData>, state: Data<ServerState>) -> Result<im
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct CaptchaResponse {
|
||||||
|
captcha_svg: String,
|
||||||
|
captcha_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request a captcha from the captcha service
|
||||||
|
#[post("/captcha")]
|
||||||
|
pub async fn captcha_request(cstate: Data<CaptchaState>) -> Result<impl Responder> {
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,25 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use sqlx::Pool;
|
use sqlx::Pool;
|
||||||
use sqlx::Sqlite;
|
use sqlx::Sqlite;
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use sqlx::{self, sqlite};
|
use sqlx::{self, sqlite};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CaptchaState {
|
||||||
|
pub capthca_db: Arc<Mutex<BTreeMap<i32, String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CaptchaState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
capthca_db: Arc::new(Mutex::new(BTreeMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ServerState {
|
pub struct ServerState {
|
||||||
pub pool: Pool<Sqlite>,
|
pub pool: Pool<Sqlite>,
|
||||||
|
|
Loading…
Reference in a new issue