diff --git a/server/src/db.rs b/server/src/db.rs index 5d01141..0c694ee 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -1,5 +1,9 @@ -use crate::routes::{NewPost, Post}; -use log::warn; +use crate::routes::{Post, User}; +use argon2::{ + password_hash::{rand_core::OsRng, SaltString}, + Argon2, PasswordHasher, PasswordVerifier, +}; +use log::{info, warn}; use sqlx::SqlitePool; // Gets the latest posts from the database, ordered by created_at @@ -16,10 +20,13 @@ 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 { +pub async fn db_new_post(userid: i64, content: &str, pool: &SqlitePool) -> Option { + info!("User with id {} submitted a post", userid); + let insert_query = sqlx::query!( - "INSERT INTO posts (user_id, content) VALUES (1, ?)", - post.content + "INSERT INTO posts (user_id, content) VALUES (?, ?)", + userid, + content ) .execute(pool) .await; @@ -41,3 +48,83 @@ pub async fn db_new_post(post: NewPost, pool: &SqlitePool) -> Option { Some(post) } + +pub async fn db_user_exists(username: String, pool: &SqlitePool) -> bool { + let exists = sqlx::query!("SELECT username FROM users WHERE username = ?", username) + .fetch_one(pool) + .await + .ok() + .map(|row| row.username); + + exists.is_some() +} + +pub async fn db_user_login(username: String, password: String, pool: &SqlitePool) -> Option { + let username = username.clone(); + let user = sqlx::query_as!(User, "SELECT * FROM users WHERE username = ?", username) + .fetch_one(pool) + .await + .ok()?; + + let phc_password = user.password.clone(); + let phc_password = match argon2::PasswordHash::new(&phc_password) { + Ok(phc_password) => phc_password, + Err(_) => { + warn!( + "Invalid hash for user {} fetched from database (not a valid PHC string)", + username + ); + return None; + } + }; + + let argon2 = Argon2::default(); + let password = password.as_bytes(); + + match argon2.verify_password(password, &phc_password) { + Ok(_) => Some(user), + Err(_) => None, + } +} + +pub async fn db_new_user(username: String, password: String, pool: &SqlitePool) -> Option { + // First check if the user already exists + match db_user_exists(username.clone(), pool).await { + true => { + warn!("User \"{}\" already exists", username); + return None; + } + false => {} + } + + // Unwrapping here because if this fails, we have a serious problem + let phc_hash = Argon2::default() + .hash_password(password.as_bytes(), &SaltString::generate(&mut OsRng)) + .unwrap() + .to_string(); + + // Insert our new user into the database + let insert_query = sqlx::query!( + "INSERT INTO users (username, password) VALUES (?, ?)", + username, + phc_hash + ) + .execute(pool) + .await; + + match insert_query { + Ok(_) => { + info!("User: {} registered", username); + let user = sqlx::query_as!(User, "SELECT * FROM users WHERE username = ?", username) + .fetch_one(pool) + .await + .ok()?; + + Some(user) + } + Err(e) => { + warn!("Error inserting user into database: {}", e); + return None; + } + } +} diff --git a/server/src/jwt.rs b/server/src/jwt.rs index 5989cf9..d603c4d 100755 --- a/server/src/jwt.rs +++ b/server/src/jwt.rs @@ -36,7 +36,8 @@ pub fn token_factory(user: &str) -> JwtResult { Ok(token) } -#[allow(dead_code)] +// JwtResult is just a predefined error from the jsonwebtoken crate +// This function is incomplete and should be expanded to check for more things pub fn validate_token(token: &str) -> JwtResult { let token_data = decode::( token, diff --git a/server/src/routes/post.rs b/server/src/routes/post.rs index d0e5538..8380f97 100755 --- a/server/src/routes/post.rs +++ b/server/src/routes/post.rs @@ -1,4 +1,5 @@ use crate::db::{db_get_latest_posts, db_new_post}; +use crate::jwt::validate_token; use crate::ServerState; use actix_web::web::{Data, Query}; @@ -63,7 +64,29 @@ pub async fn get_posts( /// Creates a new post, requires a token in release mode #[post("/posts")] pub async fn new_post(new_post: Json, state: Data) -> Result { - return match db_new_post(new_post.into_inner(), &state.pool).await { + let user_claims = validate_token(&new_post.token); + + if let Err(e) = user_claims { + info!("Error validating token: {}", e); + return Ok(HttpResponse::BadRequest().json("Error")); + } + + // Bail if the token is invalid + let claims = user_claims.unwrap(); + info!("User {:?} created a new post", &claims.sub); + + let content = new_post.content.clone(); + let username = claims.sub.clone(); + + // This one is avoidable if we just store the user id in the token + let userid = sqlx::query!("SELECT id FROM users WHERE username = ?", username) + .fetch_one(&state.pool) + .await + .unwrap() + .id; + + // By now we know that the token is valid, so we can create the post + return match db_new_post(userid, &content, &state.pool).await { Some(post) => { info!("Created post {:?}", post.id); Ok(HttpResponse::Ok().json(post)) diff --git a/server/src/routes/users.rs b/server/src/routes/users.rs index ca47319..5b7d999 100755 --- a/server/src/routes/users.rs +++ b/server/src/routes/users.rs @@ -1,15 +1,12 @@ +use crate::db::{db_new_user, db_user_login}; 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, RngCore}; -use argon2::password_hash::SaltString; +use argon2::password_hash::rand_core::RngCore; use argon2::password_hash::*; -use argon2::Argon2; -use argon2::PasswordHasher; -use argon2::PasswordVerifier; use biosvg::BiosvgBuilder; use log::*; use serde::{Deserialize, Serialize}; @@ -21,7 +18,7 @@ pub struct LoginData { } #[derive(Debug, Serialize, Deserialize)] -struct LoginResponse { +pub struct LoginResponse { username: String, token: String, } @@ -38,75 +35,23 @@ pub async fn register( data: Json, state: Data, ) -> Result { - // 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(); - + db_new_user(data.username.clone(), data.password.clone(), &state.pool).await; info!("User: {} registered", data.username); Ok(HttpResponse::Ok().json("User registered")) } #[post("/login")] pub async fn login(data: Json, state: Data) -> Result { - let uname = data.username.clone(); - let q = sqlx::query!("SELECT password FROM users WHERE username = ?", uname) - .fetch_one(&state.pool) - .await - .ok(); + let result = db_user_login(data.username.clone(), data.password.clone(), &state.pool).await; - 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); + match result { + Some(_) => { return Ok(HttpResponse::Ok().json(LoginResponse { username: data.username.clone(), - token, + token: token_factory(&data.username).unwrap(), })); } - Err(_) => { + None => { info!("User \"{}\" failed to log in", data.username); return Ok(HttpResponse::BadRequest().json("Error")); } @@ -159,6 +104,7 @@ fn get_captcha() -> (String, String) { .length(4) .difficulty(6) .colors(vec![ + // Feel free to change these "#0078D6".to_string(), "#aa3333".to_string(), "#f08012".to_string(), diff --git a/server/src/state.rs b/server/src/state.rs index 3e4796b..93b1d1d 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -52,10 +52,9 @@ impl ServerState { async fn debug_setup(pool: &SqlitePool) -> Result<(), sqlx::Error> { use sqlx::query; - // Or ignore is just to silence the error if the user already exists - query!("INSERT OR IGNORE INTO users (username, password) VALUES ('testuser', 'testpassword')",) - .execute(pool) - .await?; + use crate::db::db_new_user; + + db_new_user("testuser".to_string(), "testpassword".to_string(), pool).await; // This requires that the user with id 1 exists in the user table query!("INSERT INTO posts (user_id, content) VALUES (1, 'Hello world!')",)