Registration endpoint fixing

This commit is contained in:
Imbus 2023-11-15 16:05:07 +01:00
parent f8dc9cfd29
commit dea4ac1fb3
8 changed files with 109 additions and 44 deletions

1
server/Cargo.lock generated
View file

@ -1851,6 +1851,7 @@ dependencies = [
"jsonwebtoken", "jsonwebtoken",
"lipsum", "lipsum",
"log", "log",
"rand",
"serde", "serde",
"serde_json", "serde_json",
"sled", "sled",

View file

@ -17,6 +17,7 @@ env_logger = "0.10.0"
jsonwebtoken = "8.3.0" jsonwebtoken = "8.3.0"
lipsum = "0.9.0" lipsum = "0.9.0"
log = "0.4.20" log = "0.4.20"
rand = "0.8.5"
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" }

View file

@ -6,7 +6,7 @@ use argon2::{
use log::{info, warn}; use log::{info, warn};
use sqlx::PgPool; use sqlx::PgPool;
// Gets the latest posts from the database, ordered by created_at /// Gets the latest posts from the database, ordered by created_at
pub async fn db_get_latest_posts(pool: &PgPool, limit: i64, offset: i64) -> Vec<Post> { pub async fn db_get_latest_posts(pool: &PgPool, limit: i64, offset: i64) -> Vec<Post> {
sqlx::query_as!( sqlx::query_as!(
Post, Post,
@ -19,7 +19,7 @@ pub async fn db_get_latest_posts(pool: &PgPool, limit: i64, offset: i64) -> Vec<
.unwrap() .unwrap()
} }
// Gets the post with id from the database /// Gets the post with id from the database
pub async fn db_get_post(id: i64, pool: &PgPool) -> Option<Post> { pub async fn db_get_post(id: i64, pool: &PgPool) -> Option<Post> {
sqlx::query_as!(Post, "SELECT * FROM posts WHERE id = $1", id) sqlx::query_as!(Post, "SELECT * FROM posts WHERE id = $1", id)
.fetch_one(pool) .fetch_one(pool)
@ -27,7 +27,7 @@ pub async fn db_get_post(id: i64, pool: &PgPool) -> Option<Post> {
.ok() .ok()
} }
// Inserts a new post to the database /// Inserts a new post to the database
pub async fn db_new_post(userid: i64, content: &str, pool: &PgPool) -> Option<Post> { pub async fn db_new_post(userid: i64, content: &str, pool: &PgPool) -> Option<Post> {
info!("User with id {} submitted a post", userid); info!("User with id {} submitted a post", userid);
@ -57,6 +57,7 @@ pub async fn db_new_post(userid: i64, content: &str, pool: &PgPool) -> Option<Po
Some(post) Some(post)
} }
/// Checks if the user exists in the database
pub async fn db_user_exists(username: String, pool: &PgPool) -> bool { pub async fn db_user_exists(username: String, pool: &PgPool) -> bool {
let exists = sqlx::query!("SELECT username FROM users WHERE username = $1", username) let exists = sqlx::query!("SELECT username FROM users WHERE username = $1", username)
.fetch_one(pool) .fetch_one(pool)
@ -67,6 +68,7 @@ pub async fn db_user_exists(username: String, pool: &PgPool) -> bool {
exists.is_some() exists.is_some()
} }
/// Checks if the user exists and if the password is correct
pub async fn db_user_login(username: String, password: String, pool: &PgPool) -> Option<User> { pub async fn db_user_login(username: String, password: String, pool: &PgPool) -> Option<User> {
let username = username.clone(); let username = username.clone();
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE username = $1", username) let user = sqlx::query_as!(User, "SELECT * FROM users WHERE username = $1", username)
@ -95,6 +97,7 @@ pub async fn db_user_login(username: String, password: String, pool: &PgPool) ->
} }
} }
/// Creates a new user if the username is not already taken
pub async fn db_new_user(username: String, password: String, pool: &PgPool) -> Option<User> { pub async fn db_new_user(username: String, password: String, pool: &PgPool) -> Option<User> {
// First check if the user already exists // First check if the user already exists
match db_user_exists(username.clone(), pool).await { match db_user_exists(username.clone(), pool).await {

View file

@ -8,10 +8,12 @@ mod db;
mod jwt; mod jwt;
mod routes; mod routes;
mod state; mod state;
mod util;
use routes::{get_posts, login, new_post, post_by_id, register}; use routes::{get_posts, login, new_post, post_by_id, register};
use state::CaptchaState; use state::CaptchaState;
use state::ServerState; use state::ServerState;
use util::hex_string;
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
@ -20,6 +22,15 @@ async fn main() -> std::io::Result<()> {
let data = ServerState::new().await; let data = ServerState::new().await;
let capt_db = CaptchaState::new(); let capt_db = CaptchaState::new();
#[cfg(debug_assertions)]
{
for _ in 0..10 {
let s = hex_string(10);
info!("Adding captcha key: {}", &s);
capt_db.capthca_db.lock().unwrap().insert(s);
}
}
info!("Spinning up server on http://localhost:8080"); info!("Spinning up server on http://localhost:8080");
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()

View file

@ -5,8 +5,6 @@ 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::RngCore;
use argon2::password_hash::*;
use biosvg::BiosvgBuilder; use biosvg::BiosvgBuilder;
use log::*; use log::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -18,7 +16,7 @@ pub struct LoginData {
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct LoginResponse { pub struct AuthResponse {
username: String, username: String,
token: String, token: String,
} }
@ -34,10 +32,31 @@ pub struct RegisterData {
pub async fn register( pub async fn register(
data: Json<RegisterData>, data: Json<RegisterData>,
state: Data<ServerState>, state: Data<ServerState>,
captcha_state: Data<CaptchaState>,
) -> Result<impl Responder> { ) -> Result<impl Responder> {
db_new_user(data.username.clone(), data.password.clone(), &state.pool).await; if !captcha_state
info!("User: {} registered", data.username); .capthca_db
Ok(HttpResponse::Ok().json("User registered")) .lock()
.unwrap()
.remove(&data.captcha)
{
info!("User failed to register, captcha was wrong");
return Ok(HttpResponse::BadRequest().json("Error"));
}
match db_new_user(data.username.clone(), data.password.clone(), &state.pool).await {
Some(user) => {
info!("User: {} registered", &user.username);
Ok(HttpResponse::Ok().json(AuthResponse {
username: user.username.clone(),
token: token_factory(&user.username).unwrap(),
}))
}
None => {
info!("User \"{}\" already exists", data.username);
return Ok(HttpResponse::BadRequest().json("Error"));
}
}
} }
#[post("/login")] #[post("/login")]
@ -46,7 +65,7 @@ pub async fn login(data: Json<LoginData>, state: Data<ServerState>) -> Result<im
match result { match result {
Some(_) => { Some(_) => {
return Ok(HttpResponse::Ok().json(LoginResponse { return Ok(HttpResponse::Ok().json(AuthResponse {
username: data.username.clone(), username: data.username.clone(),
token: token_factory(&data.username).unwrap(), token: token_factory(&data.username).unwrap(),
})); }));
@ -67,35 +86,38 @@ pub struct CaptchaResponse {
/// Request a captcha from the captcha service /// Request a captcha from the captcha service
#[post("/captcha")] #[post("/captcha")]
pub async fn captcha_request(cstate: Data<CaptchaState>) -> Result<impl Responder> { pub async fn captcha_request(cstate: Data<CaptchaState>) -> Result<impl Responder> {
unimplemented!("Captcha is currently disabled");
return Ok(HttpResponse::InternalServerError().json("Error"));
// This might block the thread a bit too long // This might block the thread a bit too long
let (answer, svg) = get_captcha(); // let (answer, svg) = get_captcha();
let id = rand_core::OsRng.next_u32() as i32; // let id = rand_core::OsRng.next_u32() as i32;
let cresponse = CaptchaResponse { // let cresponse = CaptchaResponse {
captcha_svg: svg.clone(), // captcha_svg: svg.clone(),
captcha_id: id, // captcha_id: id,
}; // };
// This is bad in about every way i can think of // 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 // 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 // handle rng and maybe set a trigger to delete old captchas
match cstate.capthca_db.lock() { // match cstate.capthca_db.lock() {
Ok(mut db) => { // Ok(mut db) => {
if (db.len() as i32) > 100 { // if (db.len() as i32) > 100 {
// To prevent the database from growing too large // // To prevent the database from growing too large
// Replace with a proper LRU cache or circular buffer // // Replace with a proper LRU cache or circular buffer
db.remove(&(id % 100)); // This is terrible // db.remove(&(id % 100)); // This is terrible
} // }
db.insert(id, answer.clone()); // We do not care about collisions // db.insert(id, answer.clone()); // We do not care about collisions
return Ok(HttpResponse::Ok().json(cresponse)); // return Ok(HttpResponse::Ok().json(cresponse));
} // }
Err(_) => { // Err(_) => {
// This shouldnt happen // // This shouldnt happen
error!("Failed to lock captcha database"); // error!("Failed to lock captcha database");
return Ok(HttpResponse::InternalServerError().json("Error")); // return Ok(HttpResponse::InternalServerError().json("Error"));
} // }
} // }
} }
/// Returns a new captcha in the form of a tuple (answer, svg) /// Returns a new captcha in the form of a tuple (answer, svg)

View file

@ -1,4 +1,4 @@
use std::collections::BTreeMap; use std::collections::BTreeSet;
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
@ -9,13 +9,14 @@ use sqlx::PgPool;
#[derive(Clone)] #[derive(Clone)]
pub struct CaptchaState { pub struct CaptchaState {
pub capthca_db: Arc<Mutex<BTreeMap<i32, String>>>, // pub capthca_db: Arc<Mutex<BTreeMap<i32, String>>>,
pub capthca_db: Arc<Mutex<BTreeSet<String>>>,
} }
impl CaptchaState { impl CaptchaState {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
capthca_db: Arc::new(Mutex::new(BTreeMap::new())), capthca_db: Arc::new(Mutex::new(BTreeSet::new())),
} }
} }
} }
@ -62,6 +63,7 @@ impl ServerState {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
async fn debug_setup(pool: &PgPool) -> Result<(), sqlx::Error> { async fn debug_setup(pool: &PgPool) -> Result<(), sqlx::Error> {
use lipsum::lipsum; use lipsum::lipsum;
use rand::prelude::*;
use sqlx::query; use sqlx::query;
use crate::db::db_new_user; use crate::db::db_new_user;
@ -69,26 +71,29 @@ async fn debug_setup(pool: &PgPool) -> Result<(), sqlx::Error> {
db_new_user("user".to_string(), "pass".to_string(), pool).await; db_new_user("user".to_string(), "pass".to_string(), pool).await;
// Check if the demo post already exists // Check if the demo post already exists
let posted = query!("SELECT * FROM posts WHERE id = 1",) let no_posts = query!("SELECT * FROM posts WHERE id = 1",)
.fetch_one(pool) .fetch_one(pool)
.await .await
.ok(); .ok()
.is_none();
// If the demo user already has a post, don't insert another one // If the demo user already has a post, don't insert another one
if !posted.is_some() { if no_posts {
// This requires that the user with id 1 exists in the user table let mut rng = rand::thread_rng();
query!("INSERT INTO posts (user_id, content) VALUES (1, 'Hello world! The demo username is user and the password is pass.')",)
.execute(pool)
.await?;
for _ in 0..10 { // This requires that the user with id 1 exists in the user table
for _ in 0..100 {
query!( query!(
"INSERT INTO posts (user_id, content) VALUES (1, $1)", "INSERT INTO posts (user_id, content) VALUES (1, $1)",
lipsum(50) lipsum(rng.gen_range(10..100))
) )
.execute(pool) .execute(pool)
.await?; .await?;
} }
query!("INSERT INTO posts (user_id, content) VALUES (1, 'Hello world! The demo username is user and the password is pass.')",)
.execute(pool)
.await?;
} }
Ok(()) Ok(())

3
server/src/util/mod.rs Normal file
View file

@ -0,0 +1,3 @@
mod util;
pub use util::*;

19
server/src/util/util.rs Normal file
View file

@ -0,0 +1,19 @@
use rand::{Rng, RngCore};
// This will do for now
pub fn hex_string(length: usize) -> String {
let mut rng = rand::thread_rng();
let mut bytes = vec![0u8; length];
rng.fill(&mut bytes[..]);
bytes.iter().map(|b| format!("{:X}", b)).collect::<String>()[..length].to_string()
}
mod tests {
use super::*;
#[test]
fn test_random_hex_string() {
let s = hex_string(16);
assert_eq!(s.len(), 16);
}
}