Splitting database access logic from routing code
This commit is contained in:
parent
fa9b2b6fc1
commit
508cf528af
5 changed files with 131 additions and 75 deletions
|
@ -1,5 +1,9 @@
|
||||||
use crate::routes::{NewPost, Post};
|
use crate::routes::{Post, User};
|
||||||
use log::warn;
|
use argon2::{
|
||||||
|
password_hash::{rand_core::OsRng, SaltString},
|
||||||
|
Argon2, PasswordHasher, PasswordVerifier,
|
||||||
|
};
|
||||||
|
use log::{info, warn};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
// Gets the latest posts from the database, ordered by created_at
|
// 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
|
// 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(userid: i64, content: &str, pool: &SqlitePool) -> Option<Post> {
|
||||||
|
info!("User with id {} submitted a post", userid);
|
||||||
|
|
||||||
let insert_query = sqlx::query!(
|
let insert_query = sqlx::query!(
|
||||||
"INSERT INTO posts (user_id, content) VALUES (1, ?)",
|
"INSERT INTO posts (user_id, content) VALUES (?, ?)",
|
||||||
post.content
|
userid,
|
||||||
|
content
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await;
|
.await;
|
||||||
|
@ -41,3 +48,83 @@ pub async fn db_new_post(post: NewPost, pool: &SqlitePool) -> Option<Post> {
|
||||||
|
|
||||||
Some(post)
|
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<User> {
|
||||||
|
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<User> {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -36,7 +36,8 @@ pub fn token_factory(user: &str) -> JwtResult<String> {
|
||||||
Ok(token)
|
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<Claims> {
|
pub fn validate_token(token: &str) -> JwtResult<Claims> {
|
||||||
let token_data = decode::<Claims>(
|
let token_data = decode::<Claims>(
|
||||||
token,
|
token,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::db::{db_get_latest_posts, db_new_post};
|
use crate::db::{db_get_latest_posts, db_new_post};
|
||||||
|
use crate::jwt::validate_token;
|
||||||
use crate::ServerState;
|
use crate::ServerState;
|
||||||
|
|
||||||
use actix_web::web::{Data, Query};
|
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
|
/// Creates a new post, requires a token in release mode
|
||||||
#[post("/posts")]
|
#[post("/posts")]
|
||||||
pub async fn new_post(new_post: Json<NewPost>, state: Data<ServerState>) -> Result<impl Responder> {
|
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 {
|
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) => {
|
Some(post) => {
|
||||||
info!("Created post {:?}", post.id);
|
info!("Created post {:?}", post.id);
|
||||||
Ok(HttpResponse::Ok().json(post))
|
Ok(HttpResponse::Ok().json(post))
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
|
use crate::db::{db_new_user, db_user_login};
|
||||||
use crate::jwt::token_factory;
|
use crate::jwt::token_factory;
|
||||||
use crate::state::CaptchaState;
|
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, RngCore};
|
use argon2::password_hash::rand_core::RngCore;
|
||||||
use argon2::password_hash::SaltString;
|
|
||||||
use argon2::password_hash::*;
|
use argon2::password_hash::*;
|
||||||
use argon2::Argon2;
|
|
||||||
use argon2::PasswordHasher;
|
|
||||||
use argon2::PasswordVerifier;
|
|
||||||
use biosvg::BiosvgBuilder;
|
use biosvg::BiosvgBuilder;
|
||||||
use log::*;
|
use log::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -21,7 +18,7 @@ pub struct LoginData {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct LoginResponse {
|
pub struct LoginResponse {
|
||||||
username: String,
|
username: String,
|
||||||
token: String,
|
token: String,
|
||||||
}
|
}
|
||||||
|
@ -38,75 +35,23 @@ pub async fn register(
|
||||||
data: Json<RegisterData>,
|
data: Json<RegisterData>,
|
||||||
state: Data<ServerState>,
|
state: Data<ServerState>,
|
||||||
) -> Result<impl Responder> {
|
) -> Result<impl Responder> {
|
||||||
// First check if the user already exists
|
db_new_user(data.username.clone(), data.password.clone(), &state.pool).await;
|
||||||
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);
|
info!("User: {} registered", data.username);
|
||||||
Ok(HttpResponse::Ok().json("User registered"))
|
Ok(HttpResponse::Ok().json("User registered"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/login")]
|
#[post("/login")]
|
||||||
pub async fn login(data: Json<LoginData>, state: Data<ServerState>) -> Result<impl Responder> {
|
pub async fn login(data: Json<LoginData>, state: Data<ServerState>) -> Result<impl Responder> {
|
||||||
let uname = data.username.clone();
|
let result = db_user_login(data.username.clone(), data.password.clone(), &state.pool).await;
|
||||||
let q = sqlx::query!("SELECT password FROM users WHERE username = ?", uname)
|
|
||||||
.fetch_one(&state.pool)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
if q.is_none() {
|
match result {
|
||||||
info!("User \"{}\" failed to log in", data.username);
|
Some(_) => {
|
||||||
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 {
|
return Ok(HttpResponse::Ok().json(LoginResponse {
|
||||||
username: data.username.clone(),
|
username: data.username.clone(),
|
||||||
token,
|
token: token_factory(&data.username).unwrap(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Err(_) => {
|
None => {
|
||||||
info!("User \"{}\" failed to log in", data.username);
|
info!("User \"{}\" failed to log in", data.username);
|
||||||
return Ok(HttpResponse::BadRequest().json("Error"));
|
return Ok(HttpResponse::BadRequest().json("Error"));
|
||||||
}
|
}
|
||||||
|
@ -159,6 +104,7 @@ fn get_captcha() -> (String, String) {
|
||||||
.length(4)
|
.length(4)
|
||||||
.difficulty(6)
|
.difficulty(6)
|
||||||
.colors(vec![
|
.colors(vec![
|
||||||
|
// Feel free to change these
|
||||||
"#0078D6".to_string(),
|
"#0078D6".to_string(),
|
||||||
"#aa3333".to_string(),
|
"#aa3333".to_string(),
|
||||||
"#f08012".to_string(),
|
"#f08012".to_string(),
|
||||||
|
|
|
@ -52,10 +52,9 @@ impl ServerState {
|
||||||
async fn debug_setup(pool: &SqlitePool) -> Result<(), sqlx::Error> {
|
async fn debug_setup(pool: &SqlitePool) -> Result<(), sqlx::Error> {
|
||||||
use sqlx::query;
|
use sqlx::query;
|
||||||
|
|
||||||
// Or ignore is just to silence the error if the user already exists
|
use crate::db::db_new_user;
|
||||||
query!("INSERT OR IGNORE INTO users (username, password) VALUES ('testuser', 'testpassword')",)
|
|
||||||
.execute(pool)
|
db_new_user("testuser".to_string(), "testpassword".to_string(), pool).await;
|
||||||
.await?;
|
|
||||||
|
|
||||||
// This requires that the user with id 1 exists in the user table
|
// This requires that the user with id 1 exists in the user table
|
||||||
query!("INSERT INTO posts (user_id, content) VALUES (1, 'Hello world!')",)
|
query!("INSERT INTO posts (user_id, content) VALUES (1, 'Hello world!')",)
|
||||||
|
|
Loading…
Reference in a new issue